入职新公司两个月了,用JDK11写了两个月JDK8的代码,再也憋不住想看看JDK11的新特性了。没用过JDK9,10. 那就说说JDK8-JDK11的变化吧。
新
- 语法增强
- 本地变量类型推断
Collection增强Stream增强Optional增强InputStream增强String增强
- 模块化开发
- 新工具
REPL交互式编程Low-Overhead Heap ProfilingFlight Recorder
- 新功能
- 源代码直接执行
- 完全支持
Linux容器 - 支持
Unicode 10 - 新支持的加密算法
HttpClient
- 垃圾回收器
ZGCEpsilon- 更好的
G1
- 移除与不再推荐使用的类库或功能
- 移除了
Java EE和CORBA Moudles - 将
Nashorn Javascript标记为不推荐 - 将
Pack200 Tools and API标记为不推荐
- 移除了
JDK8-JDK11的语法新特性
本地变量类型推断
since JDK10
Java10以后可以用var定义一个局部变量,不用显式写出它的类型。但要注意,被var定义的变量仍然是静态类型,编译器会试图去推断其类型。所以,我们需要注意 1.不兼容的类型是不能重新赋值的! 2.只要编译器无法推断出变量类型,就会编译错误!
举个栗子:
基本使用
public static void test1() {
var str1 = "local variable type interface";
String str2 = "local variable type interface";
System.out.println(str1 == str2); //true
}
简化泛型声明
/**
* 简化泛型声明
*/
public static void test2() {
var lists = new ArrayList<Map<String, List<String>>>();
for (var item : lists) {
var entries = item.entrySet();
for (var entry : entries) {
String key = entry.getKey();
var values = entry.getValue();
for (var value : values) {
System.out.println(value);
}
}
}
}
幸好编译器会有类型提示。如下图。

简化lambda参数
/**
* lambda参数
* 从Java 11开始,lambda参数也允许使用var关键字:
*/
public static void test3() {
Predicate<String> predNotNull = (var a) -> a != null && !a.isBlank();
String strAfterFilter = Stream.of("I", "", "love", " ", "my", "wife", null, "very", "much")
.filter(predNotNull)
.collect(Collectors.joining(" "));
System.out.println(strAfterFilter); //I love my wife very much
}
不兼容的类型赋值
这种直接编译失败, 第一行和第二行已经推断出来了 str 是 String 类型。 第三行就不能赋值为double 类型了。

不能推测的类型

但是,对于用习惯了 IDEA快捷指令的我来说,这个var变量,对我来说毫无意义。感觉jdk是在炫技,但我毫无兴趣。
比如,在idea中,你输入 new ArrayList().var ,然后回车,IDEA就会为你自动生成类型。
类似的还有 lists.for, lists.fori 等等. 所以,个人并不推荐大家使用该语法,并非不能用,比如在 lambda 中使用替换类型时还是比较好的。如果你还没有掌握 xx.var 这种异能加持的话,想用就用吧。
总之,看别人写了个 var 时,不要发出 “哇藕,Java 还可以这么写”。
Collection增强
这个比较简单,就是 List , Set, Map 这三种集合多了两个方法 of 和 copyOf.
since jdk9
这里直接用三个栗子分别演示一下
List
List.of 内部是创建一个的 immutable collections。不可变集合。所以不可以增删改元素。
List.of() 和 List.copyOf() 都是创建的不可变集合
/**
* 演示 List
*/
public static void test1() {
List<Integer> integers = List.of(1, 2, 3, 4);
List<Integer> integers1 = List.copyOf(integers);
System.out.println(integers == integers1); // true
integers.add(5); // UnsupportedOperationException
}
Set
Set 和 List 的用法类似。 同样的,也是不可变集合。需要注意的是,copyOf方法,如果形参是可变集合,则返回的也是可变集合。
/**
* 演示 Set
*/
public static void test2() {
Set<Integer> integers = Set.of(1, 2, 3, 4);
Set<Integer> integers1 = Set.copyOf(integers);
System.out.println(integers == integers1); // true
// integers.add(5); // UnsupportedOperationException
Set<Integer> integers2 = new HashSet<>();
integers2.add(1);
integers2.add(2);
integers2.add(3);
Set<Integer> integers3 = Set.copyOf(integers2);
System.out.println(integers3 == integers2); // false
// 元素不能重复 => java.lang.IllegalArgumentException
Set<Integer> integers4 = Set.of(1, 2, 3, 4,3);
}
Map
/**
* 演示Map
*/
public static void test3(){
// 使用of创建Map
Map<String, Integer> map1 = Map.of("k1", 2, "k2", 2);
Map<String, Integer> map2 = Map.copyOf(map1);
System.out.println(map1 == map2); // true
// copyOf,取决于传入的map是否为可变集合
HashMap<String, Integer> map3 = new HashMap<>();
map3.put("k1",1);
map3.put("k2",2);
Map<String, Integer> map4 = Map.copyOf(map3);
System.out.println(map3 == map4); //false
// key不能重复 => java.lang.IllegalArgumentExceptio
Map<String, Integer> map5 = Map.of("k1", 2, "k2", 2, "k1", 3);
}
Stream增强
since JDK9
Stream相关内容,新增了4个方法.
增加单个参数构造方法
/**
* 新增单个元素的Stream构造,允许为空
*/
public static void demo1() {
System.out.println(Stream.ofNullable(null).count()); // 0
System.out.println(Stream.of(1).count()); //1
}
增加takeWhile方法
/**
* 演示take while方法
* <p>
* 从头开始计算,遇到不满足Predicate的元素即停止。
*/
public static void demo2() {
List<Integer> collect1 = Stream.of(1, 2, 3, 4, 5, 3, 2, 1).takeWhile(i -> i <= 3).collect(Collectors.toList());
System.out.println(collect1); // [1, 2, 3]
List<Integer> collect2 = Stream.of(1, 2, 3, 4, 5, 4, 3, 2, 1).takeWhile(i -> i > 6).collect(Collectors.toList());
System.out.println(collect2); // [ ]
}
增加 dropWhile 方法
/**
* 演示 dropWhile 方法
* 从头开始计算,遇到第一个不满足 Predicte的元素时,开始计算。
*/
public static void demo3() {
List<Integer> collect1 = Stream.of(1, 2, 3, 4, 5, 4, 3, 2, 1).dropWhile(i -> i <= 3).collect(Collectors.toList());
System.out.println(collect1); // [1, 2, 3, 4, 5, 4, 3, 2, 1]
List<Integer> collect2 = Stream.of(1, 2, 3, 4, 5, 4, 3, 2, 1).dropWhile(i -> i > 3).collect(Collectors.toList());
System.out.println(collect2); // [4, 5, 4, 3, 2, 1]
}
iterate 重载方法
这个 iterate 方法的新重载方法,可以让你提供一个 Predicate (判断条件)来指定什么时候结束迭代。
需要注意的是:相比JDK8的方法,iterator是第三个参数,第二个参数是Prediction。Prediction中进行判断的取值是迭代之后的数值。
/**
* 演示 iterate 的重载方法
*/
public static void demo4() {
// Jdk8支持使用这种方式, 生成从1开始的平方数
List<Integer> collect1 = Stream.iterate(1, i -> i * 2).limit(5).collect(Collectors.toList());
System.out.println(collect1); // [1, 2, 4, 8, 16]
// JDK9 支持,加入一个 Prediction, 判断如何终止。
List<Integer> collect2 = Stream.iterate(1, i -> i <= 16, i -> i * 2).limit(10).collect(Collectors.toList());
System.out.println(collect2); // [1, 2, 4, 8, 16]
}
Optional 增强
Since JDK 9
/**
* 提供一个默认的Optional对象
*/
private static void demo1() {
// 定义一个变量, 可能为空 or not
var variable = "jdk 11";
// jdk 8 支持of(),ofNullable() 转换为Optional对象。
List<String> collect = Optional.of(variable).stream().collect(Collectors.toList());
// JDK 8 可以通过 orElse, 当 variable 为空的时候,返回一个默认的字符串值。
Optional.ofNullable(variable).orElse("");
// jdk 9 提供了提供 or() 返回一个默认Optional对象。
Optional.empty().or(() -> Optional.of("jdk 11")).get();
}
InputStream 加强
InputStream 终于有了一个非常有用的方法:transferTo,可以用来将数据直接传输到 OutputStream,这是在处理原始数据流时非常常见的一种用法。
还是举个栗子吧:
/**
* 直接将数据写入输出流
*/
public static void demo1() throws IOException {
var classLoader = ClassLoader.getSystemClassLoader();
var inputStream = classLoader.getResourceAsStream("readFile.txt");
var javastack = File.createTempFile("writeFile", "txt");
try (var outputStream = new FileOutputStream(javastack)) {
inputStream.transferTo(outputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
String增强
isBlank
判断目标字符串是否是空白字符。
使用功能上的问题,感觉还是例子来的舒服。
/**
* 判断字符串是否为空
*/
private static void demo1() {
// 半角空格 ===> true
System.out.println(" ".isBlank());
// 全角空格 ===> true
System.out.println(" ".isBlank());
// 半角空格的unicode字符值 ===> true
System.out.println("\u0020".isBlank());
// 全角空格的unicode字符值 ===> true
System.out.println("\u3000".isBlank());
// 制表符 ===> true
System.out.println("\t".isBlank());
// 回车 ===> true
System.out.println("\r".isBlank());
// 换行 ===> true
System.out.println("\n".isBlank());
// 各种空白字符拼接 ===> true
System.out.println(" \t\r\n ".isBlank());
}
strip,stripLeading与stripTrailing
去除字符串的前后字符串
// 全角空格 + 制表符 + 回车 + 换行 + 半角空格 + <内容> + 全角空格 + 制表符 + 回车 + 换行 + 半角空格
var strTest = " \t\r\n 你好 jdk11 \t\r\n ";
// strip 去除两边空白字符
System.out.println("[" + strTest.strip() + "]");
// stripLeading 去除开头的空白字符
System.out.println("[" + strTest.stripLeading() + "]");
// stripTrailing 去除结尾的空白字符
System.out.println("[" + strTest.stripTrailing() + "]");
repeat
var strOri = "jdk11";
var str1 = strOri.repeat(1);
var str2 = strOri.repeat(3);
System.out.println(str1);
System.out.println(str2);
// repeat传入参数为1时,不会创建一个新的String对象,而是直接返回原来的String对象。
System.out.println(str1 == strOri);
lines
/**
* 按照换行符拆分字符串
*/
private static void demo3() {
var strContent = "hello java\rhello jdk11\nhello world\r\nhello everyone";
// lines方法用 \r 或 \n 或 \r\n 对字符串切割并返回stream对象
strContent.lines().forEach(System.out::println);
System.out.println(strContent.lines().count());
}
模块化开发
Java9引入了模块化,Java Platform Module System,java平台模块系统,简称JPMS。
这里和大家一起做一下。
新建两个 module
我们新建两个模块 core 和 business。 如下图

core 模块配置
- 新建一个类 RestResult ,表示公共调用的类。
public class RestResult<T> {
private T data;
private Integer code;
private String errMsg;
public RestResult(T data) {
this.data = data;
}
}
- 新建一个
module-info.java. 声明module信息.exports将com.fxb.learn.module.core这个包下的类都export出去。- 如果是一个
java的普通应用,则在src目录下,新建module-info.java文件即可。 - 如果是一个
maven应用,则需要在src/main/java目录下,新建module-info.java文件。
- 如果是一个
// core 是module的名称
module core {
// 将指定的包下类,export出去
exports com.fxb.learn.module.core;
}
business 模块配置
- 在
business模块下,新建一个module-info.java. 文件位置上core模块中的一致。
module business {
requires core;
}
- 在
Idea中配置,business模块引用core模块。

写个例子看看
/**
* 演示模块调用。
*/
private RestResult<User> getUserById() {
return new RestResult<>(new User(1, "fangjiaxiaobai"));
}
新工具
JDK还提供了一些新的工具, REPL交互式编程,Low-Overhead Heap Profiling(免费的低耗能飞行记录仪和堆分析仪),Flight Recorder(黑盒子)
REPL交互式编程
你是否使用 jupter, java 也有了!
Java提供了一个新的工具jshell,Java终于可以像python,scala等语言那样,交互式演示语法了

具体命令可以使用 /help 命令查看。
Low-Overhead Heap Profiling
免费的低耗能飞行记录仪和堆分析仪。
通过JVMTI的SampledObjectAlloc回调提供了一个开销低的heap分析方式提供一个低开销的,为了排错java应用问题,以及JVM问题的数据收集框架。
具有一下功能:
- 提供用于生产和消费数据作为事件的
API - 提供缓存机制和二进制数据格式
- 允许事件配置和事件过滤
- 提供
OS,JVM和JDK库的事件
Flight Recorder
Flight Recorder 源自飞机的黑盒子。 Flight Recorder 以前是商业版的特性,在java11当中开源出来,它可以导出事件到文件中,之后可以用Java Mission Control 来分析。
两种启动方式:
可以在应用启动时配置java -XX:StartFlightRecording
应用启动之后,使用jcmd来录制,如下代码:
$ jcmd <pid> JFR.start # 启动记录仪
$ jcmd <pid> JFR.dump.filename=recording.jfr # 将记录内容保存到文件里
$ jcmd <pid> JFR.stop # 停止记录仪
不过在 jdk11 是没办法查看 jfr 的。如果想看,安装 jdk12 吧。 不,可以试试 jdk16 , jdk16 也是LTS 版本!·
JFR是一套集成进入JDK、JVM内部的事件机制框架,通过良好架构和设计的框架,硬件层面的极致优化,生产环境的广泛验证,它可以做到极致的可靠和低开销。在SPECjbb2015等基准测试中,JFR的性能开销最大不超过1%,所以,工程师可以基本没有心理负担地在大规模分布式的生产系统使用,这意味着,我们既可以随时主动开启JFR进行特定诊断,也可以让系统长期运行JFR,用以在复杂环境中进行”After-the-fact”分析。还需要苦恼重现随机问题吗? JFR让问题简化了很多 在保证低开销的基础上, JFR提供的能力也令人眼前一亮,例如:我们无需BCI就可以进行Object Allocation Profiling, 终于不用担心BTrace 之类把进程搞挂了。对锁竞争、阻塞、延迟,JVM GC、SafePoint 等领域,进行非常细粒度分析。甚至深入JIT Compiler 内部,全面把握热点方法、内联、逆优化等等。JFR提供了标准的Java,C++ 等扩展API,可以与各种层面的应用进行定制、集成,为复杂的企业应用栈或者复杂的分布式应用,提供All-in-One 解决方案。而这一切都是内建在JDK和JVM内部的,并不需要额外的依赖,开箱即用。
新功能
HttpClient
JDK 9开始引入HttpClient API来处理HTTP请求。 从JDK 11开始,这个·正式进入标准库包。
参考网址:http://openjdk.java.net/groups/net/httpclient/intro.html
HttpClient具有以下特性:
- 支持
HTTP1.1和HTTP2,websocket协议 - 支持同步和异步编程模型
- 将请求和响应主体作为响应式流(
reactive-streams)处理,并使用构建器模式 - 要发送
http请求,首先要使用其构建器创建一个HttpClient。这个构建器能够配置每个客户端的状态:- 首选协议版本 (
HTTP/1.1或HTTP/2) - 是否跟随重定向
- 代理
- 身份验证
- 首选协议版本 (
一旦构建完成,就可以使用HttpClient发送多个请求。
HttpRequest
HttpRequest是由它的构建器创建的。请求的构建器可用于设置:
- 请求
URI - 请求
Method(GET,PUT,POST) - 请求主体(如果有)
- 超时时间
- 请求头
HttpRequest构建之后是不可变的,但可以发送多次。
Synchronous or Asynchronous
请求既可以同步发送,也可以异步发送。当然同步的API会导致线程阻塞直到HttpResponse可用。异步API立即返回一个CompletableFuture,当HttpResponse可用时,它将获取HttpResponse并执行后续处理。
Data as reactive-streams
请求和响应的主体作为响应式流(具有非阻塞背压的异步数据流)供外部使用。HttpClient实际上是请求正文的订阅者和响应正文字节的发布者。BodyHandler接口允许在接收实际响应体之前检查响应代码和报头,并负责创建响应BodySubscriber。
HttpRequest和HttpResponse类型提供了许多便利的工厂方法,用于创建请求发布者和响应订阅者,以处理常见的主体类型,如文件、字符串和字节。这些便利的实现要么累积数据,直到可以创建更高级别的Java类型(如String),要么就文件流传输数据。BodySubscriber和BodyPublisher接口可以实现为自定义反应流处理数据。
HttpRequest和HttpResponse还提供了转换器,用于将 java.util.concurrent.Flow 的 Publisher/Subscriber 类型转换为 HTTP Client 的 BodyPublisher/BodySubscriber 类型。
请求协议 HTTP/2
Java HTTP Client支持 HTTP/1.1 和 HTTP/2。默认情况下,客户端将使用 HTTP/2 发送请求。发送到尚不支持 HTTP/2 的服务器的请求将自动降级为 HTTP/1.1。
以下是HTTP/2带来的主要改进:
- 标头压缩。 HTTP/2 使用 HPACK 压缩,从而减少了开销。
- 与服务器的单一连接减少了建立多个TCP连接所需的往返次数。
- 多路复用。 在同一连接上,同时允许多个请求。
- 服务器推送。 可以将其他将来需要的资源发送给客户端。
- 二进制格式。 更紧凑。
由于HTTP/2是默认的首选协议,并且在需要的地方无缝地实现回退到HTTP/1.1,那么当HTTP/2被更广泛地部署时,Java HTTP客户端就无需修正它的应用代码。
具体的Java Doc 可以参考: https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/package-summary.html
看了 Java Doc, 感觉使用起来比较简单。这里就不举例了。感兴趣的朋友,可以自行深入研究一下。
源代码直接执行
一个单文件源代码,也就是说,单独的java文件,有main方法,且只依赖jdk类库以及自己文件内部定义的类,可以直接用java执行而无需先编译再执行编译后的class文件了。
你可能问了,有什么用呢? 我平时也不关系它是否生成了class文件。
如果你是做数据相关的工作,可能需要写一些脚本的时候,这却是轻松了很多。
➜ learn git:(master) ✗ ll
total 8
-rw-r--r-- 1 wangxiyue staff 411B Nov 9 22:58 SourceCodeExecDemo.java
➜ learn git:(master) ✗ java SourceCodeExecDemo.java
1.解析数据格式.....
2.处理数据.....
3.重组数据.....
➜ learn git:(master) ✗ ll
total 8
-rw-r--r-- 1 wangxiyue staff 411B Nov 9 22:58 SourceCodeExecDemo.java
可以看到确实没有 class 文件生成。
完全支持Linux容器
在Docker容器中运行Java应用程序一直存在一个问题,那就是在容器中运行的JVM程序在设置内存大小和CPU使用率后,会导致应用程序的性能下降。这是因为Java应用程序没有意识到它正在容器中运行。随着Java10的发布,这个问题总算得以解诀,JVM现在可以识别由容器控制组(cgroups) 设置的约束,可以在容器中使用内存和CPU约束来直接管理Java应用程序,其中包括:
- 遵守容器中设置的内存限制
- 在容器中设置可用的
CPU - 在容器中设置
CPU约束
支持Unicode 10
Unicode 10新增了8518个字符,总计达到了136690个字符。包括56个新的emoji表情符号。
JDK11在java.lang下增加了4个类来处理:
CharacterData00.classCharacterData01.classCharacterData02.classCharacterData0E.class
新支持的加密算法
Java实现了RFC7539中指定的ChaCha20和Poly1305两种加密算法,代替RC4。
RFC7748定义的密钥协商方案更高效,更安全,JDK增加了两个新的接口XECPublicKey和XECPrivateKey。
垃圾回收器
ZGC
启用方法:-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
说明:ZGC, A Scalable Low-Latency Garbage collector( Experimental) ,一个可伸缩的低延时的垃圾回收器。GC暂停时间不会超过10ms,既能处理几百兆的小堆,也能处理几个T的大堆。和G1相比,应用吞吐能力不会下降超过15%,为未来的GC功能和利用colord指针以及Load barriers 优化奠定了基础。初始只支持64位系统。
ZGC的设计目标是:支持TB级内存容量,暂停时间低(<10ms),对整个程序吞吐量的影响小于15%。将来还可以扩 展实现机制,以支持不少令人兴奋的功能,例如多层堆(即热对象置于DRAM和冷对象置于NVMe闪存),或压缩堆。
GC是java主要优势之一。然而,当GC停顿太长,就会开始影响应用的响应时间。消除或者减少GC停顿时长,java将有可能在更广泛的应用场景中成长为一个更有吸引力的平台。此外,现代系统中可用内存不断增长,用户和程序员希望JVM能够以高效的方式充分利用这些内存,并且无需长时间的GC暂停时间。
ZGC是一个并发,基于region,压缩型的垃圾收集器,只有root扫描阶段会STW,因此GC停顿时间不会随着堆的增长和存活对象的增长而变长。
Epsilon
实验性质,生产环境不建议使用。
启用方法:-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
说明:开发一个处理内存分配但不实现任何实际内存回收机制的GC,一旦可用堆内存用完,JVM就会退出,如果有System.gc()调用,实际上什么也不会发生(这种场景下和-XX:+DisableExplicitGC效果一样), 因为没有内存回收,这个实现可能会警告用户尝试强制GC是徒劳的。
主要用途如下:
- 性能测试(它可以帮助过滤掉
GC引起的性能假象) - 内存压力测试(例如,知道测试用例应该分配不超过
1GB的内存,我们可以使用-Xmx1g -XX:+UseEpsilonGC,如果程序有问题,则程序会崩溃。 - 非常短的
JOB任务(对于这种任务,GC是在浪费资源) VM接口测试Last-drop延迟&吞吐改进
更好的G1
对于G1 GC,相比于JDK8,升级到JDK 11即可免费享受到:并行的Full GC,快速的CardTable扫描,自适应的堆占用比例调整(IHOP),在并发标记阶段的类型卸载等等。这些都是针对G1的不断增强,其中串行FullGC等甚至是曾经被广泛诟病的短板,你会发现GC配置和调优在JDK11中越来越方便。
移除与不再推荐使用的类库或功能
Jdk9到Jdk11,陆续移除了一些类库或功能。
移除了Java EE和CORBA Moudles
在java11中移除了不太使用的JavaEE模块和CORBA技术。
CORBA来自于二十世纪九十年代,Oracle认为,现在用CORBA开发现代Java应用程序已经没有意义了,维护CORBA的成本已经超过了保留它带来的好处。
但是删除CORBA将使得那些依赖于JDK提供部分CORBAAPI的CORBA实现无法运行。目前还没有第三方CORBA版本,也不确定是否会有第三方愿意接手CORBA API的维护工作。
在java11中将java9标记废弃的Java EE及CORBA模块移除掉,具体如下:
xml 相关被移除的:
java.xml.wsjava.xml.bindjava.xml.wsjava.xml.ws.annotationjdk.xml.bindjdk.xml.ws
只剩下java.xml, java.xml.crypto.jdk.xml.dom 这几个模块。
其它被移除的Java EE和CORBA相关类库:
java.corbajava.se.eejava.activationjava.transaction(但是java11新增了一个java.transaction.xa模块)
其他移除的类库
com.sun.awt.AWTUtilitiessun.miss.Unsafe.defineClassThread.destroy()以及Thread.stop(Throwable)方法sun.nio.ch.disableSystemWideOverlappingFileLockCheck属性sun.locale.formatasdefault属性jdk snmp模块javafxjava Mission ControlRoot Certificates: 一些根证书被移除:Baltimore Cybertrust Code Signing CA, SECOM Root Certificate, AOL and Swisscom Root Certificates
其中,使用java.lang.invoke.MethodHandles.Lookup.defineClass来替代移除的sun.miss.Unsafe.defineClass。
将Nashorn Javascript标记为不推荐
将Javascript引擎标记为Deprecate,后续版本会移除,有需要的可以考虑使用开源的GraalVM。
将Pack200 Tools and API标记为不推荐
java11中将pack200以及unpack200工具以及java.tiljar中的Pack200 API标记为Deprecate。因为Pack200主要是用来压缩jar包的工具,由于网络下载速度的提升以及java9引入模块化系统之后不再依赖Pack200,因此这个版本将其标记为Deprecate。
预告
继LTS JDK8之后,又一LTS,你会用吗? JDK16,它来了。
对了
JDK11 写 JDK8 的代码?
代码始终是代码,写的再多,写不懂你我。
多看一点,就比其他们多懂一点。所以,你关不关注我,问题不大!
人情世故。不是世故,就是事故。问题真的不大。
文中所有代码, 在 https://gitee.com/fangjiaxiaobai/learn_java/tree/master/fxb_jdk11
最后
希望和你一起遇见更好的自己
