Java 8

lambda、Optional和Stream

lambda补充

不仅可以引用参数和待实现的接口抽象方法匹配的静态方法,也可以引用对象的方法

如:两个数比较

调用静态方法

1
2
3
4
5
6
7
8
9
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}

public static void main(String[] args) {
Integer[] array = new Integer[]{4, 6, 1, 9, 2, 0, 3, 7, 8, 5};
Arrays.sort(array, Integer::compare); //需要两个Integer参数
System.out.println(Arrays.toString(array));
}

也可以使用对象的方法,此时lambda会自动将第一个参数作为载体,调用其对象方法

1
2
3
4
5
6
7
8
9
10
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
// Integer类的成员方法

public static void main(String[] args) {
Integer[] array = new Integer[]{4, 6, 1, 9, 2, 0, 3, 7, 8, 5};
Arrays.sort(array, Integer::compareTo); //注意这里调用的不是静态方法
System.out.println(Arrays.toString(array));
}

成员方法也可以仅引用,不作为参与载体

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
Main mainObject = new Main();
Integer[] array = new Integer[]{4, 6, 1, 9, 2, 0, 3, 7, 8, 5};
Arrays.sort(array, mainObject::reserve); //使用Main类的成员方法,但是mainObject对象并未参与进来,只是借用了一下刚好匹配的方法
System.out.println(Arrays.toString(array));
}

public int reserve(Integer a, Integer b){ //现在Main类中有一个刚好匹配的方法
return b.compareTo(a);
}

构造方法也可以作为引用传递

1
2
3
4
5
6
7
8
9
10
11
public interface Test {
String test(String str); //现在我们需要一个参数为String返回值为String的实现
}
public String(String original) { //由于String类的构造方法返回的肯定是一个String类型的对象,且此构造方法需要一个String类型的对象,所以,正好匹配了接口中的
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
public static void main(String[] args) {
Test test = String::new; //没错,构造方法直接使用new关键字就行
}

Optional判空包装

语法

1
2
3
Optional.ofNullable(exp)
.ifPresent( exp -> "非空") // lambda
.orElse( exp -> "为空") // lambda

例子

1
2
3
4
5
6
7
8
9
10
private static void test(String str){
Optional
.ofNullable(str) //将传入的对象包装进Optional中
.ifPresent(s -> System.out.println("字符串长度为:"+s.length()));
//如果不为空,则执行这里的Consumer实现
.orElse("备选方案")

// 原本需要进行判空,现在更优雅了
if (str == null) return;
}
1
2
3
4
Integer i = Optional
.ofNullable(str)
.map(String::length) //使用map来进行映射,将当前类型转换为其他类型,或者是进行处理
.orElse(-1);

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
2
3
4
5
6
7
8
// module-info.java
module 模块名 {
requires 模块名; // 导入模块
requires transitive 模块名; // 传递依赖

exports 包名; // 导出的包;
exports 包名 to 模块名; // 对指定模块导出
}

image-20230306174940813

反射控制

模块使得我们可以将模块作为很多包的集合,提供便利的导入和导出功能。

模块机制不仅做了包的分离性,还增加了访问权限的控制。

现在,在没有授权的情况下,你无法通过反射去修改其他包的private字段。

而开放反射权限的语法如下:

  • open 开放整个模块的反射权限
  • opens 开放某个包的权限
  • opens to 开放某个包的反射权限给某个模块
1
2
3
4
open module a { // 开放整个模块\
opens com.test; //通过使用opens关键字来为其他模块开放反射权限
opens com.test to b; // 给模块b开放com.test包的反射权限
}

服务接口

通过模块的相关语法,可以进行动态加载相关接口的实现,实现一些灵活功能。

  • 插件系统:定义一个服务接口,可以让第三方开发者提供实现,在运行时通过ServicesLoader动态加载,实现插件功能。
  • 解耦合架构:模块直接用接口进行通信,不依赖具体实现,可以方便的更换。如日志系统可以定义一个日志接口,接入数据库日志、文件日志等不同的实现。
  • 动态加载和扩展:在运行时可以根据需要动态加载需要的功能模块。

语法如下:

  • uses ... 使用某个接口
  • providers interface with implements 为某个接口提供实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 接口模块(声明)
module com.example.spi {
exports com.example.myapp.spi;
}
// 提供者模块(实现)
module com.example.impl {
requires com.example.spi;
provides com.example.myapp.spi.MyService with com.example.myapp.impl.MyServiceImpl;
}
// 消费者模块(使用)
module com.example.consumer {
requires com.example.spi;
uses com.example.myapp.spi.MyService;
}

接口的private方法

接口的私有方法只能被 默认实现方法私有方法 调用。

私有方法必须具有默认实现。

私有方法可以将一些共享逻辑封装,供多个默认实现使用。

1
2
3
4
5
6
7
8
9
10
public interface Test {
default void test(){
System.out.println("我是test方法默认实现");
this.inner(); //接口中方法的默认实现可以直接调用接口中的私有方法
}

private void inner(){ //声明一个私有方法
System.out.println("我是接口中的私有方法!");
}
}

集合类新增工厂方法

以前创建Map需要在创建对象后手动初始化

1
2
3
4
Map<String, Integer> map = new HashMap<>();   //要快速使用Map,需要先创建一个Map对象,然后再添加数据
map.put("AAA", 19);
map.put("BBB", 23);

Java 9 之后,可以用 of 方法快速创建了

1
Map<String, Integer> map = Map.of("AAA", 18, "BBB", 20);  //直接一句搞定

不过该方法只适用于 0-10对 键值对,因为只重载了这么多

默认创建的对象是 只读

其他的集合类也有of方法

1
2
3
Set<String> set = Set.of("BBB", "CCC", "AAA");  //注意Set中元素顺序并不一定你的添加顺序
List<String> list = List.of("AAA", "CCC", "BBB"); //好耶,再也不用Arrays了

Stream API改进

Java 9 对Stream API进行了改进

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
Stream
.of(null) //如果传入null会报错
.ofNullable(null) //使用新增的ofNullable方法,这样就不会了,不过这样的话流里面就没东西了
.iterate(0, i -> i + 1) // 迭代生成流,Java 9中可以限制生成数量
.limit(20) // 限制20个
.iterate(0, i -> i < 20, i -> i + 1) // 也可以在iterate中间用断言限制
.takeWhile(i -> i < 10) // 读到满足条件的元素直接截断
.dropWhile(i -> i < 10) // 默认截断,满足条件继续读取
.forEach(System.out::println);
}

其他

try-with-resource可以使用现有变量了

1
2
3
4
5
6
7
public static void main(String[] args) throws IOException {
InputStream inputStream = Files.newInputStream(Paths.get("pom.xml"));
try (inputStream) { //单独丢进try中,效果是一样的
for (int i = 0; i < 100; i++)
System.out.print((char) inputStream.read());
}
}

Optional包装可以同时处理两种情况了,使用ifPresentOrElse

1
2
3
4
5
6
7
8
public static void main(String[] args) {
String str = null;
Optional.ofNullable(str).ifPresentOrElse(s -> { //通过使用ifPresentOrElse,我们同时处理两种情况
System.out.println("被包装的元素为:"+s); //第一种情况和ifPresent是一样的
}, () -> {
System.out.println("被包装的元素为null"); //第二种情况是如果为null的情况
});
}

支持用 or() 替换为另一个Optional类

如果被包装的是null 返回Supplier提供的另一个Optional类

1
2
Optional.ofNullable(null)
.or(() -> Optional.of("AAA"))

Java 10

局部变量类型推断

可以使用 var 关键字对局部变量类型推断进行类型推断了

注意:必须是具有初始值的变量才可以进行推断

1
2
String str = "你好!" // 以前的语法
var str = "新语法!" // 自动推断成String

推断只发生在编译期,编译完成后会确定具体的类型。

Java 11

作为继Java 8之后的LTS版本,Java 11带来了

  • Lambda的改进
  • String类的新方法
  • 全新HttpClient

Lambda形参类型推断

1
2
3
4
5
6
Consumer<String> c = (String str) -> {
System.out.println(str);
}; // 老写法
Consumer<String> c = (var str) -> {
System.out.println(str);
}; // 新写法

String类新增方法

方法 描述
isBlank(); 判断是否为空/全为空格
lines(); 通过换行符切割,转为Stream
repeat(num); 返回重复几次的String
strip()、stripLeading()、stripTrailing() 去除首尾空格

全新HttpClient

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient(); //直接创建一个新的HttpClient
//现在我们只需要构造一个Http请求实体,就可以让客户端帮助我们发送出去了(实际上就跟浏览器访问类似)
HttpRequest request = HttpRequest.newBuilder().uri(new URI("https://www.baidu.com")).build();
//现在我们就可以把请求发送出去了,注意send方法后面还需要一个响应体处理器(内置了很多)这里我们选择ofString直接吧响应实体转换为String字符串
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
//来看看响应实体是什么吧
System.out.println(response.body());
}

Java 12-16

主要是一些实验性功能,在Java 16被正式引入

  • Switch表达式
  • 文本块
  • Instanceof模式匹配
  • 记录类型

Switch表达式(Java 14)

使用类lambda作为case的函数体,同时可以将多个case匹配值写入一个case中

函数体需要使用 yield 返回

使用这种语法就不需要显式 break

语法:

1
2
3
4
5
6
7
8
9
var res = switch (obj) {   //这里和之前的switch语句是一样的,但是注意这样的switch是有返回值的,所以可以被变量接收
case [匹配值, ...] -> "优秀"; //case后直接添加匹配值,匹配值可以存在多个,需要使用逗号隔开,使用 -> 来返回如果匹配此case语句的结果
case ... //根据不同的分支,可以存在多个case
case [匹配值, ...] -> {
// 函数体
yield "返回值"; // 注意使用yield返回
};
default -> "不及格"; //注意,表达式要求必须涵盖所有的可能,所以是需要添加default的
};

例子:

1
2
3
4
5
6
7
8
9
public static String grade(int score){
score /= 10; //既然分数段都是整数,那就直接整除10
return switch (score) { //增强版switch语法
case 10, 9 -> "优秀"; //语法那是相当的简洁,而且也不需要我们自己考虑break或是return来结束switch了(有时候就容易忘记,这样的话就算忘记也没事了)
case 8, 7 -> "良好";
case 6 -> "及格";
default -> "不及格";
};
}

文本块(Java 15)

多行字符串可以使用"""包围了,类似python

1
2
3
4
5
6
7
var str = """
你好
我是你
你是我
""";
// 实际上编译后是转义的
String str = "你好\n我是你\n你是我\n";

Instanceof模式匹配(Java 16)

以前在一些情境下,在类型转换之前需要通过instanceof检查一下类型

1
2
3
4
5
6
7
8
@Override
public boolean equals(Object obj) {
if(obj instanceof Student) { //首先判断是否为Student类型
Student student = (Student) obj; //如果是,那么就类型转换
return student.name.equals(this.name); //最后比对属性是否一样
}
return false;
}

现在可以直接转换成模式变量使用

1
2
3
4
5
6
7
@Override
public boolean equals(Object obj) {
if(obj instanceof Student student) { //在比较完成的屁股后面,直接写变量名字,而这个变量就是类型转换之后的
return student.name.equals(this.name); //下面直接用,是不是贼方便
}
return false;
}

Record记录类型(Java 16)

记录类型本质上也是一个普通的类,不过是final类型(即只读)且继承自java.lang.Record抽象类的,它会在编译时,会自动编译出以下方法‘

  • public get
  • hashcode
  • equals
  • toString
1
2
3
4
5
6
7
8
9
10
11
12
public record Account(String username, String password) {  //直接把字段写在括号中

}
var acc = new Account("a", "b");
var acc2 = new Account("a", "b");
System.out.println(acc.username + acc.password);
System.out.println(acc);
System.out.println(acc.equals(acc2));
// 输出
//ab
//Account[username=a, password=b]
//true

Java 17

不介绍预览特性

密封类型

目的:限制类的继承

在之前的版本,可以用final关键字禁止对类的继承。

1
public final class A {}

在Java 17之后可以用密封类实现禁止别人继承的同时,自己可以继承

使用sealed关键字标识密封类,permits标识允许继承的类

1
public sealed class A permits B {} // 允许B继承

密封类型有以下要求:

  • 可以基于普通类、抽象类、接口,也可以是继承自其他接抽象类的子类或是实现其他接口的类等。
  • 必须有子类继承,且不能是匿名内部类或是lambda的形式。
  • sealed写在原来final的位置,但是不能和finalnon-sealed关键字同时出现,只能选择其一。
  • 继承的子类必须显式标记为finalsealed或是non-sealed类型。

标准的声明格式如下:

1
2
3
public sealed [abstract] [class/interface] 类名 [extends 父类] [implements 接口, ...] permits [子类, ...]{
//里面的该咋写咋写
}

子类的格式为:

1
2
3
4
5
public [final/sealed/non-sealed] class 子类 extends 父类 {   //必须继承自父类
//final类型:任何类不能再继承当前类,到此为止,已经封死了。
//sealed类型:同父类,需要指定由哪些类继承。
//non-sealed类型:重新开放为普通类,任何类都可以继承。
}

Java 21

仅介绍正式特性

有序集合

在之前,我们使用不同的集合类,获取元素的API名称非常不一致。

它新增了 SequencedCollectionSequencedSetSequencedMap 三个接口,使得 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. 类型模式
  2. 空模式
  3. 守卫模式
  4. 常量模式

类型模式

1
2
3
4
5
6
7
8
9
10
Object[] list = {"字符串", 123, 456.0};
for (var obj : list) {
switch (obj) {
case Integer inte -> System.out.println("是整数" + inte);
case String str -> System.out.println("是字符串", str);
case Double dou -> System.out.println("是double" + dou);
case Float floa -> System.out.println("是float" + floa);
default -> System.out.println("没匹配到");
}
}

空模式

现在向switch传递一个null值不会抛出异常了

1
2
3
4
switch (null) {
case null -> System.out.println("为空");
case default -> System.out.println("默认");
}

守卫模式

如果在匹配后要进行判断,我们一般会用代码块写

1
2
3
4
5
6
7
case String str -> {
if (str.length() > 5) {
System.out.println("str大于5");
} else {
System.out.println("str小于等于5");
}
};

现在可以追加一个守卫条件,写法更简洁

1
2
case String str && str.length() > 5 ->System.out.println("str大于5");
case String str -> System.out.println("str<=5");

Record模式匹配

在Java 16,引入了Record类,声明一个小型不可变的数据载体。

也引入了Instanceof模式匹配,解决了类型转换问题。

这次引入的Record模式匹配(Record Patterns),它允许我们通过模式匹配直接提取组件,而不需要先进行强制类型转换后再提取。它需要与 instanceof模式匹配switch 模式匹配一同使用。

Instanceof模式匹配

和之前的用法一样,在判断类型后自动进行类型转换,可以很方便的提取字段。

1
2
3
4
if (obj instanceof User user) {
String name = user.name();
Integer age = user.age();
}

Switch模式匹配

在Switch模式匹配中,不仅可以自动转换类型,还可以自动提取字段

1
2
3
4
5
6
7
8
9
10
switch (obj) {
case null -> throw new NullPointerException("不能为空");
// 自动转换类型
case User user -> System.out.println("name:" + user.name() + "--age:" + user.age());
// 自动提取字段
case User(String name,Integer age) -> System.out.println("name:" + name + "--age:" + age);
// 自动推断类型
case User(var name,var age) -> System.out.println("name:" + name + "--age:" + age);
default -> obj.toString();
}

虚拟线程

概念

很多语言都有类似于“虚拟线程”的技术,比如Go、C#、Erlang、Lua等,他们称之为“协程”。

JVM为我们提供了java.lang.Thread,是对操作系统线程的抽象。但是它具有如下缺点:

  • 代价昂贵:创建平台线程的成本很高。每当创建一个平台线程时,操作系统都必须在堆栈中分配大量内存来存储线程的上下文、原生调用堆栈和 Java 调用堆栈。由于堆栈大小是固定的,这就导致了高昂的内存开销。
  • 上下文切换成本高:在多线程环境下,需要在不同线程间切换,这种上下文切换会消耗时间和资源。
  • 线程数量有限:Java 线程仅仅只是对操作系统线程的封装,而操作系统线程的数量是有限的,这就限制了 Java 同时运行的线程数量,从而限制了应用程序的并发能力。

虚拟线程是 Java 中的一种轻量级线程,它旨在解决传统线程模型中的一些限制,提供了更高效的并发处理能力,允许创建数千甚至数万个虚拟线程,而无需占用大量操作系统资源。

img

虚拟线程与传统线程具有如下几个区别:

  • 轻量级

    • 操作系统线程:每个线程由操作系统管理,具有较高的创建和上下文切换成本。操作系统线程消耗大量内存,因为每个线程都有独立的堆栈空间和内核资源。
    • 虚拟线程:由 JVM 管理,更轻量级。虚拟线程的创建和销毁成本很低,消耗的内存也较少,因为它们共享底层的操作系统线程池,避免了高昂的上下文切换成本。
  • 调度

    • 操作系统线程:由操作系统的调度器管理,操作系统线程数量一多,调度开销会显著增加。
    • 虚拟线程:由 JVM 内部的调度器管理,可以更有效地利用 CPU 资源。虚拟线程可以实现比操作系统线程更高效的调度策略。
  • 阻塞与非阻塞操作

    • 操作系统线程:在阻塞操作(如 I/O)时会导致整个线程被阻塞,无法处理其他任务。
    • 虚拟线程:虚拟线程的阻塞操作不会阻塞底层的操作系统线程。JVM 可以通过协程的方式将虚拟线程的阻塞操作挂起,并在资源可用时恢复执行,这样可以实现高并发而不增加系统负担。

优势:

  • 更高的并发性: 虚拟线程允许数百万级别的并发线程,这是操作系统线程无法轻易实现的。高并发应用程序(如服务器和大规模数据处理应用)可以从中受益。
  • 简化编程模型: 虚拟线程使编写并发程序变得更简单。开发者可以用直观的阻塞代码风格来编写逻辑,而不必使用复杂的回调和非阻塞编程技术。
  • 资源利用率优化: 虚拟线程可以更好地利用系统资源。通过避免大量操作系统线程的开销,虚拟线程可以实现更高效的资源使用和性能优化。

虽然虚拟线程这么厉害,但是它不做以下几件事:

  • 不替代传统线程:虚拟线程并不旨在完全替代传统的操作系统线程,而是作为一个补充。对于需要密集计算和精细控制线程行为的场景,传统线程仍然是主流。
  • 非针对最低延迟:虚拟线程主要针对高并发和高吞吐量,而不是最低延迟。对于需要极低延迟的应用,传统线程可能是更好的选择。
  • 不改变基本的线程模型:虚拟线程改进了线程的实现方式,但并未改变Java基本的线程模型和同步机制。锁和同步仍然是并发控制的重要工具。

使用

创建虚拟线程的方法也封装在java.lang.Thead内。

最简单的办法就是使用Thread.startVirtualThread(Runnable),创建一个虚拟线程立刻执行Runnable

1
2
3
Thread.startVirtualThread(() -> {
System.out.println("我是虚拟线程");
});

也可以使用Builder API来创建,方法名为ofVirtual(),可以设置(名称、守护状态、优先级、未捕获异常处理器)等相关属性

1
2
3
4
5
6
7
8
9
10
Thread.ofVirtual()
.name("virtualThreadTest") // 设置名称
.daemon(true) // 守护进程
.priority(3) // 优先级
// 异常处理
.uncaughtExceptionHandler((t,e)-> System.out.println("线程[" + t.getName() + "发生了异常。message:" + e.getMessage()))
// 启动
.start(()->{
System.out.println("虚拟线程启动");
});

线程池

可以使用Executors.newVirtualThreadPerTaskExecutor() 创建一个线程池,该线程池会给每个任务分配一个虚拟线程。

1
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

使用submit()提交任务,可以是RunnableCallable

1
2
3
4
5
6
7
executor.submit(() -> {
System.out.println("我是虚拟线程");
});

Future<String> future = executor.submit(() -> {
return "我是虚拟线程";
});

可以使用shutdown() 来关闭线程池,它会等待正在执行的任务完成,但不会接受新的任务。如果需要立即停止所有任务,可以使用 shutdownNow()