Java 8
lambda、Optional和Stream
lambda补充
不仅可以引用参数和待实现的接口抽象方法匹配的静态方法,也可以引用对象的方法
如:两个数比较
调用静态方法
1 | public int compare(Integer o1, Integer o2) { |
也可以使用对象的方法,此时lambda会自动将第一个参数作为载体,调用其对象方法
1 | public int compareTo(Integer anotherInteger) { |
成员方法也可以仅引用,不作为参与载体
1 | public static void main(String[] args) { |
构造方法也可以作为引用传递
1 | public interface Test { |
Optional判空包装
语法
1 | Optional.ofNullable(exp) |
例子
1 | private static void test(String str){ |
1 | Integer i = Optional |
Java 9
Java 9的主要特性:
- 模块机制
- 接口
private方法 - 集合的工厂方法
- StreamAPI改进
模块机制
导入导出
四种类型:
- 系统模块: 来自JDK和JRE的模块(官方提供的模块,比如我们上面用的),我们也可以直接使用
java --list-modules命令来列出所有的模块,不同的模块会导出不同的包供我们使用。 - 应用程序模块: 我们自己写的Java模块项目。
- 自动模块: 可能有些库并不是Java 9以上的模块项目,这种时候就需要做兼容了,默认情况下是直接导出所有的包,可以访问所有其他模块提供的类,不然之前版本的库就用不了了。
- 未命名模块: 我们自己创建的一个Java项目,如果没有创建
module-info.java,那么会按照未命名模块进行处理,未命名模块同样可以访问所有其他模块提供的类,这样我们之前写的Java 8代码才能正常地在Java 9以及之后的版本下运行。不过,由于没有使用Java 9的模块新特性,未命名模块只能默认暴露给其他未命名的模块和自动模块,应用程序模块无法访问这些类(实际上就是传统Java 8以下的编程模式,因为没有模块只需要导包就行)
通过在模块根目录创建module-info.java 我们可以控制导出的包范围,以及导入依赖的包。
如果没有创建
module-info.java,则是无模块模式,默认可以使用其他所有模块提供的类
其中,有两种语法
requires添加依赖模块,默认不会传递transitive将依赖传递到其他导入此模块的模块中
exports导出模块中的包exports ... to ...对指定模块导出包
1 | // module-info.java |

反射控制
模块使得我们可以将模块作为很多包的集合,提供便利的导入和导出功能。
模块机制不仅做了包的分离性,还增加了访问权限的控制。
现在,在没有授权的情况下,你无法通过反射去修改其他包的private字段。
而开放反射权限的语法如下:
open开放整个模块的反射权限opens开放某个包的权限opens to开放某个包的反射权限给某个模块
1 | open module a { // 开放整个模块\ |
服务接口
通过模块的相关语法,可以进行动态加载相关接口的实现,实现一些灵活功能。
- 插件系统:定义一个服务接口,可以让第三方开发者提供实现,在运行时通过
ServicesLoader动态加载,实现插件功能。 - 解耦合架构:模块直接用接口进行通信,不依赖具体实现,可以方便的更换。如日志系统可以定义一个日志接口,接入数据库日志、文件日志等不同的实现。
- 动态加载和扩展:在运行时可以根据需要动态加载需要的功能模块。
语法如下:
uses ...使用某个接口providers interface with implements为某个接口提供实现类
1 | // 接口模块(声明) |
接口的private方法
接口的私有方法只能被 默认实现方法 和 私有方法 调用。
私有方法必须具有默认实现。
私有方法可以将一些共享逻辑封装,供多个默认实现使用。
1 | public interface Test { |
集合类新增工厂方法
以前创建Map需要在创建对象后手动初始化
1 | Map<String, Integer> map = new HashMap<>(); //要快速使用Map,需要先创建一个Map对象,然后再添加数据 |
Java 9 之后,可以用 of 方法快速创建了
1 | Map<String, Integer> map = Map.of("AAA", 18, "BBB", 20); //直接一句搞定 |
不过该方法只适用于 0-10对 键值对,因为只重载了这么多
默认创建的对象是 只读 的
其他的集合类也有of方法
1 | Set<String> set = Set.of("BBB", "CCC", "AAA"); //注意Set中元素顺序并不一定你的添加顺序 |
Stream API改进
Java 9 对Stream API进行了改进
1 | public static void main(String[] args) { |
其他
try-with-resource可以使用现有变量了
1 | public static void main(String[] args) throws IOException { |
Optional包装可以同时处理两种情况了,使用ifPresentOrElse
1 | public static void main(String[] args) { |
支持用 or() 替换为另一个Optional类
如果被包装的是null 返回Supplier提供的另一个Optional类
1 | Optional.ofNullable(null) |
Java 10
局部变量类型推断
可以使用 var 关键字对局部变量类型推断进行类型推断了
注意:必须是具有初始值的变量才可以进行推断
1 | String str = "你好!" // 以前的语法 |
推断只发生在编译期,编译完成后会确定具体的类型。
Java 11
作为继Java 8之后的LTS版本,Java 11带来了
- Lambda的改进
- String类的新方法
- 全新HttpClient
Lambda形参类型推断
1 | Consumer<String> c = (String str) -> { |
String类新增方法
| 方法 | 描述 |
|---|---|
| isBlank(); | 判断是否为空/全为空格 |
| lines(); | 通过换行符切割,转为Stream |
| repeat(num); | 返回重复几次的String |
| strip()、stripLeading()、stripTrailing() | 去除首尾空格 |
全新HttpClient
1 | public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException { |
Java 12-16
主要是一些实验性功能,在Java 16被正式引入
- Switch表达式
- 文本块
- Instanceof模式匹配
- 记录类型
Switch表达式(Java 14)
使用类lambda作为case的函数体,同时可以将多个case匹配值写入一个case中
函数体需要使用 yield 返回
使用这种语法就不需要显式 break
语法:
1 | var res = switch (obj) { //这里和之前的switch语句是一样的,但是注意这样的switch是有返回值的,所以可以被变量接收 |
例子:
1 | public static String grade(int score){ |
文本块(Java 15)
多行字符串可以使用"""包围了,类似python
1 | var str = """ |
Instanceof模式匹配(Java 16)
以前在一些情境下,在类型转换之前需要通过instanceof检查一下类型
1 |
|
现在可以直接转换成模式变量使用
1 |
|
Record记录类型(Java 16)
记录类型本质上也是一个普通的类,不过是final类型(即只读)且继承自java.lang.Record抽象类的,它会在编译时,会自动编译出以下方法‘
public gethashcodeequalstoString
1 | public record Account(String username, String password) { //直接把字段写在括号中 |
Java 17
不介绍预览特性
密封类型
目的:限制类的继承
在之前的版本,可以用final关键字禁止对类的继承。
1 | public final class A {} |
在Java 17之后可以用密封类实现禁止别人继承的同时,自己可以继承
使用sealed关键字标识密封类,permits标识允许继承的类
1 | public sealed class A permits B {} // 允许B继承 |
密封类型有以下要求:
- 可以基于普通类、抽象类、接口,也可以是继承自其他接抽象类的子类或是实现其他接口的类等。
- 必须有子类继承,且不能是匿名内部类或是lambda的形式。
sealed写在原来final的位置,但是不能和final、non-sealed关键字同时出现,只能选择其一。- 继承的子类必须显式标记为
final、sealed或是non-sealed类型。
标准的声明格式如下:
1 | public sealed [abstract] [class/interface] 类名 [extends 父类] [implements 接口, ...] permits [子类, ...]{ |
子类的格式为:
1 | public [final/sealed/non-sealed] class 子类 extends 父类 { //必须继承自父类 |
Java 21
仅介绍正式特性
有序集合
在之前,我们使用不同的集合类,获取元素的API名称非常不一致。
它新增了 SequencedCollection,SequencedSet,SequencedMap 三个接口,使得 Java 中的有序集合类可以按照统一的方法来进行操作。
SequencedCollection 接口定义定义了如下方法:
addFirst():将元素添加为此集合的第一个元素。addLast():将元素添加为此集合的最后一个元素。getFirst():获取此集合的第一个元素。getLast():获取此集合的最后一个元素。removeFirst():移除并返回此集合的第一个元素。removeLast():移除并返回此集合的最后一个元素。reversed():反转此集合。
SequencedMap 接口定义了如下方法:
firstEntry():返回此 Map 中的第一个 Entry,如果为空,返回 null。lastEntry():返回此 Map 中的最后一个 Entry,如果为空,返回 null。pollFirstEntry():移除并返回此 Map 中的第一个 Entry。pollLastEntry():移除并返回此 Map 中的最后一个 Entry。putFirst():将 key-value 插入此 Map 中,如果该 key 已存在则会替换。注意,此操作完成后,该 key-value 就已经存在于此 Map 中,并且是第一位。putLast():将 key-value 插入此 Map 中,如果该 key 已存在则会替换。注意,此操作完成后,该 key-value 就已经存在于此 Map 中,并且是最后一位。reversed():反转此Map。sequencedEntrySet():返回此 Map 的 Entry。sequencedKeySet():返回此 Map 的keySet的SequencedSet视图。sequencedValues():返回此 Map 的 value集合的SequencedCollection视图。
Switch模式匹配
在Java 16引入了Instaceof模式匹配,这次引入了Switch模式匹配,这样就可以直接在 switch 语句中检查和转换类型,而不需要额外的 if...else 结构和显式类型转换。
switch 模式匹配支持以下几种模式:
- 类型模式
- 空模式
- 守卫模式
- 常量模式
类型模式
1 | Object[] list = {"字符串", 123, 456.0}; |
空模式
现在向switch传递一个null值不会抛出异常了
1 | switch (null) { |
守卫模式
如果在匹配后要进行判断,我们一般会用代码块写
1 | case String str -> { |
现在可以追加一个守卫条件,写法更简洁
1 | case String str && str.length() > 5 ->System.out.println("str大于5"); |
Record模式匹配
在Java 16,引入了Record类,声明一个小型不可变的数据载体。
也引入了Instanceof模式匹配,解决了类型转换问题。
这次引入的Record模式匹配(Record Patterns),它允许我们通过模式匹配直接提取组件,而不需要先进行强制类型转换后再提取。它需要与 instanceof模式匹配 或 switch 模式匹配一同使用。
Instanceof模式匹配
和之前的用法一样,在判断类型后自动进行类型转换,可以很方便的提取字段。
1 | if (obj instanceof User user) { |
Switch模式匹配
在Switch模式匹配中,不仅可以自动转换类型,还可以自动提取字段
1 | switch (obj) { |
虚拟线程
概念
很多语言都有类似于“虚拟线程”的技术,比如Go、C#、Erlang、Lua等,他们称之为“协程”。
JVM为我们提供了java.lang.Thread,是对操作系统线程的抽象。但是它具有如下缺点:
- 代价昂贵:创建平台线程的成本很高。每当创建一个平台线程时,操作系统都必须在堆栈中分配大量内存来存储线程的上下文、原生调用堆栈和 Java 调用堆栈。由于堆栈大小是固定的,这就导致了高昂的内存开销。
- 上下文切换成本高:在多线程环境下,需要在不同线程间切换,这种上下文切换会消耗时间和资源。
- 线程数量有限:Java 线程仅仅只是对操作系统线程的封装,而操作系统线程的数量是有限的,这就限制了 Java 同时运行的线程数量,从而限制了应用程序的并发能力。
虚拟线程是 Java 中的一种轻量级线程,它旨在解决传统线程模型中的一些限制,提供了更高效的并发处理能力,允许创建数千甚至数万个虚拟线程,而无需占用大量操作系统资源。

虚拟线程与传统线程具有如下几个区别:
轻量级:
- 操作系统线程:每个线程由操作系统管理,具有较高的创建和上下文切换成本。操作系统线程消耗大量内存,因为每个线程都有独立的堆栈空间和内核资源。
- 虚拟线程:由 JVM 管理,更轻量级。虚拟线程的创建和销毁成本很低,消耗的内存也较少,因为它们共享底层的操作系统线程池,避免了高昂的上下文切换成本。
调度:
- 操作系统线程:由操作系统的调度器管理,操作系统线程数量一多,调度开销会显著增加。
- 虚拟线程:由 JVM 内部的调度器管理,可以更有效地利用 CPU 资源。虚拟线程可以实现比操作系统线程更高效的调度策略。
阻塞与非阻塞操作:
- 操作系统线程:在阻塞操作(如 I/O)时会导致整个线程被阻塞,无法处理其他任务。
- 虚拟线程:虚拟线程的阻塞操作不会阻塞底层的操作系统线程。JVM 可以通过协程的方式将虚拟线程的阻塞操作挂起,并在资源可用时恢复执行,这样可以实现高并发而不增加系统负担。
优势:
- 更高的并发性: 虚拟线程允许数百万级别的并发线程,这是操作系统线程无法轻易实现的。高并发应用程序(如服务器和大规模数据处理应用)可以从中受益。
- 简化编程模型: 虚拟线程使编写并发程序变得更简单。开发者可以用直观的阻塞代码风格来编写逻辑,而不必使用复杂的回调和非阻塞编程技术。
- 资源利用率优化: 虚拟线程可以更好地利用系统资源。通过避免大量操作系统线程的开销,虚拟线程可以实现更高效的资源使用和性能优化。
虽然虚拟线程这么厉害,但是它不做以下几件事:
- 不替代传统线程:虚拟线程并不旨在完全替代传统的操作系统线程,而是作为一个补充。对于需要密集计算和精细控制线程行为的场景,传统线程仍然是主流。
- 非针对最低延迟:虚拟线程主要针对高并发和高吞吐量,而不是最低延迟。对于需要极低延迟的应用,传统线程可能是更好的选择。
- 不改变基本的线程模型:虚拟线程改进了线程的实现方式,但并未改变Java基本的线程模型和同步机制。锁和同步仍然是并发控制的重要工具。
使用
创建虚拟线程的方法也封装在java.lang.Thead内。
最简单的办法就是使用Thread.startVirtualThread(Runnable),创建一个虚拟线程立刻执行Runnable
1 | Thread.startVirtualThread(() -> { |
也可以使用Builder API来创建,方法名为ofVirtual(),可以设置(名称、守护状态、优先级、未捕获异常处理器)等相关属性
1 | Thread.ofVirtual() |
线程池
可以使用Executors.newVirtualThreadPerTaskExecutor() 创建一个线程池,该线程池会给每个任务分配一个虚拟线程。
1 | ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); |
使用submit()提交任务,可以是Runnable和Callable
1 | executor.submit(() -> { |
可以使用shutdown() 来关闭线程池,它会等待正在执行的任务完成,但不会接受新的任务。如果需要立即停止所有任务,可以使用 shutdownNow()。