build(www): 添加 Drone CI 流水线配置
- 新增 .drone.yml 文件用于定义 CI/CD 流程 - 配置了基于 Docker 的部署步骤 - 设置了工作区和卷映射以支持持久化数据 - 添加了构建准备阶段和 Docker 部署阶段 - 定义了环境变量和代理设置 - 配置了 artifacts 目录的处理逻辑 - 添加了 timezone 映射以确保时间同步 - 设置了 docker.sock 映射以支持 Docker in Docker
This commit is contained in:
178
blogs/database/redis/redis.md
Normal file
178
blogs/database/redis/redis.md
Normal file
@@ -0,0 +1,178 @@
|
||||
---
|
||||
title: 使用Java实现Redis客户端
|
||||
date: 2022-11-11
|
||||
sidebar: 'auto'
|
||||
tags:
|
||||
- Redis
|
||||
categories:
|
||||
- NoSQL
|
||||
- Java
|
||||
---
|
||||
## Redis通信协议-RESP协议
|
||||
Redis是一个CS架构的软件,通信一般分两步(不包括pipeline和PubSub):
|
||||
|
||||
客户端(client)向服务端(server)发送一条命令
|
||||
|
||||
服务端解析并执行命令,返回响应结果给客户端
|
||||
|
||||
因此客户端发送命令的格式、服务端响应结果的格式必须有一个规范,这个规范就是通信协议。
|
||||
|
||||
而在Redis中采用的是RESP(Redis Serialization Protocol)协议:
|
||||
- Redis 1.2版本引入了RESP协议
|
||||
- Redis 2.0版本中成为与Redis服务端通信的标准,称为RESP2
|
||||
- Redis 6.0版本中,从RESP2升级到了RESP3协议,增加了更多数据类型并且支持6.0的新特性--客户端缓存
|
||||
|
||||
但目前,默认使用的依然是RESP2协议,也是我们要学习的协议版本(以下简称RESP)。
|
||||
|
||||
在RESP中,通过首字节的字符来区分不同数据类型,常用的数据类型包括5种:
|
||||
|
||||
- 单行字符串:首字节是 ‘+’ ,后面跟上单行字符串,以CRLF( "\r\n" )结尾。例如返回"OK": "+OK\r\n"
|
||||
- 错误(Errors):首字节是 ‘-’ ,与单行字符串格式一样,只是字符串是异常信息,例如:"-Error message\r\n"
|
||||
- 数值:首字节是 ‘:’ ,后面跟上数字格式的字符串,以CRLF结尾。例如:":10\r\n"
|
||||
- 多行字符串:首字节是 ‘$’ ,表示二进制安全的字符串,最大支持512MB:
|
||||
- 如果大小为0,则代表空字符串:"$0\r\n\r\n"
|
||||
- 如果大小为-1,则代表不存在:"$-1\r\n"
|
||||
- 数组:首字节是 ‘*’,后面跟上数组元素个数,再跟上元素,元素数据类型不限
|
||||
|
||||
Redis支持TCP通信,因此我们可以使用Socket来模拟客户端,与Redis服务端建立连接:
|
||||
|
||||
```java
|
||||
public class RedisClient {
|
||||
/** Redis中 RESP 协议头 */
|
||||
private final static char SIMPLE_STRING = '+';
|
||||
private final static char ERRORS = '-';
|
||||
private final static char NUMBER = ':';
|
||||
private final static char MULTI_STRING = '$';
|
||||
private final static char ARRAY = '*';
|
||||
private final static String NEW_LINEAR = "\r\n";
|
||||
/** Redis服务端连接信息 */
|
||||
private final static String CONN_HOST = "localhost";
|
||||
private final static Integer CONN_PORT = 6379;
|
||||
|
||||
private Socket socket;
|
||||
private InputStream is;
|
||||
private OutputStream os;
|
||||
|
||||
public RedisClient() {
|
||||
try {
|
||||
// 使用Socket进行远程连接
|
||||
socket = new Socket(CONN_HOST, CONN_PORT);
|
||||
is = socket.getInputStream();
|
||||
os = socket.getOutputStream();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
public void sendRequest(String msg) throws IOException {
|
||||
// 解析命令
|
||||
String[] commands = parseCommand(msg);
|
||||
// 进行命令的拼接写出
|
||||
StringBuilder sb = new StringBuilder();
|
||||
// *3\r\n 命令数组头
|
||||
sb.append(ARRAY).append(commands.length).append(NEW_LINEAR);
|
||||
for (String command : commands) {
|
||||
// $4\r\nname\r\n
|
||||
sb.append(MULTI_STRING).append(command.length()).append(NEW_LINEAR).append(command).append(NEW_LINEAR);
|
||||
}
|
||||
os.write(sb.toString().getBytes(StandardCharsets.UTF_8));
|
||||
os.flush();
|
||||
}
|
||||
|
||||
private String[] parseCommand(String commandStr) {
|
||||
return commandStr.split(" ");
|
||||
}
|
||||
|
||||
public Object handlerResponse() throws IOException {
|
||||
int read = is.read();
|
||||
switch (read) {
|
||||
case SIMPLE_STRING:
|
||||
case ERRORS:
|
||||
return new BufferedReader(new InputStreamReader(is)).readLine();
|
||||
case NUMBER:
|
||||
return readNumber();
|
||||
case MULTI_STRING:
|
||||
return readMulti();
|
||||
case ARRAY:
|
||||
return readArrays();
|
||||
default:
|
||||
throw new RuntimeException("结果有误");
|
||||
}
|
||||
}
|
||||
|
||||
private Object readMulti() throws IOException {
|
||||
int count = readNumber().intValue();
|
||||
if (count == -1) {
|
||||
return null;
|
||||
}
|
||||
if (count == 0) {
|
||||
return "";
|
||||
}
|
||||
return readLine(count);
|
||||
}
|
||||
|
||||
private Long readNumber() throws IOException {
|
||||
byte[] bytes = new byte[1024];
|
||||
int temp;
|
||||
int count = 0;
|
||||
while ((temp = is.read()) != '\n') {
|
||||
if (temp != '\r') {
|
||||
bytes[count++] = (byte) temp;
|
||||
}
|
||||
}
|
||||
return Long.parseLong(new String(bytes, 0, count));
|
||||
}
|
||||
|
||||
private Object readArrays() throws IOException {
|
||||
int count = readNumber().intValue();
|
||||
List<Object> data = new ArrayList<>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
data.add(handlerResponse());
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private String readLine(int count) throws IOException {
|
||||
byte[] bytes = new byte[1024];
|
||||
for (int i = 0; i < count + NEW_LINEAR.length(); i++) {
|
||||
int temp = is.read();
|
||||
if (temp != '\r' && temp != '\n') {
|
||||
bytes[i] = (byte) temp;
|
||||
}
|
||||
}
|
||||
return new String(bytes, 0, count, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
if (is != null) {
|
||||
is.close();
|
||||
}
|
||||
if (os != null) {
|
||||
os.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
RedisClient redisClient = new RedisClient();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
|
||||
while (true) {
|
||||
System.out.print("[INFO] 请输入redis命令>>> ");
|
||||
String command = reader.readLine();
|
||||
if (command.equalsIgnoreCase("exit")) {
|
||||
redisClient.close();
|
||||
break;
|
||||
}
|
||||
redisClient.sendRequest(command);
|
||||
System.out.println(redisClient.handlerResponse());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
315
blogs/frontend/promise.md
Normal file
315
blogs/frontend/promise.md
Normal file
@@ -0,0 +1,315 @@
|
||||
---
|
||||
title: Promise使用之道
|
||||
date: 2022-01-25
|
||||
sidebar: 'auto'
|
||||
tags:
|
||||
- Promise
|
||||
categories:
|
||||
- JavaScript
|
||||
---
|
||||
## 1 Promise的理解和使用
|
||||
|
||||
Promise 是异步编程的一种解决方案,主要用来解决**回调地狱**的问题,可以有效的减少回调嵌套。真正解决需要配合async/await
|
||||
|
||||
* 抽象表达:
|
||||
* Promise 是一门新的技术(ES6 规范)
|
||||
* Promise 是 JS 中进行异步编程的新解决方案
|
||||
> 备注:旧方案是单纯使用回调函数
|
||||
|
||||
* 具体表达:
|
||||
* 从语法上来说: Promise 是一个构造函数
|
||||
* 从功能上来说: promise 对象用来封装一个异步操作并可以获取其成功/ 失败的结果值
|
||||
|
||||
### 1.1 Promise的状态
|
||||
|
||||
* 实例对象中的一个属性 『PromiseState』
|
||||
* pending 未决定的
|
||||
* resolved / fullfilled 成功
|
||||
* rejected 失败
|
||||
|
||||
> 说明: 只有两种状态(resovled||rejectd), 且一个 promise 对象只能改变一次 无论变为成功还是失败, 都会有一个结果数据 成功的结果数据一般称为 value, 失败的结果数据一般称为 reason
|
||||
|
||||

|
||||
|
||||
### 1.2 Promise的基本使用
|
||||
|
||||
1. 延时器的使用来启动异步任务
|
||||
```js
|
||||
const timeOutPromise = new Promise((resolve, reject) => {
|
||||
console.log("启动异步任务");
|
||||
setTimeout(() => {
|
||||
console.log("异步任务开始执行...");
|
||||
let time = Date.now();
|
||||
if( time % 2 === 0 ){
|
||||
console.log("执行成功了");
|
||||
resolve(time);
|
||||
}else{
|
||||
console.log("执行失败了");
|
||||
reject(time);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
// 异步任务的执行结果处理
|
||||
timeOutPromise
|
||||
.then(value => console.log(value),
|
||||
reason => console.log(reason))
|
||||
```
|
||||
2. ajax请求使用Promise
|
||||
```js
|
||||
// 获取原生ajax对象
|
||||
const XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;
|
||||
const ajaxPromise = new Promise((resolve, reject) => {
|
||||
// 获取原生请求对象
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'http://aopt.xyz:3366/test/hello');
|
||||
// 响应结果的回调函数
|
||||
xhr.onreadystatechange = () => {
|
||||
if( xhr.readyState !== 4 ) return;
|
||||
// 将请求的数据通过解构赋值
|
||||
const {status, responseText} = xhr;
|
||||
if( status >= 200 && status < 300 ){
|
||||
resolve(responseText);
|
||||
}else{
|
||||
reject(status);
|
||||
}
|
||||
}
|
||||
// 将请求进行发送
|
||||
xhr.send();
|
||||
})
|
||||
|
||||
ajaxPromise
|
||||
.then(value => console.log(value),
|
||||
reason => console.log(reason))
|
||||
```
|
||||
3. fs模块使用Promise
|
||||
```js
|
||||
const fs = require('fs')
|
||||
// 创建一个用来读取文件的函数
|
||||
const fsPromise = path => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 进行文件的读取, 第二个参数是以错误为优先的回调函数
|
||||
fs.readFile(path, (err, data) => {
|
||||
if( err ) reject(err);
|
||||
resolve(data);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fsPromise('./笔记.md')
|
||||
.then(value => console.log(value.toString()),
|
||||
reason => console.log(reason))
|
||||
```
|
||||
4. 异常穿透的Promise
|
||||
```js
|
||||
fsPromise('./aksdhfa.txt')
|
||||
.then()
|
||||
// 捕捉该Promise中出现的异常, 然后进行处理
|
||||
.catch(err => console.log("这是catch捕捉到的异常"))
|
||||
```
|
||||
5. `util.promisify`方法, 直接将函数变成Promise封装的方式
|
||||
```js
|
||||
const util = require('util')
|
||||
const fs = require('fs')
|
||||
let creatorFsPromise = util.promisify(fs.readFile);
|
||||
creatorFsPromise('./笔记.md')
|
||||
.then(value => console.log("这是promisify方法"))
|
||||
```
|
||||
|
||||
## 2 为什么使用Promise
|
||||
|
||||
1. 指定回调函数的方式更加灵活
|
||||
* 旧的: 必须在启动异步任务前指定
|
||||
* promise: 启动异步任务 => 返回promie对象 => 给promise对象绑定回调函 数(甚至可以在异步任务结束后指定/多个)
|
||||
2. 持链式调用, 可以解决回调地狱问题
|
||||
1. 什么是回调地狱
|
||||
* 回调函数嵌套调用, 外部回调函数异步执行的结果是嵌套的回调执行的条件
|
||||
2. 回调地狱的缺点?
|
||||
* 不便于阅读 不便于异常处理
|
||||
3. 解决方案?
|
||||
* promise 链式调用,用来解决回调地狱问题,但是只是简单的改变格式,并没有彻底解决上面的问题真正要解决上述问题,一定要利用promise再加上await和async关键字实现异步传同步
|
||||
4. 终极解决方案?
|
||||
* promise + async/await
|
||||
|
||||
## 3 Promise中常用的API
|
||||
|
||||
### 3.1 Promise 构造函数: Promise(excutor){}
|
||||
|
||||
(1) executor 函数: 执行器 (resolve, reject) => {}
|
||||
|
||||
(2) resolve 函数: 内部定义成功时我们调用的函数 value => {}
|
||||
|
||||
(3) reject 函数: 内部定义失败时我们调用的函数 reason => {}
|
||||
|
||||
> 说明: executor 会在 Promise 内部立即同步调用,异步操作在执行器中执行,换话说Promise支持同步也支持异步操作
|
||||
|
||||
### 3.2 Promise.prototype.then 方法: (onResolved, onRejected) => {}
|
||||
|
||||
(1) onResolved 函数: 成功的回调函数 (value) => {}
|
||||
|
||||
(2) onRejected 函数: 失败的回调函数 (reason) => {}
|
||||
|
||||
> 说明: 指定用于得到成功 value 的成功回调和用于得到失败 reason 的失败回调 返回一个新的 promise 对象
|
||||
|
||||
### 3.3 Promise.prototype.catch 方法: (onRejected) => {}
|
||||
|
||||
(1) onRejected 函数: 失败的回调函数 (reason) => {}
|
||||
|
||||
> 说明: then()的语法糖, 相当于: then(undefined, onRejected)
|
||||
|
||||
(2) 异常穿透使用: 当运行到最后, 没被处理的所有异常错误都会进入这个方法的回调函数中
|
||||
|
||||
### 3.4 Promise.resolve 方法: (value) => {}
|
||||
|
||||
(1) value: 成功的数据或 promise 对象
|
||||
|
||||
> 说明: 返回一个成功/失败的 promise 对象,直接改变promise状态
|
||||
|
||||
### 3.5 Promise.reject 方法: (reason) => {}
|
||||
|
||||
(1) reason: 失败的原因
|
||||
|
||||
> 说明: 返回一个失败的 promise 对象,直接改变promise状态
|
||||
|
||||
### 3.6 Promise.all 方法: (promises) => {}
|
||||
|
||||
promises: 包含 n 个 promise 的数组
|
||||
|
||||
> 说明: 返回一个新的 promise, 只有所有的 promise 都成功才成功, 只要有一 个失败了就直接失败
|
||||
|
||||
```js
|
||||
let p1 = new Promise((resolve, reject) => { resolve('成功'); })
|
||||
let p2 = Promise.reject('错误错误错误');
|
||||
let p3 = Promise.resolve('也是成功')
|
||||
const result = Promise.all([p1, p2, p3]);
|
||||
console.log(result);
|
||||
```
|
||||
|
||||
### 3.7 Promise.race 方法: (promises) => {}
|
||||
|
||||
(1) promises: 包含 n 个 promise 的数组
|
||||
|
||||
> 说明: 返回一个新的 promise, 第一个完成的 promise 的结果状态就是最终的结果状态, 如p1延时,开启了异步,内部正常是同步进行,所以p2>p3>p1,结果是P2
|
||||
|
||||
```js
|
||||
let p1 = new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve('OK');
|
||||
}, 1000);
|
||||
})
|
||||
let p2 = Promise.resolve('Success');
|
||||
let p3 = Promise.resolve('Oh Yeah');
|
||||
//调用
|
||||
const result = Promise.race([p1, p2, p3]);
|
||||
(result.then(value => console.log(value));
|
||||
```
|
||||
|
||||
## 4 Promise的几个关键问题
|
||||
|
||||
### 4.1 如何改变 promise 的状态?
|
||||
|
||||
(1) resolve(value): 如果当前是 pending 就会变为 resolved
|
||||
|
||||
(2) reject(reason): 如果当前是 pending 就会变为 rejected
|
||||
|
||||
(3) 抛出异常: 如果当前是 pending 就会变为 rejected
|
||||
|
||||
### 4.2 一个 promise 指定多个成功/失败回调函数, 都会调用吗?
|
||||
|
||||
当 promise 改变为对应状态时都会调用, 改变状态后, 多个回调函数都会调用, 并不会自动停止(多次调用then)
|
||||
|
||||
### 4.3 改变 promise 状态和指定回调函数谁先谁后?
|
||||
|
||||
(1) 都有可能, 正常情况下是先指定回调再改变状态, 但也可以先改状态再指定回调
|
||||
|
||||
* 先指定回调再改变状态(`异步`): 先指定回调--> 再改变状态 -->改变状态后才进入异步队列执行回调函数
|
||||
* 先改状态再指定回调(`同步`): 改变状态 -->指定回调 并马上执行回调
|
||||
|
||||
(2) 如何先改状态再指定回调? -->注意: 指定并不是执行
|
||||
|
||||
① 在执行器中直接调用 resolve()/reject() -->即,不使用定时器等方法,执行器内直接同步操作
|
||||
|
||||
② 延迟更长时间才调用 then() -->即,在`.then()`这个方法外再包一层例如延时器这种方法
|
||||
|
||||
(3) 什么时候才能得到数据?
|
||||
|
||||
① 如果先指定的回调, 那当状态发生改变时, 回调函数就会调用, 得到数据(异步)
|
||||
|
||||
② 如果先改变的状态, 那当指定回调时, 回调函数就会调用, 得到数据(同步)
|
||||
|
||||
(4) 理解--结合源码
|
||||
|
||||
* 源码中,promise的状态是通过一个`默认为padding`的变量进行判断,所以当你`resolve/reject`延时(异步导致当then加载时,状态还未修改)后,这时直接进行`p.then()`会发现,目前状态还是进行中,所以只是这样导致只有同步操作才能成功.
|
||||
* 所以promise将传入的`回调函数`拷贝到promise对象实例上,然后在`resolve/reject`的执行过程中再进行调用,达到异步的目的
|
||||
* 具体代码实现看下方自定义promise
|
||||
|
||||
### 4.4 promise.then()返回的新 promise 的结果状态由什么决定?
|
||||
|
||||
(1) 简单表达: 由 then()指定的回调函数执行的结果决定
|
||||
|
||||
(2) 详细表达:
|
||||
|
||||
① 如果抛出异常, 新 promise 变为 rejected, reason 为抛出的异常
|
||||
|
||||
② 如果返回的是非 promise 的任意值, 新 promise 变为 resolved, value 为返回的值
|
||||
|
||||
③ 如果返回的是另一个新 promise, 此 promise 的结果就会成为新 promise 的结果
|
||||
|
||||
### 4.5 promise 如何串连多个操作任务?
|
||||
|
||||
(1) promise 的 then()返回一个新的 promise, 可以开成 then()的链式调用
|
||||
|
||||
(2) 通过 then 的链式调用串连多个同步/异步任务,这样就能用then()将多个同步或异步操作串联成一个同步队列
|
||||
|
||||
### 4.6 promise 异常传透?
|
||||
|
||||
* 当使用 promise 的 then 链式调用时, 可以在最后指定失败的回调
|
||||
* 前面任何操作出了异常, 都会传到最后失败的回调中处理
|
||||
|
||||
### 4.7 中断 promise 链?
|
||||
|
||||
(1) 当使用 promise 的 then 链式调用时, 在中间中断, 不再调用后面的回调函数
|
||||
|
||||
(2) 办法: 在回调函数中返回一个 pendding 状态的promise 对象
|
||||
|
||||
```js
|
||||
let p = new Promise((resolve, reject) => {setTimeout(() => { resolve('OK');}, 1000);});
|
||||
|
||||
p.then(value => {return new Promise(() => {});})//有且只有这一个方式
|
||||
.then(value => { console.log(222);})
|
||||
.then(value => { console.log(333);})
|
||||
.catch(reason => {console.warn(reason);});
|
||||
```
|
||||
|
||||
## 5 Promise + async + await
|
||||
|
||||
1. Promise ==> 异步
|
||||
|
||||
2. await ==> 异步转同步
|
||||
|
||||
1. await 可以理解为是async wait的简写。await 必须出现在 async 函数内部,不能单独使用。
|
||||
2. await 后面可以跟任何的JS 表达式。虽然说 await 可以等很多类型的东西,但是它最主要的意图是用来等待 Promise 对象的状态被 resolved。如果await的是 promise对象会造成异步函数停止执行并且等待 promise 的解决,如果等的是正常的表达式则立即执行
|
||||
|
||||
3. async ==> 同步转异步
|
||||
|
||||
1. 方法体内部的某个表达式使用await修饰,那么这个方法体所属方法必须要用async修饰所以使用awit方法会自动升级为异步方法
|
||||
|
||||
4. mdn文档
|
||||
|
||||
* [async](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function)
|
||||
* [await](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/await)
|
||||
|
||||
### 5.1 async函数
|
||||
|
||||
1. 函数的返回值为 promise 对象
|
||||
2. promise 对象的结果由 async 函数执行的返回值决定
|
||||
|
||||
### 5.2 await表达式
|
||||
|
||||
1. await 右侧的表达式一般为 promise 对象, 但也可以是其它的值
|
||||
2. 如果表达式是 promise 对象, await 返回的是 promise 成功的值
|
||||
3. 如果表达式是其它值, 直接将此值作为 await 的返回值
|
||||
|
||||
### 5.3 注意
|
||||
|
||||
1. await 必须写在 async 函数中, 但 async 函数中可以没有 await
|
||||
2. 如果 await 的 promise 失败了, 就会抛出异常, 需要通过 try...catch 捕获处理
|
||||
284
blogs/frontend/typescript.md
Normal file
284
blogs/frontend/typescript.md
Normal file
@@ -0,0 +1,284 @@
|
||||
---
|
||||
title: Typescript-基本语法
|
||||
date: 2022-02-27
|
||||
sidebar: 'auto'
|
||||
tags:
|
||||
- TypeScript
|
||||
categories:
|
||||
- JavaScript
|
||||
---
|
||||
## 1 Typescript安装
|
||||
- 安装命令: `npm i typescript -g`
|
||||
- 使用Typescript命令查看安装的版本号: `tsc -v`
|
||||
- 生成配置文件tsconfig.json: `tsc -init`
|
||||
|
||||
### 1.1 tsc常用编译参数
|
||||
|
||||
|<div style="width:120px">编译参数</div>|说明|
|
||||
|:--|:--|
|
||||
|--help | 显示帮助信息
|
||||
|--module | 载入扩展模块
|
||||
|--target | 设置 ECMA 版本
|
||||
|--declaration | 额外生成一个 .d.ts 扩展名的文件。`tsc ts-hw.ts --declaration` 以上命令会生成 ts-hw.d.ts、ts-hw.js 两个文件。
|
||||
|--removeComments | 删除文件的注释
|
||||
|--out | 编译多个文件并合并到一个输出的文件
|
||||
|--sourcemap | 生成一个 sourcemap (.map) 文件。sourcemap 是一个存储源代码与编译代码对应位置映射的信息文件。
|
||||
|--module noImplicitAny | 在表达式和声明上有隐含的 any 类型时报错
|
||||
--watch | 在监视模式下运行编译器。会监视输出文件,在它们改变时重新编译。
|
||||
|
||||
|
||||
## 2 Typescript基础类型
|
||||
|
||||
|<div style="width:80px">数据类型</div>|<div style="width:80px">关键字</div>|描述|
|
||||
|:--|:--|:--|
|
||||
|任意类型| any | 声明为 any 的变量可以赋予任意类型的值。`let x:any`
|
||||
|数字类型| number |双精度 64 位浮点值。它可以用来表示整数和分数。`let num:number`
|
||||
|字符串类型| string|一个字符系列,使用单引号(')或双引号(")来表示字符串类型。反引号(`)来定义多行文本和内嵌表达式。
|
||||
|布尔类型| boolean |表示逻辑值:true 和 false。
|
||||
|数组类型 |无 |声明变量为数组。`let arr:number[];let arr:Array<number>;// 泛型定义`
|
||||
|元组 |无 |元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。`let x:[string, number] = ['string', 15]`
|
||||
|枚举| enum | 枚举类型用于定义数值集合。`enum Color{Red, Green};// 没有赋值默认从0开始`
|
||||
|void| void |用于标识方法返回值的类型,表示该方法没有返回值。`function hello(): void {}`
|
||||
|null |null |表示对象值缺失。
|
||||
|undefined| undefined |用于初始化变量为一个未定义的值
|
||||
|never |never |never 是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。
|
||||
|
||||
|
||||
## 3 Typescript接口
|
||||
|
||||
### 3.1 属性类型接口
|
||||
```ts
|
||||
// 对传入对象的属性约束, 以下是一个约束属性接口
|
||||
interface User{
|
||||
username: string;
|
||||
password?: string; // 可选传递
|
||||
}
|
||||
|
||||
function save(user:User):void{
|
||||
console.log(user)
|
||||
}
|
||||
|
||||
let info = {
|
||||
username: "张三",
|
||||
password: "qwe123",
|
||||
age: 15 // 额外传递的参数
|
||||
}
|
||||
// 传递一个自定义对象的参数需要有必选参数, 其他的属性不会进行检查
|
||||
save(info)
|
||||
// 如果直接进行对象传递那么会进行类型的检查
|
||||
save({
|
||||
username: "张三",
|
||||
password: "qwe123"
|
||||
})
|
||||
```
|
||||
|
||||
### 3.2 函数类型接口
|
||||
```ts
|
||||
// 定义一个函数类型的接口
|
||||
interface encrypt {
|
||||
(key: string, value: string): string;
|
||||
}
|
||||
// 既然类型定义为接口类型(参数列表可以不用写类型), 那么就得符合接口的类型
|
||||
let md5: encrypt = (key, value): string => {
|
||||
return `MD5://${key}.${value}`
|
||||
}
|
||||
// 发现类型换成any也行, 因为any能接受所有类型
|
||||
let sha1: encrypt = (key: any, value: any): any => {
|
||||
return `SHA1://${key}.${value}`
|
||||
}
|
||||
console.log(md5("qwe", "123"))
|
||||
// 但是发现还是会被类型约束, 所以就算定义了any类型也没有什么起效
|
||||
// sha1(123, 564) // 错误写法
|
||||
console.log(sha1("asd", "zxc"))
|
||||
```
|
||||
|
||||
### 3.3 可索引型接口
|
||||
```ts
|
||||
// 可索引型数组接口, []中为number, 返回值为string
|
||||
interface CustomArr {
|
||||
[index: number]: string;
|
||||
}
|
||||
let arr: CustomArr = ['111', '222']
|
||||
console.log(arr[1])
|
||||
|
||||
// 可索引型对象接口, key为string, value为string
|
||||
interface CustomObj {
|
||||
[index: string]: string;
|
||||
}
|
||||
let obj: CustomObj = {name: 'zhang', sex: '男'}
|
||||
console.log(obj['name'])
|
||||
console.log(obj.sex)
|
||||
```
|
||||
|
||||
### 3.44 类类型接口
|
||||
与面向对象语言相似(Java)
|
||||
|
||||
## 4 Typescript泛型
|
||||
软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据,这样用户就可以以自己的数据类型来使用组件。
|
||||
|
||||
通俗理解:泛型就是解决类、接口、方法的复用性、以及对不特定数据类型的支持。
|
||||
|
||||
## 5 Typescript装饰器
|
||||
> 对修饰器的实验支持功能在将来的版本中可能更改。
|
||||
>
|
||||
> 在"tsconfig"或"jsconfig"中设置"experimentalDecorators"选项以删除此警告。
|
||||
>
|
||||
> "experimentalDecorators":true // 启用对ES7装饰器的实验性支持。
|
||||
|
||||
装饰器是一种特殊类型的声明,它能够被附加到类、方法、属性或参数上,可以修改类的行为,通俗的讲装饰器就是一个方法,可以注入到类、方法、属性或参数上来扩展类、方法、属性或参数的功能。常见的装饰器有:类装饰器、方法装饰器、属性装饰器、参数装饰器。
|
||||
|
||||
装饰器的写法:普通装饰器(无法传参)、装饰器工厂(可传参),装饰器是过去几年中JS最大的成就之一,已是ES7的标准特性之一。
|
||||
|
||||
### 5.1 类修饰器
|
||||
+ 类装饰器: 普通装饰器(无法传参)
|
||||
```ts
|
||||
function logClass(param: any){
|
||||
// 获取到的参数是该实例化对象
|
||||
console.log(param)
|
||||
// 在这里可以对实例话对象进行动态挂载数据
|
||||
param.prototype.apiUrl = 'http://localhost'
|
||||
param.prototype.run = () => {
|
||||
console.log("动态扩展的run方法")
|
||||
}
|
||||
}
|
||||
|
||||
// 该类装饰器不能传递参数
|
||||
@logClass
|
||||
class HttpClient {}
|
||||
|
||||
let http:any = new HttpClient()
|
||||
console.log(http.apiUrl)
|
||||
http.run()
|
||||
```
|
||||
|
||||
+ 类装饰器: 装饰器工厂(可传参)
|
||||
```ts
|
||||
function logClass(...params: string[]) {
|
||||
return function(target: any) {
|
||||
// target是当前类的对象
|
||||
console.log(target)
|
||||
// params是装饰器中传递的参数, 可以定义多个参数
|
||||
target.prototype.apiUrl = params[0] + params[1]
|
||||
}
|
||||
}
|
||||
|
||||
@logClass('http://localhost', "/api/users")
|
||||
class HttpClient{}
|
||||
|
||||
let http: any = new HttpClient()
|
||||
console.log(http.apiUrl)
|
||||
```
|
||||
|
||||
### 5.2 属性修饰器
|
||||
属性装饰器会被应用到属性描述上,可以用来监视、修改或者替换属性的值。
|
||||
|
||||
属性装饰器会在运行时传入下列2个参数:
|
||||
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
|
||||
- 成员的名字。
|
||||
```ts
|
||||
function logProperty(params: string) {
|
||||
return function(target: any, attr: any) {
|
||||
// 装饰器传递的参数
|
||||
console.log(params)
|
||||
// 给类的属性挂载数据
|
||||
target[attr] = params
|
||||
}
|
||||
}
|
||||
|
||||
class HttpClient {
|
||||
@logProperty("http://localhost")
|
||||
apiUrl: string | undefined;
|
||||
|
||||
getApiUrl() {
|
||||
console.log(this.apiUrl)
|
||||
}
|
||||
}
|
||||
|
||||
let http: HttpClient = new HttpClient()
|
||||
http.getApiUrl()
|
||||
```
|
||||
|
||||
### 5.3 方法修饰器
|
||||
方法装饰器会被应用到方法描述上,可以用来监视、修改或者替换方法定义。
|
||||
|
||||
方法装饰器会在运行时传入下列3个参数:
|
||||
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
|
||||
- 成员的名字。
|
||||
- 成员的属性描述符。
|
||||
```ts
|
||||
class HttpClient {
|
||||
public url: any | undefined;
|
||||
|
||||
@get("http://localhost")
|
||||
getData() {
|
||||
console.log(this.url);
|
||||
}
|
||||
}
|
||||
|
||||
function get(param: string){
|
||||
return function(target: any, methodName: any, desc: any){
|
||||
console.log(target)
|
||||
console.log(methodName)
|
||||
console.log(desc)
|
||||
// 可以通过类的原型对象target对该实例进行数据挂载
|
||||
target.url = param
|
||||
target.run = function(){
|
||||
console.log('run')
|
||||
}
|
||||
// 可以修改装饰器所放置方法的实现, 以下的代码就是获取该方法的内容
|
||||
console.log(desc.value)
|
||||
}
|
||||
}
|
||||
|
||||
let http: any = new HttpClient()
|
||||
http.getData()
|
||||
http.run()
|
||||
```
|
||||
|
||||
### 5.4 参数修饰器
|
||||
参数装饰器表达式会在运行时当作函数被调用,可以使用参数装饰器为类的原型增加一些元素数据 ,传入下列3个参数:
|
||||
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
|
||||
- 方法的名字。
|
||||
- 参数在函数参数列表中的索引。
|
||||
```ts
|
||||
function logParams(params: any) {
|
||||
return function (target: any, methodName: any, paramsIndex: any) {
|
||||
console.log(target);
|
||||
console.log(methodName);
|
||||
console.log(paramsIndex);
|
||||
target.apiUrl = params;
|
||||
}
|
||||
}
|
||||
|
||||
class HttpClient {
|
||||
getData(name: string, @logParams("10086") uuid: any) {
|
||||
console.log(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
let http: any = new HttpClient();
|
||||
http.getData('zhang', 123456);
|
||||
console.log(http.apiUrl);
|
||||
```
|
||||
|
||||
### 5.5 修饰器的执行顺序
|
||||
装饰器执行顺序:++属性 > 方法 > 方法参数 > 类++
|
||||
|
||||
如果有多个**同类型**的装饰器, 那么会++从后往前执行++
|
||||
|
||||
## 6 Typescript模块化
|
||||
模块化的好处
|
||||
- 防止命名冲突
|
||||
- 代码复用
|
||||
- 高维护性
|
||||
|
||||
模块功能主要由两个命令构成:export 和 import。
|
||||
- export 命令用于规定模块的对外接口
|
||||
- import 命令用于输入其它模块提供的功能
|
||||
|
||||
## 7 Typescript命名空间
|
||||
命名空间:在代码量较大的情况下,为了避免各种变量命名相冲突,可将相似功能的函数、类、接口等放置到命名空间内,同Java的包、.Net的命名空间一样,TypeScript的命名空间可以将代码包裹起来,只对外暴露需要在外部访问的对象,命名空间内的对象通过export关键字对外暴露。
|
||||
|
||||
命名空间和模块的区别:
|
||||
- 命名空间:内部模块,主要用于组织代码,避免命名冲突。
|
||||
- 模块:ts的外部模块的简称,侧重代码的复用,一个模块里可能会有多个命名空间。
|
||||
561
blogs/java/java8/java8-1.md
Normal file
561
blogs/java/java8/java8-1.md
Normal file
@@ -0,0 +1,561 @@
|
||||
---
|
||||
title: JDK8 特性(一)
|
||||
date: 2022-05-23
|
||||
sidebar: 'auto'
|
||||
tags:
|
||||
- JDK8
|
||||
categories:
|
||||
- Java
|
||||
---
|
||||
## 1 Lambda表达式
|
||||
|
||||
### 1.1 Lambda标准格式
|
||||
|
||||
- Lambda省去面向对象的条条框框, Lambda的标准格式格式由3个部分组成
|
||||
- 格式说明:
|
||||
- `(...args)`: 参数列表
|
||||
- `->`: 分隔或连接参数与方法体的标识符
|
||||
- `{...;}`: 方法体, 主要的代码逻辑
|
||||
- eg:
|
||||
|
||||
```java
|
||||
class Demo{
|
||||
public static void main(String[] args) {
|
||||
new Thread(() -> {
|
||||
System.out.println("线程启动");
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Lambda初体验
|
||||
|
||||
#### 无参无返回值
|
||||
|
||||
```java
|
||||
public class Demo1 {
|
||||
public static void main(String[] args) {
|
||||
fishDo(() -> System.out.println("小鱼在欢乐的游着..."));
|
||||
}
|
||||
|
||||
private static void fishDo(Fish fish){
|
||||
fish.swim();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 定义一个接口, 并且该接口只有一个需要实现的方法
|
||||
**/
|
||||
interface Fish{
|
||||
void swim();
|
||||
}
|
||||
```
|
||||
|
||||
#### 有参有返回值
|
||||
|
||||
```java
|
||||
public class Demo1 {
|
||||
public static void main(String[] args) {
|
||||
catEat(foodName -> {
|
||||
System.out.println("小猫在吃" + foodName);
|
||||
return 3;
|
||||
});
|
||||
}
|
||||
private static void catEat(Cat cat){
|
||||
System.out.println("小猫吃了" + cat.eat("🐟") + "分钟");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 定义一个接口, 并且该接口只有一个需要实现的方法
|
||||
**/
|
||||
interface Cat{
|
||||
int eat(String foodName);
|
||||
}
|
||||
```
|
||||
|
||||
#### 小结
|
||||
|
||||
以后我们调用方法时,看到参数是接口就可以考虑使用Lambda表达式,Lambda表达式相当于是对接口中抽象方法的重写
|
||||
|
||||
### 1.3 Lambda实现原理
|
||||
|
||||
现有以下类
|
||||
|
||||
- 接口类
|
||||
|
||||
```java
|
||||
public interface Swimable {
|
||||
void swimming();
|
||||
}
|
||||
```
|
||||
|
||||
- main入口
|
||||
|
||||
```java
|
||||
public class SwimImplDemo {
|
||||
public static void main(String[] args) {
|
||||
goSwimming(new Swimable() {
|
||||
@Override
|
||||
public void swimming() {
|
||||
System.out.println("去匿名内部类游泳了");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void goSwimming(Swimable swim){
|
||||
swim.swimming();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 将以上的main方法进行执行后, 会编译生成以下字节码文件
|
||||
|
||||
- 
|
||||
|
||||
- 将内部类的字节码文件通过 [XJad] 进行反编译
|
||||
|
||||
- 匿名内部类在编译后会形成一个新的类.$
|
||||
|
||||
```java
|
||||
// Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov.
|
||||
// Jad home page: http://kpdus.tripod.com/jad.html
|
||||
// Decompiler options: packimports(3) fieldsfirst ansi space
|
||||
// Source File Name: SwimImplDemo.java
|
||||
|
||||
package com.zhuhjay.lambda;
|
||||
|
||||
import java.io.PrintStream;
|
||||
|
||||
// Referenced classes of package com.zhuhjay.lambda:
|
||||
// Swimable, SwimImplDemo
|
||||
|
||||
static class SwimImplDemo$1 implements Swimable{
|
||||
|
||||
public void swimming(){
|
||||
System.out.println("去匿名内部类游泳了");
|
||||
}
|
||||
|
||||
SwimImplDemo$1(){}
|
||||
}
|
||||
```
|
||||
|
||||
- main入口改为使用Lambda表达式
|
||||
|
||||
```java
|
||||
public class SwimImplDemo {
|
||||
public static void main(String[] args) {
|
||||
goSwimming(() -> {
|
||||
System.out.println("去Lambda游泳了");
|
||||
});
|
||||
}
|
||||
|
||||
private static void goSwimming(Swimable swim){
|
||||
swim.swimming();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 将以上的main方法进行执行后, 不会生成多余的字节码文件
|
||||
|
||||
- 使用 [XJad] 反编译工具会失败
|
||||
- 使用JDK工具来对Lambda表达式的字节码进行反汇编
|
||||
|
||||
```text
|
||||
javap -c -p 文件名.class
|
||||
-c:表示对代码进行反汇编 -p:显示所有类和成员
|
||||
```
|
||||
|
||||
- 对Lambda表达式的字节码文件进行反汇编
|
||||
|
||||
```S
|
||||
public class com.zhuhjay.lambda.SwimImplDemo {
|
||||
public com.zhuhjay.lambda.SwimImplDemo();
|
||||
Code:
|
||||
0: aload_0
|
||||
1: invokespecial #1 // Method java/lang/Object."<init>":()V
|
||||
4: return
|
||||
|
||||
public static void main(java.lang.String[]);
|
||||
Code:
|
||||
0: invokedynamic #2, 0 // InvokeDynamic #0:swimming:()Lcom/zhuhjay/lambda/Swimable;
|
||||
5: invokestatic #3 // Method goSwimming:(Lcom/zhuhjay/lambda/Swimable;)V
|
||||
8: return
|
||||
|
||||
private static void goSwimming(com.zhuhjay.lambda.Swimable);
|
||||
Code:
|
||||
0: aload_0
|
||||
1: invokeinterface #4, 1 // InterfaceMethod com/zhuhjay/lambda/Swimable.swimming:()V
|
||||
6: return
|
||||
|
||||
private static void lambda$main$0();
|
||||
Code:
|
||||
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
|
||||
3: ldc #6 // String 去Lambda游泳了
|
||||
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
|
||||
8: return
|
||||
}
|
||||
```
|
||||
|
||||
- 通过与源代码的比较后, 发现多了静态方法`lambda$main$0`
|
||||
|
||||
- 得出: Lambda表达式会在类中新生成一个私有的静态方法, 命名为`lambda$方法名$序列`
|
||||
|
||||
- 通过断点调试Lambda也可以从调用栈中发现该静态方法的生成(当然不会显式的在源码中出现)
|
||||
|
||||
- 验证在Lambda表达式运行中会生成一个内部类
|
||||
|
||||
- 命令格式: `java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名`
|
||||
|
||||
- 执行(需要退出包运行该命令): `java -Djdk.internal.lambda.dumpProxyClasses com.zhuhjay.lambda.SwimImplDemo`
|
||||
|
||||
- 
|
||||
- 查看反编译结果
|
||||
|
||||
```java
|
||||
final class SwimImplDemo$$Lambda$1
|
||||
implements Swimable {
|
||||
|
||||
public void swimming() {
|
||||
SwimImplDemo.lambda$main$0();
|
||||
}
|
||||
|
||||
private SwimImplDemo$$Lambda$1() {}
|
||||
}
|
||||
```
|
||||
|
||||
- 对以上结论来推断Lambda生成的字节码文件
|
||||
|
||||
```java
|
||||
public class SwimImplDemo {
|
||||
public static void main(String[] args) {
|
||||
// 也是相当于Lambda生成了一个匿名内部类, 来调用Lambda生成的静态方法
|
||||
goSwimming(new Swimable() {
|
||||
public void swimming(){
|
||||
SwimImplDemo.lambda$main$0();
|
||||
}
|
||||
});
|
||||
}
|
||||
/** Lambda生成的私有静态方法 **/
|
||||
private static void lambda$main$0(){
|
||||
System.out.println("去Lambda游泳了");
|
||||
}
|
||||
|
||||
private static void goSwimming(Swimable swim){
|
||||
swim.swimming();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 小结
|
||||
- 匿名内部类在编译的时候会一个class文件
|
||||
- Lambda在程序运行的时候形成一个类
|
||||
1. 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码
|
||||
2. 还会形成一个匿名内部类, 实现接口, 重写抽象方法
|
||||
3. 在接口的重写方法中会调用新生成的方法.
|
||||
|
||||
### 1.4 Lambda省略格式
|
||||
|
||||
在Lambda标准格式的基础上,使用省略写法的规则为:
|
||||
|
||||
1. 小括号内参数的类型可以省略
|
||||
2. 如果小括号内有且仅有一个参数,则小括号可以省略
|
||||
3. 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号
|
||||
|
||||
### 1.5 Lambda前提条件
|
||||
|
||||
Lambda的语法非常简洁,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意:
|
||||
|
||||
1. 方法的参数或局部变量类型必须为接口才能使用Lambda
|
||||
2. 接口中有且仅有一个抽象方法(@FunctionalInterface用来检测接口是否为函数式接口)
|
||||
|
||||
### 1.6 Lambda和匿名内部类对比
|
||||
|
||||
1. 所需的类型不一样
|
||||
- 匿名内部类,需要的类型可以是类,抽象类,接口
|
||||
- Lambda表达式,需要的类型必须是接口
|
||||
2. 抽象方法的数量不一样
|
||||
- 匿名内部类所需的接口中抽象方法的数量随意
|
||||
- Lambda表达式所需的接口只能有一个抽象方法
|
||||
3. 实现原理不同
|
||||
- 匿名内部类是在编译后会形成class
|
||||
- Lambda表达式是在程序运行的时候动态生成class
|
||||
|
||||
## 2 JDK8接口增强
|
||||
|
||||
### 2.1 增强介绍
|
||||
|
||||
- JDK8以前的接口:
|
||||
|
||||
```text
|
||||
interface 接口名 {
|
||||
静态常量;
|
||||
抽象方法;
|
||||
}
|
||||
```
|
||||
|
||||
- JDK8的接口:
|
||||
|
||||
```text
|
||||
interface 接口名 {
|
||||
静态常量;
|
||||
抽象方法;
|
||||
默认方法;
|
||||
静态方法;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 接口默认方法和静态方法的区别
|
||||
|
||||
1. 默认方法通过实例调用,静态方法通过接口名调用。
|
||||
2. 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。
|
||||
3. 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用。
|
||||
|
||||
## 3 常用内置函数式接口
|
||||
|
||||
### 3.1 内置函数式接口的由来
|
||||
|
||||
我们知道使用Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。
|
||||
|
||||
### 3.2 常用函数式接口的介绍
|
||||
|
||||
它们主要在`java.util.function`包中。下面是最常用的几个接口。
|
||||
|
||||
#### Supplier:生产者
|
||||
|
||||
`java.util.function.Supplier<T>`接口,它意味着"供给", 对应的Lambda表达式需要"对外提供"一个符合泛型类型的对象数据。
|
||||
|
||||
```java
|
||||
@FunctionalInterface
|
||||
public interface Supplier<T> {
|
||||
public abstract T get();
|
||||
}
|
||||
```
|
||||
|
||||
#### Consumer:消费者
|
||||
|
||||
`java.util.function.Consumer<T>`接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。
|
||||
|
||||
```java
|
||||
@FunctionalInterface
|
||||
public interface Consumer<T> {
|
||||
public abstract void accept(T t);
|
||||
/** 该方法使得两个Consumer先后调用 c1.andThen(c2).accept(t) **/
|
||||
default Consumer<T> andThen(Consumer<? super T> after) {
|
||||
Objects.requireNonNull(after);
|
||||
return (T t) -> { accept(t); after.accept(t); };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 默认方法: andThen
|
||||
- 如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen
|
||||
- 要想实现组合,需要两个或多个Lambda表达式即可,而 andThen 的语义正是"一步接一步"操作
|
||||
|
||||
#### Function:类型转换
|
||||
|
||||
`java.util.function.Function<T,R>`接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值。
|
||||
|
||||
```java
|
||||
@FunctionalInterface
|
||||
public interface Function<T, R> {
|
||||
public abstract R apply(T t);
|
||||
|
||||
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
|
||||
Objects.requireNonNull(before);
|
||||
return (V v) -> apply(before.apply(v));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 默认方法: andThen
|
||||
- Function 接口中有一个默认的 andThen 方法,用来进行组合操作
|
||||
- 该方法同样用于"先做什么,再做什么"的场景,和 Consumer 中的 andThen 差不多
|
||||
|
||||
#### Predicate:判断
|
||||
|
||||
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用`java.util.function.Predicate<T>`接口。
|
||||
|
||||
```java
|
||||
@FunctionalInterface
|
||||
public interface Predicate<T> {
|
||||
public abstract boolean test(T t);
|
||||
/** && **/
|
||||
default Predicate<T> and(Predicate<? super T> other) {
|
||||
Objects.requireNonNull(other);
|
||||
return (t) -> test(t) && other.test(t);
|
||||
}
|
||||
/** ! **/
|
||||
default Predicate<T> negate() {
|
||||
return (t) -> !test(t);
|
||||
}
|
||||
/** || **/
|
||||
default Predicate<T> or(Predicate<? super T> other) {
|
||||
Objects.requireNonNull(other);
|
||||
return (t) -> test(t) || other.test(t);
|
||||
}
|
||||
|
||||
static <T> Predicate<T> isEqual(Object targetRef) {
|
||||
return (null == targetRef)
|
||||
? Objects::isNull
|
||||
: object -> targetRef.equals(object);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 默认方法: and
|
||||
- 既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用"与"逻辑连接起来实现"并且"的效果时,可以使用default方法 and
|
||||
|
||||
- 默认方法: or
|
||||
- 与 and 的"与"类似,默认方法 or 实现逻辑关系中的"或"
|
||||
|
||||
- 默认方法: negate
|
||||
- "与"、"或"已经了解了,剩下的"非"(取反)也会简单。它是执行了test方法之后,对结果boolean值进行"!"取反而已。一定要在 test 方法调用之前调用 negate 方法
|
||||
|
||||
## 4 方法引用
|
||||
|
||||
方法引用的注意事项
|
||||
|
||||
1. 被引用的方法,参数要和接口中抽象方法的参数一样
|
||||
2. 当接口抽象方法有返回值时,被引用的方法也必须有返回值
|
||||
|
||||
### 4.1 方法引用简化Lambda
|
||||
|
||||
使用Lambda表达式求一个数组的和
|
||||
|
||||
```java
|
||||
public class Demo2 {
|
||||
public static void main(String[] args) {
|
||||
// 将数组进行求和
|
||||
printSum((arr) -> {
|
||||
int sum = 0;
|
||||
for (int i : arr) {
|
||||
sum += i;
|
||||
}
|
||||
System.out.println("sum = " + sum);
|
||||
});
|
||||
// 使用已有的方法进行方法引用(让已实现的方法复用)
|
||||
// 类名::静态方法
|
||||
printSum(Demo2::getSum);
|
||||
}
|
||||
/** 已有的求和方法 **/
|
||||
private static void getSum(int[] arr){
|
||||
int sum = 0;
|
||||
for (int i : arr) {
|
||||
sum += i;
|
||||
}
|
||||
System.out.println("sum = " + sum);
|
||||
}
|
||||
/** 使用消费者函数式接口 **/
|
||||
private static void printSum(Consumer<int[]> consumer){
|
||||
int[] arr = new int[]{11, 22, 33, 44};
|
||||
consumer.accept(arr);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 方法引用格式
|
||||
|
||||
- 符号表示: `::`
|
||||
- 符号说明: 双冒号为方法引用运算符,而它所在的表达式被称为方法引用。
|
||||
- 应用场景: 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用
|
||||
|
||||
### 4.3 常见引用方式
|
||||
|
||||
方法引用在JDK 8中使用方式相当灵活,有以下几种形式:
|
||||
|
||||
1. `instanceName::methodName` 对象::方法名
|
||||
2. `ClassName::staticMethodName` 类名::静态方法
|
||||
3. `ClassName::methodName` 类名::普通方法
|
||||
4. `ClassName::new` 类名::new 调用的构造器
|
||||
5. `TypeName[]::new` String[]::new 调用数组的构造器
|
||||
|
||||
#### 对象::成员方法
|
||||
|
||||
这是最常见的一种用法。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:
|
||||
|
||||
```java
|
||||
public class MethodRefDemo {
|
||||
public static void main(String[] args) {
|
||||
Date now = new Date();
|
||||
// 使用Lambda表达式获取当前时间
|
||||
Supplier<Long> su1 = () -> now.getTime();
|
||||
System.out.println(su1.get());
|
||||
// 使用方法引用获取当前时间
|
||||
Supplier<Long> su2 = now::getTime;
|
||||
System.out.println(su2.get());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 类名::静态方法
|
||||
|
||||
由于在`java.lang.System`类中已经存在了静态方法 currentTimeMillis,所以当我们需要通过Lambda来调用该方法时,可以使用方法引用, 写法是:
|
||||
|
||||
```java
|
||||
public class MethodRefDemo {
|
||||
public static void main(String[] args) {
|
||||
Supplier<Long> su3 = () -> System.currentTimeMillis();
|
||||
// 等同于, 调用该类的静态方法
|
||||
Supplier<Long> su4 = System::currentTimeMillis;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 类名::引用实例方法
|
||||
|
||||
Java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。
|
||||
|
||||
```java
|
||||
public class MethodRefDemo {
|
||||
public static void main(String[] args) {
|
||||
Function<String, Integer> f1 = (str) -> str.length();
|
||||
// 等同于, 将参数作为调用者去调用方法, 然后接收对应数据类型的返回值
|
||||
Function<String, Integer> f2 = String::length;
|
||||
|
||||
BiFunction<String, Integer, String> f3 = String::substring;
|
||||
// 等同于, 将第一个参数作为调用者, 第二个参数作为参数, 然后接收对应数据类型的返回值
|
||||
BiFunction<String, Integer, String> f4 = (str, index) -> str.substring(index);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 类名::new
|
||||
|
||||
由于构造器的名称与类名完全一样。所以构造器引用使用`类名称::new`的格式表示
|
||||
|
||||
```java
|
||||
public class MethodRefDemo {
|
||||
public static void main(String[] args) {
|
||||
// 使用无参构造器实例一个String类
|
||||
Supplier<String> s1 = String::new;
|
||||
// 等同于
|
||||
Supplier<String> s2 = () -> new String();
|
||||
s1.get();
|
||||
|
||||
// 把具体地调用体现在了接口上
|
||||
|
||||
// 使用一个参数的构造器实例一个String类
|
||||
Function<String, String> s3 = String::new;
|
||||
// 等同于
|
||||
Function<String, String> s4 = (str) -> new String(str);
|
||||
s3.apply("张三");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 数组::new
|
||||
|
||||
数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同
|
||||
|
||||
```java
|
||||
public class MethodRefDemo {
|
||||
public static void main(String[] args) {
|
||||
Function<Integer, int[]> f = length -> new int[length];
|
||||
// 等同于
|
||||
Function<Integer, int[]> ff = int[]::new;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 小结
|
||||
|
||||
方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为Lambda表达式的缩写形式, 不过要注意的是方法引用只能"引用"已经存在的方法!
|
||||
810
blogs/java/java8/java8-2.md
Normal file
810
blogs/java/java8/java8-2.md
Normal file
@@ -0,0 +1,810 @@
|
||||
---
|
||||
title: JDK8 特性(二)
|
||||
date: 2022-05-24
|
||||
sidebar: 'auto'
|
||||
tags:
|
||||
- JDK8
|
||||
categories:
|
||||
- Java
|
||||
---
|
||||
## 1 Stream流
|
||||
|
||||
### 1.1 集合处理数据的弊端
|
||||
|
||||
- 案例引入:
|
||||
1. 首先筛选所有姓张的人;
|
||||
2. 然后筛选名字有三个字的人;
|
||||
3. 最后进行对结果进行打印输出。
|
||||
|
||||
- 传统实现
|
||||
|
||||
```java
|
||||
public class StreamDemo {
|
||||
public static void main(String[] args) {
|
||||
List<String> list = new ArrayList<>();
|
||||
Collections.addAll(list, "张三丰", "周芷若", "张无忌", "赵敏", "张强");
|
||||
|
||||
// 1. 首先筛选所有姓张的人;
|
||||
List<String> zhangList = new ArrayList<>();
|
||||
for (String name : list) {
|
||||
if(name.contains("张")){
|
||||
zhangList.add(name);
|
||||
}
|
||||
}
|
||||
// 2. 然后筛选名字有三个字的人;
|
||||
List<String> threeList = new ArrayList<>();
|
||||
for (String name : zhangList) {
|
||||
if(name.length() == 3){
|
||||
threeList.add(name);
|
||||
}
|
||||
}
|
||||
// 3. 最后进行对结果进行打印输出。
|
||||
for (String name : threeList) {
|
||||
System.out.println(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Stream流式实现
|
||||
|
||||
```java
|
||||
public class StreamDemo {
|
||||
public static void main(String[] args) {
|
||||
List<String> list = new ArrayList<>();
|
||||
Collections.addAll(list, "张三丰", "周芷若", "张无忌", "赵敏", "张强");
|
||||
|
||||
// 1. 首先筛选所有姓张的人; 2. 然后筛选名字有三个字的人; 3. 最后进行对结果进行打印输出
|
||||
list.stream()
|
||||
.filter(str -> str.contains("张"))
|
||||
.filter(str -> str.length() == 3)
|
||||
.forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Stream流式思想概述
|
||||
|
||||
**注意:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象**!
|
||||
|
||||
Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约。
|
||||
|
||||
### 1.3 获取Stream流的两种方式
|
||||
|
||||
- 方式一
|
||||
- Collection集合接口中有默认方法`default Stream<E> stream(){...}`
|
||||
- 也就是说实现该接口的集合类都可以直接调用stream()方法获取流对象
|
||||
- 方式二
|
||||
- Stream中的静态方法`static<T> Stream<T> of(T t){...}`
|
||||
- 可以调用该方法获取流对象
|
||||
|
||||
### 1.4 了解Stream流的常用方法和注意事项
|
||||
|
||||
- Stream常用方法
|
||||
|
||||
| 方法名 | 方法作用 | 返回值类型 | 方法种类 |
|
||||
| ------- | ---------- | ---------- | -------- |
|
||||
| count | 统计个数 | long | 终结 |
|
||||
| forEach | 逐一处理 | void | 终结 |
|
||||
| filter | 过滤 | Stream | 函数拼接 |
|
||||
| limit | 取用前几个 | Stream | 函数拼接 |
|
||||
| skip | 跳过前几个 | Stream | 函数拼接 |
|
||||
| map | 映射 | Stream | 函数拼接 |
|
||||
| concat | 组合 | Stream | 函数拼接 |
|
||||
|
||||
- 终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。
|
||||
- 非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结方法。)
|
||||
|
||||
- Stream注意事项
|
||||
1. Stream只能操作一次
|
||||
2. Stream方法返回的是新的流
|
||||
3. Stream不调用终结方法,中间的操作不会执行
|
||||
|
||||
#### forEach
|
||||
|
||||
- forEach用来遍历流中的数据 -> 终结方法
|
||||
|
||||
```text
|
||||
void forEach(Consumer<? super T> action);
|
||||
```
|
||||
|
||||
- 该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
private List<String> list;
|
||||
|
||||
@Before
|
||||
public void before(){
|
||||
list = new ArrayList<>();
|
||||
Collections.addAll(list, "张三丰", "周芷若", "张无忌", "赵敏", "张强");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forEach(){
|
||||
list.stream().forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### count
|
||||
|
||||
- count方法来统计其中的元素个数 -> 终结方法
|
||||
|
||||
```text
|
||||
long count();
|
||||
```
|
||||
|
||||
- 该方法返回一个long值代表元素个数。基本使用
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
private List<String> list;
|
||||
|
||||
@Before
|
||||
public void before(){
|
||||
list = new ArrayList<>();
|
||||
Collections.addAll(list, "张三丰", "周芷若", "张无忌", "赵敏", "张强");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void count(){
|
||||
System.out.println(list.stream().count());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### filter
|
||||
|
||||
- filter用于过滤数据,返回符合过滤条件的数据 -> 非终结方法
|
||||
|
||||
```text
|
||||
Stream<T> filter(Predicate<? super T> predicate);
|
||||
```
|
||||
|
||||
- 
|
||||
|
||||
- 该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
private List<String> list;
|
||||
|
||||
@Before
|
||||
public void before(){
|
||||
list = new ArrayList<>();
|
||||
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
|
||||
}
|
||||
@Test
|
||||
public void filter(){
|
||||
// 计算名字长度为3的人数
|
||||
System.out.println(list.stream().filter(name -> name.length() == 3).count());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### limit
|
||||
|
||||
- limit 方法可以对流进行截取,只取用前n个 -> 非终结方法
|
||||
|
||||
```text
|
||||
Stream<T> limit(long maxSize);
|
||||
```
|
||||
|
||||
- 参数是一个long型,如果集合当前长度大于参数则进行截取。否则不进行操作
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
private List<String> list;
|
||||
|
||||
@Before
|
||||
public void before(){
|
||||
list = new ArrayList<>();
|
||||
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void limit(){
|
||||
list.stream().limit(3).forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### skip
|
||||
|
||||
- 如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流 -> 非终结方法
|
||||
|
||||
```text
|
||||
Stream<T> skip(long n);
|
||||
```
|
||||
|
||||
- 如果流的当前长度大于n,则跳过前n个; 否则将会得到一个长度为0的空流。
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
private List<String> list;
|
||||
|
||||
@Before
|
||||
public void before(){
|
||||
list = new ArrayList<>();
|
||||
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skip(){
|
||||
list.stream().skip(3).forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### map
|
||||
|
||||
- 如果需要将流中的元素映射到另一个流中,可以使用 map 方法 -> 非终结方法
|
||||
|
||||
```text
|
||||
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
|
||||
```
|
||||
|
||||
- 该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
private List<String> list;
|
||||
|
||||
@Before
|
||||
public void before(){
|
||||
list = new ArrayList<>();
|
||||
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void map(){
|
||||
list.stream()
|
||||
// 类型增强
|
||||
.map(name -> "name: " + name)
|
||||
.forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### sorted
|
||||
|
||||
- 如果需要将数据排序,可以使用 sorted 方法。 -> 非终结方法
|
||||
|
||||
```text
|
||||
Stream<T> sorted(); // 根据元素的自然顺序排序
|
||||
Stream<T> sorted(Comparator<? super T> comparator); // 自定义比较器排序
|
||||
```
|
||||
|
||||
- 该方法可以有两种不同地排序实现
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
private List<String> list;
|
||||
|
||||
@Before
|
||||
public void before(){
|
||||
list = new ArrayList<>();
|
||||
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sorted(){
|
||||
list.stream()
|
||||
// 默认自然序
|
||||
.sorted()
|
||||
// 执行比较器进行排序
|
||||
.sorted(((o1, o2) -> o1.length() - o2.length()))
|
||||
.forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### distinct
|
||||
|
||||
- 如果需要去除重复数据,可以使用 distinct 方法 -> 非终结方法
|
||||
- 自定义类型是根据对象的 hashCode 和 equals 来去除重复元素的
|
||||
|
||||
```text
|
||||
Stream<T> distinct();
|
||||
```
|
||||
|
||||
- 基本使用
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
private List<String> list;
|
||||
|
||||
@Before
|
||||
public void before(){
|
||||
list = new ArrayList<>();
|
||||
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void distinct(){
|
||||
list.add("迪丽热巴");
|
||||
list.add("张无忌");
|
||||
list.stream()
|
||||
.distinct()
|
||||
.forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### match
|
||||
|
||||
- 如果需要判断数据是否匹配指定的条件,可以使用 Match 相关方法 -> 终结方法
|
||||
|
||||
```text
|
||||
boolean anyMatch(Predicate<? super T> predicate);
|
||||
boolean allMatch(Predicate<? super T> predicate);
|
||||
boolean noneMatch(Predicate<? super T> predicate);
|
||||
```
|
||||
|
||||
- 基本使用
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
private List<String> list;
|
||||
|
||||
@Before
|
||||
public void before(){
|
||||
list = new ArrayList<>();
|
||||
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void match(){
|
||||
// allMatch 所有元素都需要满足
|
||||
System.out.println(list.stream()
|
||||
.allMatch(name -> name.contains("张")));
|
||||
|
||||
// anyMatch 如果有元素满足即可
|
||||
System.out.println(list.stream()
|
||||
.anyMatch(name -> name.contains("张")));
|
||||
|
||||
// noneMatch 所有元素都不满足
|
||||
System.out.println(list.stream()
|
||||
.noneMatch(name -> name.contains("男")));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### find
|
||||
|
||||
- 如果需要找到某些数据,可以使用 find 相关方法 -> 终结方法
|
||||
|
||||
```text
|
||||
Optional<T> findFirst();
|
||||
Optional<T> findAny();
|
||||
```
|
||||
|
||||
- 基本使用
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
private List<String> list;
|
||||
|
||||
@Before
|
||||
public void before(){
|
||||
list = new ArrayList<>();
|
||||
Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void find(){
|
||||
// 都是获取第一个元素
|
||||
Optional<String> optional = list.stream().findAny();
|
||||
Optional<String> optional1 = list.stream().findFirst();
|
||||
System.out.println(optional.get());
|
||||
System.out.println(optional1.get());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### max && min
|
||||
|
||||
- 如果需要获取最大和最小值,可以使用 max 和 min 方法 -> 终结方法
|
||||
|
||||
```text
|
||||
Optional<T> max(Comparator<? super T> comparator);
|
||||
Optional<T> min(Comparator<? super T> comparator);
|
||||
```
|
||||
|
||||
- 基本使用
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
@Test
|
||||
public void min_max(){
|
||||
// 传入一个比较器, 排序完后获取最后一个
|
||||
Optional<Integer> max = Stream.of(4, 2, 7, 1).max((o1, o2) -> o1 - o2);
|
||||
System.out.println(max.get());
|
||||
|
||||
// 传入一个比较器, 排序完后获取第一个
|
||||
Optional<Integer> min = Stream.of(4, 2, 7, 1).min((o1, o2) -> o1 - o2);
|
||||
System.out.println(min.get());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### reduce
|
||||
|
||||
- 如果需要将所有数据归纳得到一个数据,可以使用 reduce 方法 -> 终结方法
|
||||
|
||||
```text
|
||||
T reduce(T identity, BinaryOperator<T> accumulator);
|
||||
```
|
||||
|
||||
- 基本使用
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
@Test
|
||||
public void reduce(){
|
||||
Integer reduce = Stream.of(4, 5, 3, 9)
|
||||
// T identity: 默认值
|
||||
// BinaryOperator<T> accumulator: 对数据的处理方式
|
||||
// 获取最大值, 初始值为0
|
||||
.reduce(0, Integer::max);
|
||||
System.out.println(reduce);
|
||||
|
||||
reduce = Stream.of(4, 5, 3, 9)
|
||||
// T identity: 默认值
|
||||
// BinaryOperator<T> accumulator: 对数据的处理方式
|
||||
// 获取加和结果, 初始值为0
|
||||
.reduce(0, Integer::sum);
|
||||
System.out.println(reduce);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
#### map && reduce结合
|
||||
|
||||
- 使用案例
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
@Test
|
||||
public void reduceAndMap(){
|
||||
// 获取最大年龄
|
||||
Optional<Integer> maxAge = Stream.of(
|
||||
new Person("张三", 18),
|
||||
new Person("李四", 20),
|
||||
new Person("王五", 17),
|
||||
new Person("小红", 19),
|
||||
new Person("小明", 18))
|
||||
.map(Person::getAge)
|
||||
.reduce(Integer::max);
|
||||
System.out.println(maxAge.get());
|
||||
|
||||
// 获取年龄总和
|
||||
Optional<Integer> totalAge = Stream.of(
|
||||
new Person("张三", 18),
|
||||
new Person("李四", 22),
|
||||
new Person("王五", 17),
|
||||
new Person("小红", 19),
|
||||
new Person("小明", 21))
|
||||
.map(Person::getAge)
|
||||
.reduce(Integer::sum);
|
||||
System.out.println(totalAge.get());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### mapToInt
|
||||
|
||||
- 如果需要将Stream中的Integer类型数据转成int类型,可以使用 mapToInt 方法 -> 终结方法
|
||||
|
||||
```text
|
||||
IntStream mapToInt(ToIntFunction<? super T> mapper);
|
||||
```
|
||||
|
||||
- 
|
||||
|
||||
- 该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
@Test
|
||||
public void mapToInt(){
|
||||
// Integer占用的内存比int多, 在Stream流中会自动拆箱装箱
|
||||
Stream<Integer> stream = Stream.of(1, 2, 3, 4);
|
||||
// 将Integer类型转换为int类型, 可以节省空间
|
||||
IntStream intStream = stream
|
||||
.mapToInt(Integer::intValue);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### concat
|
||||
|
||||
- 如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat -> 终结方法
|
||||
- 这是一个静态方法,与 `java.lang.String` 当中的 concat 方法是不同的
|
||||
|
||||
```text
|
||||
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b){...}
|
||||
```
|
||||
|
||||
- 基本使用
|
||||
|
||||
```java
|
||||
public class StreamTest {
|
||||
@Test
|
||||
public void concat(){
|
||||
Stream<String> streamA = Stream.of("张三");
|
||||
Stream<String> streamB = Stream.of("李四");
|
||||
// 将以上流合并, 合并的流就不能够继续操作了
|
||||
Stream.concat(streamA, streamB).forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 Stream综合案例
|
||||
|
||||
- 现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,要求使用Stream实现若干操作步骤:
|
||||
1. 第一个队伍只要名字为3个字的成员姓名;
|
||||
2. 第一个队伍筛选之后只要前3个人;
|
||||
3. 第二个队伍只要姓张的成员姓名;
|
||||
4. 第二个队伍筛选之后不要前2个人;
|
||||
5. 将两个队伍合并为一个队伍;
|
||||
6. 根据姓名创建 Person 对象;
|
||||
7. 打印整个队伍的Person对象信息。
|
||||
|
||||
```java
|
||||
public class StreamCaseDemo {
|
||||
public static void main(String[] args) {
|
||||
List<String> one = new ArrayList<>();
|
||||
List<String> two = new ArrayList<>();
|
||||
Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七公");
|
||||
Collections.addAll(two, "古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱", "张三");
|
||||
|
||||
Stream<String> streamOne = one.stream()
|
||||
// 1. 第一个队伍只要名字为3个字的成员姓名
|
||||
.filter(name -> name.length() == 3)
|
||||
// 2. 第一个队伍筛选之后只要前3个人
|
||||
.limit(3);
|
||||
|
||||
Stream<String> streamTwo = two.stream()
|
||||
// 3. 第二个队伍只要姓张的成员姓名
|
||||
.filter(name -> name.startsWith("张"))
|
||||
// 4. 第二个队伍筛选之后不要前2个人
|
||||
.skip(2);
|
||||
// 5. 将两个队伍合并为一个队伍
|
||||
Stream<String> stream = Stream.concat(streamOne, streamTwo);
|
||||
// 6. 根据姓名创建 Person 对象
|
||||
stream.map(Person::new)
|
||||
// 7. 打印整个队伍的Person对象信息
|
||||
.forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.6 收集Stream流中的结果
|
||||
|
||||
- 对流操作完成之后,如果需要将流的结果保存到数组或集合中,可以收集流中的数据
|
||||
|
||||
#### 收集结果到集合中
|
||||
|
||||
- Stream流提供 collect 方法,其参数需要一个`java.util.stream.Collector<T,A, R>`接口对象来指定收集到哪种集合中。`java.util.stream.Collectors`类提供一些方法,可以作为Collector`接口的实例:
|
||||
- `public static <T> Collector<T, ?, List<T>> toList()`: 转换为 List 集合
|
||||
- `public static <T> Collector<T, ?, Set<T>> toSet()`: 转换为 Set 集合
|
||||
- `public static <T, C extends Collection<T>>
|
||||
Collector<T, ?, C> toCollection(Supplier<C> collectionFactory)`: 收集到指定的集合中
|
||||
|
||||
#### 收集结果到数组中
|
||||
|
||||
- Stream提供 toArray 方法来将结果放到一个数组中
|
||||
|
||||
```text
|
||||
Object[] toArray(); // 返回Object数组
|
||||
<A> A[] toArray(IntFunction<A[]> generator); // 返回指定数组
|
||||
```
|
||||
|
||||
#### 对流中数据进行聚合计算
|
||||
|
||||
- 当我们使用Stream流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作。比如获取最大值,获取最小值,求总和,平均值,统计数量。
|
||||
|
||||
```java
|
||||
public class StreamCollectTest {
|
||||
@Test
|
||||
public void aggregation(){
|
||||
List<Person> personList = Stream.of(
|
||||
new Person("张三", 18),
|
||||
new Person("李四", 22),
|
||||
new Person("王五", 17),
|
||||
new Person("小红", 19),
|
||||
new Person("小明", 21))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 获取最大值
|
||||
Optional<Person> optional = personList.stream()
|
||||
.collect(Collectors.maxBy((o1, o2) -> o1.getAge() - o2.getAge()));
|
||||
System.out.println("最大值: " + optional.get());
|
||||
|
||||
// 获取最小值
|
||||
Optional<Person> optionalMin = personList.stream()
|
||||
.collect(Collectors.minBy((o1, o2) -> o1.getAge() - o2.getAge()));
|
||||
System.out.println("最小值: " + optionalMin.get());
|
||||
|
||||
// 求总和
|
||||
Integer totalAge = personList.stream()
|
||||
.collect(Collectors.summingInt(Person::getAge));
|
||||
System.out.println("总和: " + totalAge);
|
||||
|
||||
// 平均值
|
||||
Double avg = personList.stream()
|
||||
.collect(Collectors.averagingInt(Person::getAge));
|
||||
System.out.println("平均值: " + avg);
|
||||
|
||||
// 统计数量
|
||||
Long count = personList.stream()
|
||||
.collect(Collectors.counting());
|
||||
System.out.println("总共: " + count);
|
||||
|
||||
// 以上方法聚合
|
||||
IntSummaryStatistics summaryStatistics = personList.stream()
|
||||
.collect(Collectors.summarizingInt(Person::getAge));
|
||||
System.out.println("上面所有方法的聚合: " + summaryStatistics);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 对流中数据进行分组
|
||||
|
||||
- 当我们使用Stream流处理数据后,可以根据某个属性将数据分组
|
||||
|
||||
```java
|
||||
public class StreamCollectTest {
|
||||
@Test
|
||||
public void group(){
|
||||
List<Person> personList = Stream.of(
|
||||
new Person("张三", 18),
|
||||
new Person("李四", 22),
|
||||
new Person("王五", 18),
|
||||
new Person("小红", 18),
|
||||
new Person("小明", 21))
|
||||
.collect(Collectors.toList());
|
||||
// 通过年龄来进行分组
|
||||
personList.stream()
|
||||
.collect(Collectors.groupingBy(Person::getAge))
|
||||
// 分组结果为键值对
|
||||
.forEach((key, value) -> System.out.println(key + "::" + value));
|
||||
|
||||
// 将年龄大于19的分为一组, 小于19分为一组
|
||||
personList.stream()
|
||||
.collect(Collectors.groupingBy(s -> {
|
||||
if(s.getAge() > 19){
|
||||
return ">=19";
|
||||
}else{
|
||||
return "<19";
|
||||
}
|
||||
}))
|
||||
// 分组结果为键值对, 键为groupingBy返回的数据
|
||||
.forEach((k, v) -> System.out.println(k + "::" + v));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 对流中数据进行多级分组
|
||||
|
||||
- 还可以对数据进行多级分组:
|
||||
|
||||
```java
|
||||
public class StreamCollectTest {
|
||||
@Test
|
||||
public void multiGroup(){
|
||||
List<Person> personList = Stream.of(
|
||||
new Person("张三丰", 18),
|
||||
new Person("迪丽热巴", 22),
|
||||
new Person("古力娜扎", 18),
|
||||
new Person("迪迦奥特曼", 18),
|
||||
new Person("宇宙无敌法外狂徒张三", 21))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 先根据年龄分组, 每组中再根据名字的长度分组
|
||||
personList.stream()
|
||||
.collect(
|
||||
// 根据年龄分组
|
||||
Collectors.groupingBy(p -> {
|
||||
if(p.getAge() > 19){
|
||||
return "大于19";
|
||||
}else{
|
||||
return "小于等于19";
|
||||
}
|
||||
},
|
||||
// 根据名字长度分组
|
||||
Collectors.groupingBy(p -> {
|
||||
if(p.getName().length() > 4){
|
||||
return "较长的名字";
|
||||
}else{
|
||||
return "较短的名字";
|
||||
}
|
||||
}))
|
||||
)
|
||||
// 结果的类型为: Map<String, Map<String, Person>>
|
||||
.forEach((oneK, oneV) -> {
|
||||
System.out.println("年龄" + oneK);
|
||||
oneV.forEach((twoK, twoV) -> {
|
||||
System.out.println("\t" + twoK + "::" + twoV);
|
||||
});
|
||||
});
|
||||
/*
|
||||
result -> {
|
||||
年龄小于等于19
|
||||
较长的名字::[Person{name='迪迦奥特曼', age=18}]
|
||||
较短的名字::[Person{name='张三丰', age=18}, Person{name='古力娜扎', age=18}]
|
||||
年龄大于19
|
||||
较长的名字::[Person{name='宇宙无敌法外狂徒张三', age=21}]
|
||||
较短的名字::[Person{name='迪丽热巴', age=22}]
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 对流中数据进行分区
|
||||
|
||||
- `Collectors.partitioningBy`会根据值是否为true,把集合分割为两个列表,一个true列表,一个false列表
|
||||
- 
|
||||
|
||||
```java
|
||||
public class StreamCollectTest {
|
||||
@Test
|
||||
public void partition(){
|
||||
List<Person> personList = Stream.of(
|
||||
new Person("张三", 18),
|
||||
new Person("李四", 22),
|
||||
new Person("王五", 18),
|
||||
new Person("小红", 18),
|
||||
new Person("小明", 21))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
personList.stream()
|
||||
// 将结果分为, true和false两个分区
|
||||
.collect(Collectors.partitioningBy(p -> p.getAge() > 19))
|
||||
.forEach((k, v) -> System.out.println(k + "::" + v));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 对流中数据进行拼接
|
||||
|
||||
- `Collectors.joining`会根据指定的连接符,将所有元素连接成一个字符串
|
||||
|
||||
```java
|
||||
public class StreamCollectTest {
|
||||
@Test
|
||||
public void join(){
|
||||
List<Person> personList = Stream.of(
|
||||
new Person("张三", 18),
|
||||
new Person("李四", 22),
|
||||
new Person("王五", 18),
|
||||
new Person("小红", 18),
|
||||
new Person("小明", 21))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 根据一个字符拼接
|
||||
String names = personList.stream()
|
||||
.map(Person::getName)
|
||||
.collect(Collectors.joining("-"));
|
||||
System.out.println(names);
|
||||
|
||||
// 根据三个字符拼接
|
||||
names = personList.stream()
|
||||
.map(Person::getName)
|
||||
// 参数说明: 分隔符, 前缀, 后缀
|
||||
.collect(Collectors.joining(",", "{", "}"));
|
||||
System.out.println(names);
|
||||
}
|
||||
}
|
||||
```
|
||||
258
blogs/java/java8/java8-3.md
Normal file
258
blogs/java/java8/java8-3.md
Normal file
@@ -0,0 +1,258 @@
|
||||
---
|
||||
title: JDK8 特性(三)
|
||||
date: 2022-05-25
|
||||
sidebar: 'auto'
|
||||
tags:
|
||||
- JDK8
|
||||
categories:
|
||||
- Java
|
||||
---
|
||||
## 1 Stream流-续
|
||||
|
||||
### 1.7 并行的Stream流
|
||||
|
||||
- 串行的Stream流
|
||||
- 目前我们使用的Stream流是串行的,就是在一个线程上执行。
|
||||
|
||||
- 并行的Stream流
|
||||
- parallelStream其实就是一个并行执行的流。它通过默认的ForkJoinPool,可能提高多线程任务的速度。
|
||||
|
||||
#### 获取并行Stream流的两种方式
|
||||
|
||||
1. 直接获取并行的流
|
||||
|
||||
```java
|
||||
public class StreamParallelTest {
|
||||
@Test
|
||||
public void parallel(){
|
||||
ArrayList<Integer> list = new ArrayList<>();
|
||||
// 直接获取并行的流
|
||||
Stream<Integer> stream = list.parallelStream();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. 将串行流转成并行流
|
||||
|
||||
```java
|
||||
public class StreamParallelTest {
|
||||
@Test
|
||||
public void serialToParallel(){
|
||||
Stream.of(9, 1, 34, 5, 3, 21, 56, 9)
|
||||
// 转成并行流
|
||||
.parallel()
|
||||
.filter(i -> {
|
||||
System.out.println(Thread.currentThread() + "::" + i);
|
||||
return i > 50;
|
||||
})
|
||||
.forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 串行和并行效率的比较
|
||||
|
||||
- 使用for循环,串行Stream流,并行Stream流来对10亿个数字求和。看消耗的时间。
|
||||
|
||||
```java
|
||||
public class StreamParallelTest {
|
||||
private static final int TIMES = 1000000000;
|
||||
long start;
|
||||
|
||||
@Before
|
||||
public void init(){
|
||||
start = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void useFor(){
|
||||
// 消耗时间: 339ms
|
||||
int sum = 0;
|
||||
for (int i = 0; i < TIMES; i++) {
|
||||
sum += i;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void useSerialStream(){
|
||||
// 获取足够长度的串行流进行加和
|
||||
// 消耗时间: 631ms
|
||||
LongStream.rangeClosed(0, TIMES)
|
||||
.reduce(0, Long::sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void useParallelStream(){
|
||||
// 获取足够长度的并行流进行加和
|
||||
// 消耗时间: 317ms
|
||||
LongStream.rangeClosed(0, TIMES)
|
||||
.parallel()
|
||||
.reduce(0, Long::sum);
|
||||
}
|
||||
|
||||
@After
|
||||
public void destroy(){
|
||||
System.out.println("消耗时间: " +
|
||||
(System.currentTimeMillis() - start) +
|
||||
"ms");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 我们可以看到parallelStream的效率是最高的。
|
||||
- Stream并行处理的过程会分而治之,也就是将一个大任务切分成多个小任务,这表示每个任务都是一个操作。
|
||||
|
||||
#### ParallelStream线程安全问题
|
||||
|
||||
```java
|
||||
public class StreamParallelTest {
|
||||
@Test
|
||||
public void parallelStreamNotice(){
|
||||
List<Integer> list = new ArrayList<>(1000);
|
||||
|
||||
// 并行线程不安全
|
||||
// list.size() = 947
|
||||
IntStream.range(0, 1000)
|
||||
.parallel()
|
||||
.forEach(i -> list.add(i));
|
||||
System.out.println("list.size() = " + list.size());
|
||||
|
||||
// 解决线程安全问题方案一: 使用同步代码块
|
||||
Object lock = new Object();
|
||||
IntStream.range(0, 1000)
|
||||
.parallel()
|
||||
.forEach(i -> {
|
||||
synchronized (lock){
|
||||
list.add(i);
|
||||
}
|
||||
});
|
||||
System.out.println("list.size() = " + list.size());
|
||||
|
||||
// 解决线程安全问题方案二: 使用线程安全的集合
|
||||
// 使用 Vector<Integer> 集合
|
||||
Vector<Integer> v = new Vector<>();
|
||||
IntStream.range(0, 1000)
|
||||
.parallel()
|
||||
.forEach(i -> v.add(i));
|
||||
System.out.println("v.size() = " + v.size());
|
||||
|
||||
// 解决线程安全问题方案二: 使用线程安全的集合
|
||||
// 使用集合工具类提供的线程安全的集合
|
||||
List<Integer> synchronizedList = Collections.synchronizedList(list);
|
||||
IntStream.range(0, 1000)
|
||||
.parallel()
|
||||
.forEach(i -> synchronizedList.add(i));
|
||||
System.out.println("synchronizedList.size() = " + synchronizedList.size());
|
||||
|
||||
// 解决线程安全问题方案三: 调用Stream流的collect/toArray
|
||||
// 使用Stream流的collect
|
||||
List<Integer> collect = IntStream.range(0, 1000)
|
||||
.parallel()
|
||||
.boxed()
|
||||
.collect(Collectors.toList());
|
||||
System.out.println("collect.size() = " + collect.size());
|
||||
|
||||
// 解决线程安全问题方案三: 调用Stream流的collect/toArray
|
||||
// 使用Stream流的toArray
|
||||
Integer[] integers = IntStream.range(0, 1000)
|
||||
.parallel()
|
||||
.boxed()
|
||||
.toArray(Integer[]::new);
|
||||
System.out.println("integers.length = " + integers.length);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.8 Fork/Join框架介绍
|
||||
|
||||
- parallelStream使用的是Fork/Join框架。Fork/Join框架自JDK 7引入。Fork/Join框架可以将一个大任务拆分为很多小 任务来异步执行。 Fork/Join框架主要包含三个模块:
|
||||
1. 线程池:ForkJoinPool
|
||||
2. 任务对象:ForkJoinTask
|
||||
3. 执行任务的线程:ForkJoinWorkerThread
|
||||
|
||||

|
||||
|
||||
#### Fork/Join原理-分治法
|
||||
|
||||
- ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成 两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。
|
||||
|
||||

|
||||
|
||||
#### Fork/Join原理-工作窃取算法
|
||||
|
||||
- Fork/Join最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的cpu,那么如何利用好这个空闲的cpu就成了提高性能的关键,而这里我们要提到的工作窃取(work-stealing)算法就是整个Fork/Join框架的核心理念 Fork/Join工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。
|
||||
|
||||

|
||||
|
||||
- 那么为什么需要使用工作窃取算法呢?假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的 任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任 务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永 远从双端队列的尾部拿任务执行。
|
||||
|
||||
- 工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
|
||||
|
||||
- 上文中已经提到了在Java 8引入了自动并行化的概念。它能够让一部分Java代码自动地以并行的方式执行,也就是我们使用了ForkJoinPool的ParallelStream。
|
||||
|
||||
- 对于ForkJoinPool通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量。可以通过设置系统属性:`java.util.concurrent.ForkJoinPool.common.parallelism=N`(N为线程数量),来调整ForkJoinPool的线程数量,可以尝试调整成不同的参数来观察每次的输出结果。
|
||||
|
||||
#### Fork/Join案例
|
||||
|
||||
- 需求:使用Fork/Join计算1-10000的和,当一个任务的计算数量大于3000时拆分任务,数量小于3000时计算。
|
||||
|
||||

|
||||
|
||||
```java
|
||||
public class ForkJoinDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
long start = System.currentTimeMillis();
|
||||
ForkJoinPool pool = new ForkJoinPool();
|
||||
SumRecursiveTask task = new SumRecursiveTask(1, 10000L);
|
||||
Long result = pool.invoke(task);
|
||||
System.out.println("result = " + result);
|
||||
long end = System.currentTimeMillis();
|
||||
|
||||
System.out.println("消耗的时间为: " + (end - start));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SumRecursiveTask extends RecursiveTask<Long> {
|
||||
|
||||
private static final long THRESHOLD = 3000L;
|
||||
private final long start;
|
||||
private final long end;
|
||||
|
||||
public SumRecursiveTask(long start, long end){
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Long compute() {
|
||||
long length = end - start;
|
||||
|
||||
if (length <= THRESHOLD) {
|
||||
// 任务不用再拆分了.可以计算了
|
||||
long sum = 0;
|
||||
for (long i = start; i <= end; i++) {
|
||||
sum += i;
|
||||
}
|
||||
return sum;
|
||||
} else {
|
||||
// 数量大于预定的数量,任务还需要再拆分
|
||||
long middle = (start + end) / 2;
|
||||
|
||||
SumRecursiveTask left = new SumRecursiveTask(start, middle);
|
||||
left.fork();
|
||||
SumRecursiveTask right = new SumRecursiveTask(middle + 1, end);
|
||||
right.fork();
|
||||
return left.join() + right.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.9 小结
|
||||
|
||||
1. parallelStream是线程不安全的
|
||||
2. parallelStream适用的场景是CPU密集型的,只是做到别浪费CPU,假如本身电脑CPU的负载很大,那还到处用并行流,那并不能起到作用
|
||||
3. I/O密集型磁盘I/O、网络I/O都属于I/O操作,这部分操作是较少消耗CPU资源,一般并行流中不适用于I/O密集型的操作,就比如使用并流行进行大批量的消息推送,涉及到了大量I/O,使用并行流反而慢了很多
|
||||
4. 在使用并行流的时候是无法保证元素的顺序的,也就是即使你用了同步集合也只能保证元素都正确但无法保证其中的顺序
|
||||
445
blogs/java/java8/java8-4.md
Normal file
445
blogs/java/java8/java8-4.md
Normal file
@@ -0,0 +1,445 @@
|
||||
---
|
||||
title: JDK8 特性(四)
|
||||
date: 2022-05-26
|
||||
sidebar: 'auto'
|
||||
tags:
|
||||
- JDK8
|
||||
categories:
|
||||
- Java
|
||||
---
|
||||
## 1 Optional
|
||||
|
||||
### 1.1 Optional介绍
|
||||
|
||||
- Optional是一个没有子类的工具类,Optional是一个可以为null的容器对象。它的作用主要就是为了解决避免Null检查,防止NullPointerException。
|
||||
|
||||
### 1.2 Optional基本使用
|
||||
|
||||
- Optional类的创建方式
|
||||
|
||||
```text
|
||||
Optional.of(T t) : 创建一个 Optional 实例, 不能为空
|
||||
Optional.empty() : 创建一个空的 Optional 实例
|
||||
Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例
|
||||
```
|
||||
|
||||
- Optional类的常用方法
|
||||
|
||||
```text
|
||||
isPresent() : 判断是否包含值,包含值返回true,不包含值返回false
|
||||
get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException
|
||||
orElse(T t) : 如果调用对象包含值,返回该值,否则返回参数t
|
||||
orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值
|
||||
map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()
|
||||
```
|
||||
|
||||
### 1.3 Optional高级使用
|
||||
|
||||
```java
|
||||
public class OptionalTest {
|
||||
@Test
|
||||
public void ifPresent(){
|
||||
// Optional<User> op = Optional.of(new User("张三", 18));
|
||||
Optional<User> op = Optional.empty();
|
||||
|
||||
// 如果存在就做点什么, 没有就不做了
|
||||
op.ifPresent(user -> {
|
||||
System.out.println(user.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void map(){
|
||||
User user = new User("迪丽热巴", 20);
|
||||
// 判断如果存在, 那么就将名字重新拼接
|
||||
Optional<User> optional = Optional.of(user);
|
||||
System.out.println(getNewName(optional));
|
||||
}
|
||||
|
||||
private String getNewName(Optional<User> optional){
|
||||
return optional.map(User::getUsername)
|
||||
.map(str -> str.substring(2))
|
||||
.orElse("null");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2 新的日期时间API
|
||||
|
||||
- 旧版日期时间API存在的问题
|
||||
1. 设计很差:在`java.util`和`java.sql`的包中都有日期类,`java.util.Date`同时包含日期和时间,而`java.sql.Date`仅包含日期。此外用于格式化和解析的类在`java.text`包中定义。
|
||||
2. 非线程安全:`java.util.Date`是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
|
||||
3. 时区处理麻烦:日期类并不提供国际化,没有时区支持,因此Java引入了`java.util.Calendar`和 `java.util.TimeZone`类,但他们同样存在上述所有的问题。
|
||||
|
||||
### 2.1 新的日期时间API介绍
|
||||
|
||||
- JDK 8中增加了一套全新的日期时间API,这套API设计合理,是线程安全的。新的日期及时间API位于 `java.time`包中,下面是一些关键类。
|
||||
- LocalDate :表示日期,包含年月日,格式为 2019-10-16
|
||||
- LocalTime :表示时间,包含时分秒,格式为 16:38:54.158549300
|
||||
- LocalDateTime :表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750
|
||||
- DateTimeFormatter :日期时间格式化类。
|
||||
- Instant:时间戳,表示一个特定的时间瞬间。
|
||||
- Duration:用于计算2个时间(LocalTime,时分秒)的距离
|
||||
- Period:用于计算2个日期(LocalDate,年月日)的距离
|
||||
- ZonedDateTime :包含时区的时间
|
||||
|
||||
- Java中使用的历法是ISO 8601日历系统,它是世界民用历法,也就是我们所说的公历。平年有365天,闰年是366天。此外Java 8还提供了4套其他历法,分别是:
|
||||
- ThaiBuddhistDate:泰国佛教历
|
||||
- MinguoDate:中华民国历
|
||||
- JapaneseDate:日本历
|
||||
- HijrahDate:伊斯兰历
|
||||
|
||||
### 2.2 日期时间类
|
||||
|
||||
- LocalDate、LocalTime、LocalDateTime类的实例是不可变的对象,分别表示使用 ISO-8601 日历系统的日期、时 间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
|
||||
|
||||
- LocalDate: 获取日期的信息
|
||||
|
||||
```java
|
||||
public class DateTest {
|
||||
@Test
|
||||
public void localDate(){
|
||||
// 创建指定日期
|
||||
LocalDate localDate = LocalDate.of(2022, 11, 11);
|
||||
System.out.println("localDate = " + localDate); // localDate = 2022-11-11
|
||||
|
||||
// 得到当前日期
|
||||
LocalDate now = LocalDate.now();
|
||||
System.out.println("now = " + now); // now = 2022-05-23
|
||||
|
||||
// 获取日期信息
|
||||
System.out.println("年: " + now.getYear()); // 年: 2022
|
||||
System.out.println("月: " + now.getMonthValue()); // 月: 5
|
||||
System.out.println("日: " + now.getDayOfMonth()); // 日: 23
|
||||
System.out.println("星期: " + now.getDayOfWeek()); // 星期: MONDAY
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- LocalTime: 获取时间信息
|
||||
|
||||
```java
|
||||
public class DateTest {
|
||||
@Test
|
||||
public void localTime(){
|
||||
// 得到指定的时间
|
||||
LocalTime time = LocalTime.of(12, 12, 12);
|
||||
System.out.println("time = " + time); // time = 12:12:12
|
||||
|
||||
// 得到当前时间
|
||||
LocalTime now = LocalTime.now();
|
||||
System.out.println("now = " + now); // now = 11:43:10.495
|
||||
|
||||
// 获取时间信息
|
||||
System.out.println("小时: " + now.getHour()); // 小时: 11
|
||||
System.out.println("分钟: " + now.getMinute()); // 分钟: 43
|
||||
System.out.println("秒: " + now.getSecond()); // 秒: 10
|
||||
System.out.println("纳秒: " + now.getNano()); // 纳秒: 495000000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- LocalDateTime: 获取日期时间类型
|
||||
|
||||
```java
|
||||
public class DateTest {
|
||||
@Test
|
||||
public void localDateTime(){
|
||||
LocalDateTime time = LocalDateTime.of(2022, 11, 11, 12, 12, 12);
|
||||
System.out.println("time = " + time); // time = 2022-11-11T12:12:12
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
System.out.println("now = " + now); // now = 2022-05-23T11:47:29.308
|
||||
|
||||
// 获取日期信息
|
||||
System.out.println("年: " + now.getYear()); // 年: 2022
|
||||
System.out.println("月: " + now.getMonthValue()); // 月: 5
|
||||
System.out.println("日: " + now.getDayOfMonth()); // 日: 23
|
||||
System.out.println("时: " + now.getHour()); // 时: 11
|
||||
System.out.println("分: " + now.getMinute()); // 分: 47
|
||||
System.out.println("秒: " + now.getSecond()); // 秒: 29
|
||||
System.out.println("纳秒: " + now.getNano()); // 纳秒: 308000000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 对日期时间的修改,对已存在的LocalDate对象,创建它的修改版,最简单的方式是使用withAttribute方法。 withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。以下所有的方法都返回了一个修改属性的对象,他们不会影响原来的对象。
|
||||
|
||||
```java
|
||||
public class DateTest {
|
||||
@Test
|
||||
public void localDateTimeUpdate(){
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
System.out.println("now = " + now); // now = 2022-05-23T12:47:18.640
|
||||
|
||||
// 修改年份, 每份修改后的数据都是一个新的对象, 不会与原对象冲突
|
||||
LocalDateTime setYear = now.withYear(2025);
|
||||
System.out.println("修改年份后 = " + setYear); // 修改年份后 = 2025-05-23T12:47:18.640
|
||||
System.out.println("setYear == now? " + (setYear == now)); // setYear == now? false
|
||||
|
||||
System.out.println("修改月份: " + now.withMonth(1)); // 修改月份: 2022-01-23T12:47:18.640
|
||||
System.out.println("修改日: " + now.withDayOfMonth(1)); // 修改日: 2022-05-01T12:47:18.640
|
||||
System.out.println("修改小时: " + now.withMonth(12)); // 修改小时: 2022-12-23T12:47:18.640
|
||||
|
||||
// 在当前对象的基础上加上或减去指定的时间
|
||||
LocalDateTime localDateTime = now.plusDays(5);
|
||||
System.out.println("五天后: " + localDateTime); // 五天后: 2022-05-28T12:47:18.640
|
||||
System.out.println("10年后: " + now.plusYears(10)); // 10年后: 2032-05-23T12:47:18.640
|
||||
System.out.println("20年后: " + now.plusYears(20)); // 20年后: 2042-05-23T12:47:18.640
|
||||
|
||||
System.out.println("3个月前: " + now.minusMonths(3)); // 3个月前: 2022-02-23T12:47:18.640
|
||||
System.out.println("3分钟前: " + now.minusMinutes(3)); // 3分钟前: 2022-05-23T12:44:18.640
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 日期比较
|
||||
|
||||
```java
|
||||
public class DateTest {
|
||||
@Test
|
||||
public void dateEqual(){
|
||||
// 在JDK8中,LocalDate类中使用isBefore()、isAfter()、equals()方法来比较两个日期,可直接进行比较。
|
||||
LocalDate now = LocalDate.now();
|
||||
LocalDate date = LocalDate.of(2011, 11, 11);
|
||||
|
||||
System.out.println(now.isBefore(date)); // false
|
||||
System.out.println(date.isBefore(now)); // true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 日期格式化与解析
|
||||
|
||||
- 通过`java.time.format.DateTimeFormatter`类可以进行日期时间解析与格式化。
|
||||
|
||||
```java
|
||||
public class DateTest {
|
||||
@Test
|
||||
public void dateFormatAndParse(){
|
||||
// 获得当前的日期
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 自定义日期格式
|
||||
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd & HH:mm:ss");
|
||||
// 将日期转换为对应的格式
|
||||
String format = dateTimeFormatter.format(now);
|
||||
System.out.println("format = " + format);
|
||||
|
||||
// 将字符串解析为时间
|
||||
LocalDateTime time = LocalDateTime.parse(format, dateTimeFormatter);
|
||||
System.out.println("time = " + time);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Instant类
|
||||
|
||||
- Instant 时间戳/时间线,内部保存了从1970年1月1日 00:00:00以来的秒和纳秒。
|
||||
- 只能操作秒以下的级别, 用来程序做一些统计的类, 比如说计算运行时间等, 不是给用户使用的
|
||||
|
||||
```java
|
||||
public class InstantTest {
|
||||
@Test
|
||||
public void instant(){
|
||||
Instant now = Instant.now();
|
||||
System.out.println("当前时间戳 = " + now);
|
||||
// 获取从1970 01 01 00:00:00的秒
|
||||
System.out.println(now.getNano());
|
||||
System.out.println(now.getEpochSecond());
|
||||
System.out.println(now.toEpochMilli());
|
||||
System.out.println(System.currentTimeMillis());
|
||||
System.out.println("5s以后: " + now.plusSeconds(5));
|
||||
|
||||
Instant instant = Instant.ofEpochSecond(5);
|
||||
System.out.println("instant = " + instant);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 计算日期时间差的类
|
||||
|
||||
- Duration/Period类: 计算日期时间差。
|
||||
1. Duration:用于计算2个时间(LocalTime,时分秒)的距离
|
||||
2. Period:用于计算2个日期(LocalDate,年月日)的距离
|
||||
|
||||
- 注意: 两个类的计算都是第二个参数减去第一个参数, 所以当参数对调时会产生负数的情况
|
||||
|
||||
```java
|
||||
public class DifferenceDateTest {
|
||||
@Test
|
||||
public void test(){
|
||||
// 获取当前时间
|
||||
LocalTime now = LocalTime.now();
|
||||
LocalTime of = LocalTime.of(12, 12, 12);
|
||||
|
||||
// 使用Duration计算时间的间隔
|
||||
Duration duration = Duration.between(of, now);
|
||||
System.out.println("相差的天数" + duration.toDays());
|
||||
System.out.println("相差的小时数" + duration.toHours());
|
||||
System.out.println("相差的分钟数" + duration.toMinutes());
|
||||
System.out.println("相差的秒数" + duration.toMillis());
|
||||
|
||||
// 获取当前的日期
|
||||
LocalDate date = LocalDate.now();
|
||||
LocalDate localDate = LocalDate.of(2021, 2, 2);
|
||||
|
||||
// 使用Period计算日期的间隔
|
||||
Period period = Period.between(localDate, date);
|
||||
System.out.println("相差的年" + period.getYears());
|
||||
System.out.println("相差的月" + period.getMonths());
|
||||
System.out.println("相差的日" + period.getDays());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.6 时间校正器
|
||||
|
||||
- 有时我们可能需要获取例如:将日期调整到“下一个月的第一天”等操作。可以通过时间校正器来进行。
|
||||
- TemporalAdjuster : 时间校正器。
|
||||
- TemporalAdjusters : 该类通过静态方法提供了大量的常用TemporalAdjuster的实现。
|
||||
|
||||
```java
|
||||
public class Demo{
|
||||
@Test
|
||||
public void temporalAdjuster(){
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 自定义时间调整器
|
||||
LocalDateTime with = now.with(temporal -> {
|
||||
LocalDateTime dateTime = (LocalDateTime) temporal;
|
||||
// 修改到下个月1号
|
||||
return dateTime.plusMonths(1).withDayOfMonth(1);
|
||||
});
|
||||
System.out.println("with = " + with);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.7 设置日期时间的时区
|
||||
|
||||
- Java8 中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime。
|
||||
- 其中每个时区都对应着ID,ID的格式为 “区域/城市” 。例如 :Asia/Shanghai 等。
|
||||
- ZoneId:该类中包含了所有的时区信息。
|
||||
|
||||
```java
|
||||
public class Demo{
|
||||
@Test
|
||||
public void test(){
|
||||
// 获取所有的时区ID
|
||||
ZoneId.getAvailableZoneIds().forEach(System.out::println);
|
||||
|
||||
// 不带时区获取计算机的当前时间
|
||||
// 中国默认时区东八区, 比标准时间快8h
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
System.out.println("now = " + now);
|
||||
|
||||
// 操作带时区的类
|
||||
// 创建世界标准时间, 不带参数则使用计算机默认的时区
|
||||
ZonedDateTime zonedDateTime = ZonedDateTime.now(Clock.systemUTC());
|
||||
System.out.println("zonedDateTime = " + zonedDateTime);
|
||||
System.out.println("指定时区获取时间: " + ZonedDateTime.now(ZoneId.of("Europe/London")));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.8 小结
|
||||
|
||||
- 详细学习了新的日期是时间相关类,LocalDate表示日期,包含年月日,LocalTime表示时间,包含时分 秒,`LocalDateTime = LocalDate + LocalTime`,时间的格式化和解析,通过DateTimeFormatter类型进行.
|
||||
- 学习了Instant类,方便操作秒和纳秒,一般是给程序使用的.学习Duration/Period计算日期或时间的距离,还使用时间调整器方便的调整时间,学习了带时区的3个类ZoneDate/ZoneTime/ZoneDateTime
|
||||
|
||||
- JDK 8新的日期和时间API的优势:
|
||||
1. 新版的日期和时间API中,日期和时间对象是不可变的。操纵的日期不会影响老值,而是新生成一个实例。
|
||||
2. 新的API提供了两种不同的时间表示方式,有效地区分了人和机器的不同需求。
|
||||
3. TemporalAdjuster可以更精确的操纵日期,还可以自定义日期调整器。
|
||||
4. 是线程安全的
|
||||
|
||||
## 3 重复注解与类型注释
|
||||
|
||||
### 3.1 重复注解的使用
|
||||
|
||||
- 自从Java 5中引入注解以来,注解开始变得非常流行,并在各个框架和项目中被广泛使用。不过注解有一个很大的限制是:在同一个地方不能多次使用同一个注解。JDK 8引入了重复注解的概念,允许在同一个地方多次使用同一个注解。在JDK 8中使用`@Repeatable`注解定义重复注解。
|
||||
|
||||
- 重复注解的使用步骤:
|
||||
1. 定义重复的注解容器注解
|
||||
2. 定义一个可以重复的注解
|
||||
3. 配置多个重复的注解
|
||||
4. 解析得到指定注解
|
||||
|
||||
```java
|
||||
/** 3. 配置多个重复的注解 **/
|
||||
@MyTest("AAA")
|
||||
@MyTest("BBB")
|
||||
@MyTest("CCC")
|
||||
public class RepeatableTest {
|
||||
|
||||
/** 3. 配置多个重复的注解 **/
|
||||
@Test
|
||||
@MyTest("DD")
|
||||
@MyTest("EE")
|
||||
public void test(){}
|
||||
|
||||
public static void main(String[] args) throws NoSuchMethodException {
|
||||
// 4. 解析得到指定注解
|
||||
// getAnnotationsByType是新增API用来获取重复注解的
|
||||
// 获取类上的注解
|
||||
MyTest[] annotation = RepeatableTest.class.getAnnotationsByType(MyTest.class);
|
||||
for (MyTest myTest : annotation) {
|
||||
System.out.println("myTest = " + myTest.value());
|
||||
}
|
||||
|
||||
// 获取方法上的注解
|
||||
MyTest[] tests = RepeatableTest.class.getMethod("test").getAnnotationsByType(MyTest.class);
|
||||
for (MyTest myTest : tests) {
|
||||
System.out.println("myTest = " + myTest.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 定义重复的注解容器注解
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface MyTests{
|
||||
MyTest[] value();
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. 定义一个可以重复的注解
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(MyTests.class)
|
||||
@interface MyTest{
|
||||
String value();
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 类型注解的使用
|
||||
|
||||
- JDK 8为@Target元注解新增了两种类型:TYPE_PARAMETER,TYPE_USE 。
|
||||
- TYPE_PARAMETER :表示该注解能写在类型参数的声明语句中。
|
||||
- TYPE_USE :表示注解可以再任何用到类型的地方使用。
|
||||
|
||||
```java
|
||||
public class TypeAnnotationDemo<@TypeParam T> {
|
||||
private @NotNull int one = 10;
|
||||
public <@TypeParam E extends Integer> void test(@NotNull E ele){}
|
||||
public static void main(String[] args) {
|
||||
@NotNull int x = 2;
|
||||
@NotNull String str = new @NotNull String();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 该注解能用到类型前面
|
||||
*/
|
||||
@Target(ElementType.TYPE_USE)
|
||||
@interface NotNull {}
|
||||
|
||||
/**
|
||||
* 该注解能写在泛型上
|
||||
*/
|
||||
@Target(ElementType.TYPE_PARAMETER)
|
||||
@interface TypeParam {}
|
||||
```
|
||||
|
||||
1205
blogs/java/jvm/JVM-内存结构.md
Normal file
1205
blogs/java/jvm/JVM-内存结构.md
Normal file
File diff suppressed because it is too large
Load Diff
885
blogs/java/jvm/JVM-垃圾回收.md
Normal file
885
blogs/java/jvm/JVM-垃圾回收.md
Normal file
@@ -0,0 +1,885 @@
|
||||
---
|
||||
title: JVM之垃圾回收
|
||||
date: 2022-12-01
|
||||
sidebar: 'auto'
|
||||
tags:
|
||||
- JVM
|
||||
categories:
|
||||
- Java
|
||||
---
|
||||
## 1 如何判断对象可以回收
|
||||
|
||||
|
||||
|
||||
### 1.1 引用计数法
|
||||
|
||||
当一个对象被引用时,则会将该对象进行引用计数。数值的大小代表被引用的数量,当数值为0时,则会被当作垃圾进行回收。
|
||||
|
||||
**弊端**:当有对象形成互相引用的关系,那么则会造成内存泄漏的问题。
|
||||
|
||||
**Java虚拟机 不使用该方式来进行垃圾回收判断**
|
||||
|
||||
|
||||
|
||||
### 1.2 可达性分析算法
|
||||
|
||||
- Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
|
||||
|
||||
- 扫描堆中的对象,看是否能够沿着 GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
|
||||
|
||||
- 哪些对象可以作为 GC Root ?
|
||||
|
||||
|
||||
|
||||
> Tip:
|
||||
>
|
||||
> 使用 Eclipse 官方提供的可视化工具 Memory Analyzer 进行内存分析
|
||||
>
|
||||
> 官网下载地址:https://www.eclipse.org/mat/downloads.php
|
||||
>
|
||||
> - 使用说明
|
||||
> 1. 使用 `jps` 命令定位到进程id
|
||||
> 2. 使用 `jmap -dump:format=b,live,file=<文件名.bin> 进程id`
|
||||
> - `format=b`:转储文件格式
|
||||
> - `live`:存活对象,会在快照前进行一次垃圾回收
|
||||
> - `file=<文件名.bin>`:将内存快照存放到文件中
|
||||
>
|
||||
>
|
||||
>
|
||||
> 使用案例
|
||||
>
|
||||
> ```java
|
||||
> public class Demo2_1 {
|
||||
> public static void main(String[] args) throws IOException {
|
||||
> List<Object> list = new ArrayList<>();
|
||||
> list.add("a");
|
||||
> list.add("b");
|
||||
> System.out.println(1);
|
||||
> System.in.read();
|
||||
>
|
||||
> list = null;
|
||||
> System.out.println(2);
|
||||
> System.in.read();
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> 对以上代码进行运行时内存抓取,来查看控制台输出 1 和 2 时,内存中数据的不同之处
|
||||
>
|
||||
> 1. 运行代码后,使用 `jps` 命令获取进程id,然后 dump 此时的内存状态,使用 Memory Analyzer 读取内存信息
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 2. dump 第二次内存状态,使用 Memory Analyzer 读取内存信息
|
||||
>
|
||||
> 
|
||||
|
||||
|
||||
|
||||
### 1.3 四种引用
|
||||
|
||||
1. 强引用
|
||||
|
||||
- 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
|
||||
|
||||
2. 软引用(SoftReference)
|
||||
|
||||
- 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
|
||||
|
||||
- 可以配合引用队列来释放软引用自身
|
||||
|
||||
3. 弱引用(WeakReference)
|
||||
|
||||
- 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
|
||||
|
||||
- 可以配合引用队列来释放弱引用自身
|
||||
|
||||
4. 虚引用(PhantomReference)
|
||||
|
||||
- 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存
|
||||
|
||||
5. 终结器引用(FinalReference)
|
||||
|
||||
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象
|
||||
|
||||
|
||||
|
||||
#### 1.3.1 软引用
|
||||
|
||||
案例代码,用来演示内存不够时,GC 垃圾回收会做什么事情,需要添加虚拟机参数 `-Xmx20m -XX:+PrintGCDetails -verbose:gc`
|
||||
|
||||
```java
|
||||
public class Demo2_2 {
|
||||
private static final int _4MB = 1024 * 1024 * 4;
|
||||
public static void main(String[] args) throws IOException {
|
||||
List<byte[]> list = new ArrayList<>();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
list.add(new byte[_4MB]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
分析以上代码结果:循环向数组中添加5次大小为 4M 的字节数组,因为将虚拟机堆的大小设置为 20M,所以该程序的结果就是,发生了几次 GC垃圾回收后,抛出异常 `java.lang.OutOfMemoryError: Java heap space`
|
||||
|
||||
|
||||
|
||||
使用软引用解决这个问题,代码修改为:将 byte[] 数据通过一个 SoftReference 软引用对象来实例
|
||||
|
||||
```java
|
||||
public class Demo2_2 {
|
||||
private static final int _4MB = 1024 * 1024 * 4;
|
||||
public static void main(String[] args) throws IOException {
|
||||
List<SoftReference<byte[]>> list = new ArrayList<>();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
list.add(new SoftReference<>(new byte[_4MB]));
|
||||
list.forEach(ref -> System.out.print(ref.get() + "\t"));
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行结果如下,可以发现
|
||||
|
||||
- 在加入第四个对象的时候,已经发生了内存空间不充裕的情况,调用了 Minor GC,清理了新生代的内存空间
|
||||
- 在加入第五个对象的时候,使用 Minor GC 后发现根本没有垃圾可以清理,就调用了 Full GC 进行深度清理 ,再使用 Minor GC 尝试再次清理新生代,发现还是不起作用,再次使用 Full GC 时就进行了软引用内存的清理,使得前面存入的软引用数据全部清空了,但是软引用对象 `SoftReference` 不会进行释放
|
||||
- 印证了:当内存实在不充裕的情况下,会将软引用数据清空
|
||||
|
||||
```tex
|
||||
[B@404b9385
|
||||
[B@404b9385 [B@6d311334
|
||||
[B@404b9385 [B@6d311334 [B@682a0b20
|
||||
[GC (Allocation Failure) [PSYoungGen: 3804K->496K(6144K)] 16092K->13164K(19968K), 0.0012198 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
|
||||
[B@404b9385 [B@6d311334 [B@682a0b20 [B@3d075dc0
|
||||
[GC (Allocation Failure) --[PSYoungGen: 4704K->4704K(6144K)] 17372K->17436K(19968K), 0.0008617 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
|
||||
[Full GC (Ergonomics) [PSYoungGen: 4704K->4523K(6144K)] [ParOldGen: 12732K->12697K(13824K)] 17436K->17221K(19968K), [Metaspace: 4212K->4212K(1056768K)], 0.0059000 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
|
||||
[GC (Allocation Failure) --[PSYoungGen: 4523K->4523K(6144K)] 17221K->17245K(19968K), 0.0006258 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
|
||||
[Full GC (Allocation Failure) [PSYoungGen: 4523K->0K(6144K)] [ParOldGen: 12721K->781K(8704K)] 17245K->781K(14848K), [Metaspace: 4212K->4206K(1056768K)], 0.0073492 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
|
||||
null null null null [B@214c265e
|
||||
```
|
||||
|
||||
|
||||
|
||||
现在还有一个问题,软引用数据已经清空了,那软引用对象 `SoftReference` 什么时候、又是怎么被清空的?
|
||||
|
||||
- 需要使用 `ReferenceQueue` 队列来存储对应的需要进行垃圾回收的软引用对象,然后手动进行对应数组数据的删除,之后的软引用回收交给 GC垃圾回收 去做
|
||||
|
||||
```java
|
||||
public class Demo2_2 {
|
||||
private static final int _4MB = 1024 * 1024 * 4;
|
||||
public static void main(String[] args) throws IOException {
|
||||
List<SoftReference<byte[]>> list = new ArrayList<>();
|
||||
// 软引用队列
|
||||
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
// 关联软引用队列,将来软引用对象关联的数据 byte[] 被清空了之后,软引用自己会加入到队列中
|
||||
list.add(new SoftReference<>(new byte[_4MB], queue));
|
||||
list.forEach(ref -> System.out.print(ref.get() + "\t"));
|
||||
System.out.println();
|
||||
}
|
||||
// 去除没用的软引用对象
|
||||
System.out.println("=====华丽分界线=====");
|
||||
Reference<? extends byte[]> poll = queue.poll();
|
||||
while (poll != null) {
|
||||
list.remove(poll);
|
||||
poll = queue.poll();
|
||||
}
|
||||
for (SoftReference<byte[]> softReference : list) {
|
||||
System.out.println(softReference.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行结果可以发现所有空的软引用对象都从数组中删除了
|
||||
|
||||
```tex
|
||||
[B@404b9385
|
||||
[B@404b9385 [B@6d311334
|
||||
[B@404b9385 [B@6d311334 [B@682a0b20
|
||||
[B@404b9385 [B@6d311334 [B@682a0b20 [B@3d075dc0
|
||||
null null null null [B@214c265e
|
||||
=====华丽分界线=====
|
||||
[B@214c265e
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 1.3.2 弱引用
|
||||
|
||||
使用弱引用来存放数据,需要添加虚拟机参数 `-Xmx20m -XX:+PrintGCDetails -verbose:gc`
|
||||
|
||||
```java
|
||||
public class Demo2_3 {
|
||||
private static final int _4MB = 1024 * 1024 * 4;
|
||||
public static void main(String[] args) throws IOException {
|
||||
List<WeakReference<byte[]>> list = new ArrayList<>();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
list.add(new WeakReference<>(new byte[_4MB]));
|
||||
list.forEach(ref -> System.out.print(ref.get() + "\t"));
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行结果如下,分析这几次GC的结果
|
||||
|
||||
1. 执行 Minor GC,清除新生代,获得内存空间
|
||||
2. 执行 Minor GC,清除新生代,将上一次还未放入老年代的弱引用数据清除,获得内存空间
|
||||
3. (3) -> (6) 同上步骤来获取内存空间
|
||||
4. 直到第 (7) 的时候,Minor GC 已经缓解不了当前内存空间不足的窘境,发起了 Full GC,将所有的弱引用数据全部清空,获取更多的内存空间
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
从上述引出了一个问题,不是说 `仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 `?
|
||||
|
||||
是因为有些弱引用对象存活到了老年代中,而触发 Minor GC 却清理不到老年代中的内存,所以只有当触发了 Full GC 的时候才能进行老年代中的弱引用对象数据的清理。
|
||||
|
||||
弱引用对象的释放同软引用的释放方式,需要一个 `WeakReferenceQueue` 队列来进行弱引用对象的绑定。
|
||||
|
||||
|
||||
|
||||
## 2 垃圾回收算法
|
||||
|
||||
### 2.1 标记清除
|
||||
|
||||
定义: Mark Sweep
|
||||
|
||||
- 速度较快
|
||||
|
||||
凡是没有被任何 GC Root 引用的对象,都是没有用的数据,则会被清除
|
||||
|
||||
缺点:内存碎片过多,使得当有更大的内存需要使用时导致内存溢出
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 2.2 标记整理
|
||||
|
||||
定义:Mark Compact
|
||||
|
||||
- 速度慢
|
||||
|
||||
在内存清理时,顺便将内存碎片进行整理
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 2.3 复制
|
||||
|
||||
定义:Copy
|
||||
|
||||
- 不会有内存碎片
|
||||
- 需要占用双倍内存空间
|
||||
|
||||
总有一半的内存空间用来进行内存重排
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 3 分代垃圾回收
|
||||
|
||||

|
||||
|
||||
- 对象首先分配在伊甸园区域
|
||||
- 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
|
||||
- minor gc 会引发 stop the world(STW),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
|
||||
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
|
||||
- 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
|
||||
|
||||
|
||||
|
||||
### 3.1 相关 VM 参数
|
||||
|
||||
| 描述 | 命令 |
|
||||
| ------------------ | ----------------------------------------------------------- |
|
||||
| 堆初始大小 | -Xms |
|
||||
| 堆最大大小 | -Xmx \| -XX:MaxHeapSize=size |
|
||||
| 新生代大小 | -Xmn \| -XX:NewSize=size + -XX:MaxNewSize=size |
|
||||
| 幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:UseAdaptiveSizePolicy |
|
||||
| 幸存区比例 | -XX:SurvivorRatio=ratio |
|
||||
| 晋升阈值 | -XX:MaxTenuringThreshold=threshold |
|
||||
| 晋升详情 | -XX:+PrintTenuringDistribution |
|
||||
| GC详情 | -XX:+PrintGCDetails -verbose:gc |
|
||||
| FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
|
||||
|
||||
|
||||
|
||||
### 3.2 GC分析
|
||||
|
||||
案例代码,需要使用虚拟机参数 `-Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc`
|
||||
|
||||
```java
|
||||
public class Demo2_4 {
|
||||
private static final int _512KB = 512 * 1024;
|
||||
private static final int _1MB = 1024 * 1024;
|
||||
private static final int _6MB = 6 * 1024 * 1024;
|
||||
private static final int _7MB = 7 * 1024 * 1024;
|
||||
private static final int _8MB = 8 * 1024 * 1024;
|
||||
public static void main(String[] args) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行以上案例代码,来分析一下打印的数据
|
||||
|
||||
- `def new generation`:新生代的信息,total 表示最大内存大小(伊甸园区 + form区),used 表示已使用,后面的十六进制代表地址
|
||||
- `eden space`:伊甸园,占比 80%
|
||||
- `from space`:from 分区,占比10%
|
||||
- `to space`:to 分区,占比10%
|
||||
- `tenured generation`:老年代的信息,total 表示最大内存大小,uesd 表示已使用
|
||||
- `Metaspace`:元数据信息,并不在堆中,只是方便查看,目前不关心这块
|
||||
|
||||
```tex
|
||||
Heap
|
||||
def new generation total 9216K, used 2020K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
|
||||
eden space 8192K, 24% used [0x00000000fec00000, 0x00000000fedf9048, 0x00000000ff400000)
|
||||
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
|
||||
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
|
||||
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
|
||||
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
|
||||
Metaspace used 3164K, capacity 4496K, committed 4864K, reserved 1056768K
|
||||
class space used 345K, capacity 388K, committed 512K, reserved 1048576K
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 3.2.1 分析1
|
||||
|
||||
修改案例代码为以下代码,将大小为 7MB 的数据放入数组
|
||||
|
||||
```java
|
||||
public class Demo2_4 {
|
||||
private static final int _7MB = 7 * 1024 * 1024;
|
||||
public static void main(String[] args) {
|
||||
List<byte[]> list = new ArrayList<>();
|
||||
list.add(new byte[_7MB]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行结果进行分析,发现
|
||||
|
||||
- 执行了一次 Minor GC,伊甸园最大空间为 8MB,开辟一块 7MB 的内存需要整理堆空间,将一部分数据放入了 from 分区
|
||||
- 伊甸园占用了 94% 空间,此时还没用上老年代的空间
|
||||
|
||||
```tex
|
||||
[GC (Allocation Failure) [DefNew: 1855K->600K(9216K), 0.0025878 secs] 1855K->600K(19456K), 0.0032786 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
|
||||
Heap
|
||||
def new generation total 9216K, used 8342K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
|
||||
eden space 8192K, 94% used [0x00000000fec00000, 0x00000000ff38f7a0, 0x00000000ff400000)
|
||||
from space 1024K, 58% used [0x00000000ff500000, 0x00000000ff596228, 0x00000000ff600000)
|
||||
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
|
||||
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
|
||||
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
|
||||
Metaspace used 3223K, capacity 4496K, committed 4864K, reserved 1056768K
|
||||
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 3.2.2 分析2
|
||||
|
||||
在 分析1 的基础上再添加 512KB
|
||||
|
||||
```java
|
||||
public class Demo2_4 {
|
||||
private static final int _512KB = 512 * 1024;
|
||||
private static final int _7MB = 7 * 1024 * 1024;
|
||||
public static void main(String[] args) {
|
||||
List<byte[]> list = new ArrayList<>();
|
||||
list.add(new byte[_7MB]);
|
||||
list.add(new byte[_512KB]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行结果进行分析,发现
|
||||
|
||||
- 执行了一次 Minor GC,在 7MB 的基础上添加 512KB 伊甸园已经把内存用完了,此时老年代还未用上
|
||||
|
||||
```tex
|
||||
[GC (Allocation Failure) [DefNew: 1685K->599K(9216K), 0.0011811 secs] 1685K->599K(19456K), 0.0012264 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
|
||||
Heap
|
||||
def new generation total 9216K, used 8791K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
|
||||
eden space 8192K, 100% used [0x00000000fec00000, 0x00000000ff400000, 0x00000000ff400000)
|
||||
from space 1024K, 58% used [0x00000000ff500000, 0x00000000ff595d10, 0x00000000ff600000)
|
||||
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
|
||||
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
|
||||
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
|
||||
Metaspace used 3225K, capacity 4496K, committed 4864K, reserved 1056768K
|
||||
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 3.2.3 分析3
|
||||
|
||||
在 分析2 的基础上再添加 512KB
|
||||
|
||||
```java
|
||||
public class Demo2_4 {
|
||||
private static final int _512KB = 512 * 1024;
|
||||
private static final int _7MB = 7 * 1024 * 1024;
|
||||
public static void main(String[] args) {
|
||||
List<byte[]> list = new ArrayList<>();
|
||||
list.add(new byte[_7MB]);
|
||||
list.add(new byte[_512KB]);
|
||||
list.add(new byte[_512KB]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行结果进行分析,发现
|
||||
|
||||
- 执行了两次 Minor GC,在第二次添加 512KB 内存时发生的 Minor GC 将新生代中的数据全部提升到了老年代(虽然此时还没到年龄阈值),使得伊甸园中的使用量大大减小
|
||||
|
||||
```tex
|
||||
[GC (Allocation Failure) [DefNew: 1676K->598K(9216K), 0.0011451 secs] 1676K->598K(19456K), 0.0011917 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
|
||||
[GC (Allocation Failure) [DefNew: 8769K->540K(9216K), 0.0053774 secs] 8769K->8303K(19456K), 0.0054079 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
|
||||
Heap
|
||||
def new generation total 9216K, used 1218K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
|
||||
eden space 8192K, 8% used [0x00000000fec00000, 0x00000000feca9748, 0x00000000ff400000)
|
||||
from space 1024K, 52% used [0x00000000ff400000, 0x00000000ff487368, 0x00000000ff500000)
|
||||
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
|
||||
tenured generation total 10240K, used 7762K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
|
||||
the space 10240K, 75% used [0x00000000ff600000, 0x00000000ffd949f8, 0x00000000ffd94a00, 0x0000000100000000)
|
||||
Metaspace used 3230K, capacity 4496K, committed 4864K, reserved 1056768K
|
||||
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 3.2.4 分析4-oom
|
||||
|
||||
当有以下情况时,直接添加 8MB 的数据
|
||||
|
||||
```java
|
||||
public class Demo2_4 {
|
||||
private static final int _8MB = 8 * 1024 * 1024;
|
||||
public static void main(String[] args) {
|
||||
List<byte[]> list = new ArrayList<>();
|
||||
list.add(new byte[_8MB]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行结果如下,发现
|
||||
|
||||
- 不会进行垃圾回收,伊甸园内存 8MB,该数据的大小已经在伊甸园中放不下了,JVM 将其直接晋升到老年代进行存储
|
||||
|
||||
```tex
|
||||
Heap
|
||||
def new generation total 9216K, used 2183K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
|
||||
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee21fb8, 0x00000000ff400000)
|
||||
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
|
||||
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
|
||||
tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
|
||||
the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
|
||||
Metaspace used 3276K, capacity 4496K, committed 4864K, reserved 1056768K
|
||||
class space used 355K, capacity 388K, committed 512K, reserved 1048576K
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 3.2.5 分析5
|
||||
|
||||
那如果在 分析4 的情况下再添加 8MB 的数据
|
||||
|
||||
```java
|
||||
public class Demo2_4 {
|
||||
private static final int _8MB = 8 * 1024 * 1024;
|
||||
public static void main(String[] args) {
|
||||
List<byte[]> list = new ArrayList<>();
|
||||
list.add(new byte[_8MB]);
|
||||
list.add(new byte[_8MB]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行结果如下,发现
|
||||
|
||||
- 在抛出 `java.lang.OutOfMemoryError: Java heap space` 异常前进行了两次垃圾回收,这两次垃圾回收是 JVM 尝试自救而触发的,当这两次的垃圾回收还无法解决时,就直接抛出了异常
|
||||
|
||||
```tex
|
||||
[GC (Allocation Failure) [DefNew: 2019K->607K(9216K), 0.0018340 secs][Tenured: 8192K->8798K(10240K), 0.0032842 secs] 10211K->8798K(19456K), [Metaspace: 3254K->3254K(1056768K)], 0.0057250 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
|
||||
[Full GC (Allocation Failure) [Tenured: 8798K->8780K(10240K), 0.0015195 secs] 8798K->8780K(19456K), [Metaspace: 3254K->3254K(1056768K)], 0.0015379 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
|
||||
Heap
|
||||
def new generation total 9216K, used 410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
|
||||
eden space 8192K, 5% used [0x00000000fec00000, 0x00000000fec66800, 0x00000000ff400000)
|
||||
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
|
||||
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
|
||||
tenured generation total 10240K, used 8780K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
|
||||
the space 10240K, 85% used [0x00000000ff600000, 0x00000000ffe932e8, 0x00000000ffe93400, 0x0000000100000000)
|
||||
Metaspace used 3321K, capacity 4496K, committed 4864K, reserved 1056768K
|
||||
class space used 362K, capacity 388K, committed 512K, reserved 1048576K
|
||||
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
|
||||
at com.zhuhjay.Demo2_4.main(Demo2_4.java:19)
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 3.2.6 分析6
|
||||
|
||||
当一个线程发生了 OOM,会不会导致主线程停止运行呢?
|
||||
|
||||
```java
|
||||
public class Demo2_4 {
|
||||
private static final int _8MB = 8 * 1024 * 1024;
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
new Thread(() -> {
|
||||
List<byte[]> list = new ArrayList<>();
|
||||
list.add(new byte[_8MB]);
|
||||
list.add(new byte[_8MB]);
|
||||
}).start();
|
||||
System.out.println("sleep start...");
|
||||
Thread.sleep(1000L);
|
||||
System.out.println("sleep end...");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
主线程仍然能够正常的运行,并且线程抛出异常后,会将线程使用的资源进行释放
|
||||
|
||||
```tex
|
||||
sleep start...
|
||||
[GC (Allocation Failure) [DefNew: 4023K->840K(9216K), 0.0021618 secs][Tenured: 8192K->9030K(10240K), 0.0028407 secs] 12215K->9030K(19456K), [Metaspace: 4205K->4205K(1056768K)], 0.0050747 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
|
||||
[Full GC (Allocation Failure) [Tenured: 9030K->8973K(10240K), 0.0024870 secs] 9030K->8973K(19456K), [Metaspace: 4205K->4205K(1056768K)], 0.0025340 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
|
||||
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
|
||||
at com.zhuhjay.Demo2_4.lambda$main$0(Demo2_4.java:20)
|
||||
at com.zhuhjay.Demo2_4$$Lambda$1/1324119927.run(Unknown Source)
|
||||
at java.lang.Thread.run(Thread.java:748)
|
||||
sleep end...
|
||||
Heap
|
||||
def new generation total 9216K, used 1540K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
|
||||
eden space 8192K, 18% used [0x00000000fec00000, 0x00000000fed81218, 0x00000000ff400000)
|
||||
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
|
||||
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
|
||||
tenured generation total 10240K, used 8973K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
|
||||
the space 10240K, 87% used [0x00000000ff600000, 0x00000000ffec37f8, 0x00000000ffec3800, 0x0000000100000000)
|
||||
Metaspace used 4722K, capacity 4836K, committed 4992K, reserved 1056768K
|
||||
class space used 527K, capacity 592K, committed 640K, reserved 1048576K
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 4 垃圾回收器
|
||||
|
||||
1. 串行
|
||||
- 单线程
|
||||
- 堆内存较小,适合个人电脑
|
||||
2. 吞吐量优先
|
||||
- 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高
|
||||
3. 响应时间优先
|
||||
- 多线程
|
||||
- 堆内存较大,多核 cpu
|
||||
- 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
|
||||
|
||||
|
||||
|
||||
### 4.1 串行
|
||||
|
||||
`-XX:+UseSerialGC` 通过以下部分组成
|
||||
|
||||
- `Serial`: 工作在新生代,是 `复制` 算法
|
||||
- `SerialOld`: 工作在老年代,是 `标记整理` 算法
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 4.2 吞吐量优先
|
||||
|
||||
`-XX:+UseParallelGC`(JDK1.8 默认开启,使用复制算法,工作在新生代) ~ `-XX:+UseParallelOldGC`(使用标记整理算法,工作在老年代)只要开启了其中一个,另一个就会联动开启,垃圾回收的线程数与CPU核数有关。在垃圾回收时,会瞬间把CPU拉满
|
||||
|
||||
- `-XX:+UseAdaptiveSiizePolicy`:动态调整新生代中伊甸园与幸存区的大小
|
||||
|
||||
- `-XX:GCTimeRatio=ratio` :吞吐量目标 `1/(1+radio)` ,默认 99,1/100=0.01 -> 100分钟内有1分钟的垃圾回收
|
||||
|
||||
- `-XX:MaxGCPauseMillis=ms`:最大暂停毫秒数,默认 200,与上面的 `GCTimeRatio` 互斥,需要折中考虑
|
||||
|
||||
- `-XX:ParallelGCThreads=n`:控制运行时的线程数
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 4.3 响应时间优先
|
||||
|
||||
`-XX:+UseConcMarkSweepGC`(并发标记清除算法,工作在老年代的 CMS垃圾回收器) ~ `-XX:+UseParNewGC`(基于复制算法的垃圾回收器,工作在新生代) ~ `SerialOld` (当 CMS 垃圾回收器并发失败时,会自动退化到该垃圾回收器)只有在 `初始标记` 和 `重新标记` 阶段才会触发 STW
|
||||
|
||||
- `-XX:ParallelGCThreads=n`(并行线程数,与CPU核数有关) ~ `-XX:ConcGCThreads=threads` (并发线程数)
|
||||
|
||||
- `-XX:CMSInitiatingOccupancyFraction=percent`:CMS 垃圾回收触发比例(当老年代内存占用到预定值后进行垃圾回收),值越小,CMS 垃圾回收越频繁
|
||||
- 浮动垃圾:在并发清理的同时会产生新的垃圾,这些垃圾只能等到下一次垃圾清理时才能进行回收,需要预留一些空间
|
||||
|
||||
- `-XX:+CMSScavengeBeforeRemark`:在重新标记阶段,新生代对象可能会引用老年代的对象,这时候去做可达性分析需要扫描整个堆,为了避免这个耗时的操作,可以先对新生代进行一次垃圾回收,然后再进行老年代的可达性分析
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
> **Tip:**
|
||||
>
|
||||
> 在使用 CMS 进行垃圾回收时,会造成内存碎片过多而使得并发失败的问题,这时候会将垃圾回收器退化到 SerialOld,进行一次串行的标记整理后才能恢复 CMS 垃圾回收,这时候垃圾回收的时间会变长。
|
||||
|
||||
|
||||
|
||||
### 4.4 G1
|
||||
|
||||
定义:Garbage First
|
||||
|
||||
- 2004 论文发布
|
||||
- 2009 JDK 6u14 体验
|
||||
- 2012 JDK 7u4 官方支持
|
||||
- 2017 JDK 9 默认
|
||||
|
||||
适用场景
|
||||
|
||||
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
|
||||
- 超大堆内存,会将堆划分为多个大小相等的 `Region`(区)
|
||||
- 整体上是 `标记整理` 算法,两个区域之间是 `复制` 算法
|
||||
|
||||
相关 JVM 参数
|
||||
|
||||
- `-XX:+UseG1GC`:使用 G1 垃圾回收,JDK9 开始才默认开启
|
||||
- `-XX:G1HeapRegionSize=size`:G1 堆分区的大小(需要是2的指数次)
|
||||
- `-XX:MaxGCPauseMillis=time`:暂停目标 默认 200ms
|
||||
|
||||
|
||||
|
||||
#### 1) G1 垃圾回收阶段
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
#### 2) Young Collection
|
||||
|
||||
新生代的垃圾收集
|
||||
|
||||
- 会 STW
|
||||
|
||||
|
||||

|
||||
|
||||
当内存紧张了进行垃圾回收时,使用 `复制算法` 将伊甸园中幸存的对象放置到幸存区中
|
||||
|
||||

|
||||
|
||||
过了一段时间或者内存紧张后进行垃圾回收,会将幸存区中到达年龄的对象会晋升到老年代,而其他对象会被复制到另一个新的幸存区中
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
#### 3) Young Collection + CM
|
||||
|
||||
- 在 Young GC 时会进行 GC Root 的初始标记
|
||||
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定
|
||||
|
||||
`-XX:InitiatingHeapOccupancyPercent=percent`(默认45%)
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
#### 4) Mixed Collection
|
||||
|
||||
会对 E、S、O 进行全面垃圾回收
|
||||
|
||||
- 最终标记(Remark)会 STW
|
||||
- 拷贝存活(Evacuation)会 STW
|
||||
|
||||
`-XX:MaxGCPauseMillis=ms`:最大暂停时间
|
||||
|
||||
G1 垃圾回收器会根据最大暂停时间进行有选择的、回收价值较高的垃圾回收
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
#### 5) Full GC
|
||||
|
||||
- SerialGC
|
||||
- 新生代内存不足发生的垃圾收集 - minor gc
|
||||
- 老年代内存不足发生的垃圾收集 - full gc
|
||||
- ParallelGC
|
||||
- 新生代内存不足发生的垃圾收集 - minor gc
|
||||
- 老年代内存不足发生的垃圾收集 - full gc
|
||||
- CMS
|
||||
- 新生代内存不足发生的垃圾收集 - minor gc
|
||||
- 老年代内存不足
|
||||
- G1
|
||||
- 新生代内存不足发生的垃圾收集 - minor gc
|
||||
- 老年代内存不足(两种情况)
|
||||
- 垃圾产生速度低于垃圾回收速度(有能力进行回收):并发垃圾回收
|
||||
- 垃圾产生速度高于垃圾回收速度(没有能力进行回收):串行垃圾回收,这时才是真正的 full gc
|
||||
|
||||
|
||||
|
||||
#### 6) Young Collection 跨代引用
|
||||
|
||||
- 新生代回收的跨代引用(老年代引用新生代)问题
|
||||
- 将老年代引用新生代的对象标记为 脏卡
|
||||
|
||||
|
||||

|
||||
|
||||
- 卡表与 Remembered Set(新生代对象记录脏卡对象)
|
||||
- 在引用变更时通过 post-write barrier + dirty card queue(写屏障+脏卡队列,通过异步的方式让一个线程来维护)
|
||||
- concurrent refinement threads 更新 Remembered Set
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
#### 7) Remark
|
||||
|
||||
在并发标记阶段,当对象引用改变时,jvm会加入一个写屏障,引用改变,写屏障指令就会被执行,会将该对象加入一个队列中,将对象标记为待处理状态,等到整个并发标记结束,进入重新标记阶段会STW,对象出队列,进行标记检查
|
||||
|
||||
- `pre-write barrier`(预写屏障) + `satb_mark_queue`(混合标记队列)
|
||||
|
||||
|
||||
|
||||
#### 8) JDK 8u20 字符串去重
|
||||
|
||||
- 优点:节省大量内存
|
||||
- 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
|
||||
|
||||
`-XX:+UseStringDeduplication` (默认打开)
|
||||
|
||||
- 将所有新分配的字符串放入一个队列
|
||||
- 当新生代回收时,G1并发检查是否有字符串重复
|
||||
- 如果它们值一样,让它们引用同一个 char[]
|
||||
- 注意,与 String.intern() 不一样
|
||||
- String.intern() 关注的是字符串对象
|
||||
- 而字符串去重关注的是 char[]
|
||||
- 在 JVM 内部,使用了不同的字符串表
|
||||
|
||||
|
||||
|
||||
#### 9) JDK 8u40 并发标记类卸载
|
||||
|
||||
所有对象都经过并发标记后,就能知道哪些类不再被使用,**当一个类加载器的所有类都不再使用,则卸载它所加载的所有类**
|
||||
|
||||
- `-XX:+ClassUnloadingWithConcurrentMark` 默认启用
|
||||
|
||||
|
||||
|
||||
#### 10) JDK 8u60 回收巨型对象
|
||||
|
||||
- 一个对象大于 region 的一半时,称之为巨型对象
|
||||
- G1 不会对巨型对象进行拷贝
|
||||
- 回收时被优先考虑
|
||||
- G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉
|
||||
|
||||
|
||||
|
||||
#### 11) JDK 9 并发标记起始时间的调整
|
||||
|
||||
- 并发标记必须在堆空间占满前完成,否则退化为 FullGC
|
||||
- JDK 9 之前需要使用 `-XX:InitiatingHeapOccupancyPercent`
|
||||
- JDK 9 可以动态调整
|
||||
- `-XX:InitiatingHeapOccupancyPercent` 用来设置初始值
|
||||
- 进行数据采样并动态调整
|
||||
- 总会添加一个安全的空档空间
|
||||
|
||||
|
||||
|
||||
#### 12) JDK 9 更高效的回收
|
||||
|
||||
- 250+增强
|
||||
- 180+bug修复
|
||||
- https://docs.oracle.com/en/java/javase/12/gctuning
|
||||
|
||||
|
||||
|
||||
## 5 调优
|
||||
|
||||
|
||||
|
||||
### 5.1 调优领域
|
||||
|
||||
- 内存
|
||||
- 锁竞争
|
||||
- cpu 占用
|
||||
- io
|
||||
|
||||
|
||||
|
||||
### 5.2 确认目标
|
||||
|
||||
- 【低延迟】还是【高吞吐量】,选择合适的回收器
|
||||
- CMS,G1,ZGC
|
||||
- ParallelGC
|
||||
- Zing(java虚拟机,几乎零延时的垃圾回收)
|
||||
|
||||
|
||||
|
||||
### 5.3 最快的GC
|
||||
|
||||
最快的 GC 是不发生 GC
|
||||
|
||||
- 查看 FullGC 前后的内存占用,考虑下面几个问题
|
||||
- 数据是不是太多?
|
||||
- `resultSet = statement.executeQuery("select * from 大表 limit n")`
|
||||
- 数据表示是否太臃肿?
|
||||
- 对象图
|
||||
- 对象大小占用16字节,Integer对象占用24字节,int类型占用4字节
|
||||
- 是否存在内存泄漏?
|
||||
- `static Map map = ... `,只存不释放,死数据过多
|
||||
- 使用软引用
|
||||
- 使用弱引用
|
||||
- 第三方缓存实现
|
||||
|
||||
|
||||
|
||||
### 5.4 新生代调优
|
||||
|
||||
- 新生代的特点
|
||||
- 所有的 new 操作的内存分配非常廉价
|
||||
- `TLAB:thread-local allocation buffer` :线程局部分配缓冲区,为每一个线程分配专门的伊甸园空间,避免了线程安全问题,加快了对象创建的速度
|
||||
- 死亡对象的回收代价是零
|
||||
- 大部分对象用过即死
|
||||
- Minor GC 的时间远远低于 Full GC
|
||||
|
||||
|
||||
|
||||
- 新生代堆内存分配越大越好?
|
||||
|
||||
新生代堆内存分配过大,会导致老年代的堆内存占用相对变小,使得当老年代内存紧张时,不断触发 Full GC,使得暂停时间变长,影响性能。Oracle 建议新生代内存大小占整个堆的 25%-50% 之间较为合适。
|
||||
|
||||
> **-Xmn**
|
||||
>
|
||||
> Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is performed in this region more often than in other regions. If the size for the young generation is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete. Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size.
|
||||
|
||||
|
||||
|
||||
- 新生代能容纳所有 `并发量 * (请求-响应)` 的数据
|
||||
|
||||
- 幸存区大到能保留 `当前活跃对象+需要晋升对象`
|
||||
|
||||
- 晋升阈值配置得当,让长时间存活对象尽快晋升
|
||||
- `-XX:MaxTenuringThreshold=threshold`:调整晋升阈值
|
||||
- `-XX:+PrintTenuringDistribution`:打印晋升详情信息
|
||||
|
||||
|
||||
|
||||
### 5.5 老年代调优
|
||||
|
||||
以 CMS 为例
|
||||
|
||||
- CMS 的老年代内存越大越好
|
||||
- 先尝试不做调优,如果没有 Full GC 那么已经...,否则先尝试调优新生代
|
||||
- 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
|
||||
- `-XX:CMSInitiatingOccupancyFraction=percent`
|
||||
|
||||
2393
blogs/java/jvm/JVM-字节码技术.md
Normal file
2393
blogs/java/jvm/JVM-字节码技术.md
Normal file
File diff suppressed because it is too large
Load Diff
1184
blogs/java/jvm/JVM-类加载.md
Normal file
1184
blogs/java/jvm/JVM-类加载.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user