长期支持版的 Java21 在 9 月 19 日发布了,看一下有哪些新特性,主要看看正式加入的特性,预览特性就浏览一遍就行了,毕竟预览特性随时都会变化的。

正式特性

Sequenced Collections | 有序集合

Java 的集合框架缺少一种能够表示具有定义好的排列顺序的元素序列(比如 List 和 Deque 等)的集合类型,以及对这类集合的操作集。

来看看以前获取序列元素的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void getCollectionElementTest() {
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.get(0);
list.get(list.size() - 1);

Deque<Integer> deque = new LinkedList<>(list);
deque.getFirst();
deque.getLast();

SortedSet<Integer> set = new TreeSet<>(list);
set.getFirst();
set.getLast();

LinkedHashSet<Integer> linkList = new LinkedHashSet<>(list);
Iterator<Integer> iterator = linkList.iterator();
while (iterator.hasNext()) {
Integer integer = iterator.next();
}
}

想要拿到序列的第一个元素和最后一个元素,列表使用下标直接获取,队列和有序集合有另外的实现方法,链表就只能遍历获取了,没有一个集合接口来归类这类有顺序的序列,具体的获取元素的操作方法也各不相同。

为了解决这些问题引入了 SequencedCollection 接口,此接口继承自 Collection 接口,具体可以看下图:
JEP 431: Sequenced Collections

List 和 Deque 接口实现了SequencedCollection 接口;SortedMap 和LinkedHashMap 实现了SequencedMap 接口。现在列表和链表都能使用统一的方式获取首尾元素了。

1
2
3
4
5
6
7
8
9
10
@Test
public void getCollectionElementNewTest() {
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.getFirst();
list.getLast();

LinkedHashSet<Integer> linkList = new LinkedHashSet<>(list);
linkList.getFirst();
linkList.getLast();
}

Virtual Threads 虚拟线程

虚拟线程详细看还在预览的时候之前写过一篇文章《简单了解下 JDK19 预览版的 Virtual Threads》,正式特性也没太大变化,之前写的测试用例都能正常跑通。

Record Patterns | 记录模式

Java16 开始新增了 record 类,新增的 Record Patterns 特性就是对 record 类的一些增强性操作,简化类型判断和类型转换流程,并且支持嵌套操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class RecordPatternsExample {

@Test
public void patternTest() {
Point p = new Point(1, 2);
printSum(p);
}

/**
* 类型判断和转换,及获取值
* @param obj
*/
public void printSum(Object obj) {
/**
* 跟普通类一样,传入对象再使用方法获取x和y的值
*/
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x+y);
}

/**
* 简化操作
* 定义传入对象声明参数类型,可在后续直接获取值
*/
if (obj instanceof Point(int x, int y)) {
System.out.println(x + y);
}
}

@Test
public void nestTest() {
ColoredPoint cp = new ColoredPoint(new Point(1, 2), ColInheritingor.BLUE);
printPointY(cp);
}

public void printPointY(Object obj) {
if (obj instanceof ColoredPoint coloredPoint) {
if (coloredPoint.p() != null) {
System.out.println(coloredPoint.p().y());
}
}

/**
* 可嵌套
*/
if (obj instanceof ColoredPoint(Point p, Color c)) {
System.out.println(p.y());
System.out.println(c);
}
}

}

record ColoredPoint(Point p, Color c) {}

enum Color { RED, GREEN, BLUE}

record Point(int x, int y) {
public Point(int x) {
//this.x = x;
this(x, 0);
}
}

Pattern Matching for switch | switch 的模式匹配

这也是一个增强的特性,目的是扩展 switch 表达式的适用性。

看例子,先定义一些类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
sealed abstract class Person permits Tom, Jack, Nathan {
int age;

Person(int age) {
this.age = age;
}
}

final class Tom extends Person {
public Tom(int age) {
super(age);
}
}

final class Jack extends Person {
public Jack(int age) {
super(age);
}
}

final class Nathan extends Person {
public Nathan(int age) {
super(age);
}
}

判断用户是谁可以这么操作:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void whenTest() {
Person person = new Tom(30);
switch (person) {
case null -> System.out.println("null!");
case Jack jack -> System.out.println("I am jack");
case Tom tom
when tom.age == 30 -> System.out.println("Tom's age is 30");
default -> System.out.println("Who am i?");
}
}

在以前我们需要用到 instanceof 来一个个判断 person 是谁,现在可以用 switch 的模式匹配来简化操作,甚至在 case 语句中还可以用 when 语句来对对象的属性做判断。

由于 Person 这里定义的是一个密闭类,即创建的对象只能是指定的几种类型,所以穷尽 case 之后的 default 可以省略。

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void coverageTest() {
Person person = new Nathan(29);
switch (person) {
case Tom tom -> System.out.println("I am tom.");
case Jack jack -> System.out.println("I am jack.");
case Nathan nathan -> System.out.println("I am nathan.");
//default -> ...
//由于 Person 是一个密闭类,实现类只有固定的三个,
//所以这里的三个 case 已经穷尽所有情况,可以不用加 default 分支。
};
}

也可以和记录模式的搭配使用:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void recordTest() {
record MyPair<S,T>(S fst, T snd){};
MyPair<String, Integer> akari = new MyPair<>("Akari", 30);

switch (akari) {
case null -> System.out.println("null!");
case MyPair(var name, var age) -> System.out.printf("username:%s, user age:%d", name, age);
default -> System.out.println("default");
}
}

预览特性

String Templates | 字符串模板

对字符串的操作是日常开发的高配功能,Java12 带来了处理字符串的新方式。

字符串模板处理器类 STR 用于将表达式中的变量进行字符串插值,最后返回字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void STRTest() {
String name = "nathan";
// STR 模板处理器
String str = STR."I am \{name}.";
System.out.println(str);

// 可以内嵌表达式
int a=10, b=20;
int[] arr = {1, 2, 3};
System.out.println(STR."\{a} + \{b} = \{a+b}");
System.out.println(STR."Tody is \{LocalDate.now()}.");
System.out.println(STR."\{a++}, \{a++}, \{arr[0]}");
}

在 Java15 已经可以定义多行文本块,字符串处理也能配合使用,看下面的多行模板表达式例子。

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void mutilLineTest() {
var version = 21;
var str = STR."""
<html>
<body>
<p>Java \{version} is now available!</p>
</body>
</html>
""";
System.out.println(str);
}

除了 STR 之外还有一个 FMT 模板处理器,它除了具备 STR 一样的插值功能外还能做左侧格式化处理。

对比以下 STR 和 FMT 的处理方式和输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void FMTTest() {
record Rectangle(String name, double width, double height) {
double area() {
return width * height;
}
}
Rectangle[] zone = new Rectangle[] {
new Rectangle("Alfa", 17.8, 31.4),
new Rectangle("Bravo", 9.6, 12.4),
new Rectangle("Charlie", 7.1, 11.23),
};

String strTable = STR."""
Description Width Height Area
\{zone[0].name} \{zone[0].width} \{zone[0].height} \{zone[0].area()}
\{zone[1].name} \{zone[1].width} \{zone[1].height} \{zone[1].area()}
\{zone[2].name} \{zone[2].width} \{zone[2].height} \{zone[2].area()}
Total \{zone[0].area() + zone[1].area() + zone[2].area()}
""";
String fmtTable = FMT."""
Description Width Height Area
%-12s\{zone[0].name} %7.2f\{zone[0].width} %7.2f\{zone[0].height} %7.2f\{zone[0].area()}
%-12s\{zone[1].name} %7.2f\{zone[1].width} %7.2f\{zone[1].height} %7.2f\{zone[1].area()}
%-12s\{zone[2].name} %7.2f\{zone[2].width} %7.2f\{zone[2].height} %7.2f\{zone[2].area()}
\{" ".repeat(28)} Total %7.2f\{zone[0].area() + zone[1].area() + zone[2].area()}
""";
System.out.println(strTable);
System.out.println(fmtTable);
}

此外还有一个 RAW 标准模板处理器,有待处理元数据可先用 RAW 写好模板,等拿到变量值再做插值。另外,为了保证安全,如果单纯写一个模板字符串会有检查错误。

1
2
3
4
5
6
public void RAWTest() {
String name = "Joan";
//String info = "My name is \{name}."; //error
StringTemplate st = RAW."My name is \{name}.";
System.out.println(STR.process(st));
}

模板处理器 STR 和 FMT 是功能接口 StringTemplate.Processor 的实例,实现该接口的​​抽象方法 process,该方法接受 StringTemplate 并返回一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void interfaceTest() {
int x = 10, y = 20;
StringTemplate st = RAW."\{x} plus \{y} equals \{x + y}";
System.out.println(st.fragments());
System.out.println(st.values());
//StringTemplate{ fragments = [ "", " plus ", " equals ", "" ], values = [10, 20, 30] }
//输出两部分,一部分是字符串的各段和各段之间的插值占位

//实现接口,组装数据的方法,如果自己要加料就可以在这里处理
var INTER = StringTemplate.Processor.of((StringTemplate _st) -> {
StringBuilder sb = new StringBuilder();
Iterator<String> fragIter = _st.fragments().iterator();
for (Object value : _st.values()) {
sb.append(fragIter.next());
sb.append(value);
sb.append("_");//加点东西看看
}
sb.append(fragIter.next());
return sb.toString();
});
System.out.println(INTER."\{x} plus \{y} equals \{x + y}");
//| 10 and 20 equals 30
}

Unnamed Patterns and Variables | 未命名模式和变量

这个特性主要是用来简化代码的,如果在代码中声明了变量但又用不上,可以用下划线 _ 代替,在记录模式和平常的循环和异常处理代码块中能用到,需要注意的是别跟用下划线开头做变量混淆了。

在记录模式中的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
record Point(int x, int y) { }
record User(Order order) {}
record Order() {}

@Test
public void unnamedPatternTest() {
Point p = new Point(1, 2);
unnamedPattern(p);
}
public void unnamedPattern(Object obj) {
// 如果用不上某些变量可以省略写为 _
if (obj instanceof Point(int x, _)) {
System.out.printf("x value is:%d.", x);
}

// switch 也可以这么用
switch (obj) {
case User(_) -> System.out.printf("user object.");
case Point(_, _) -> System.out.println("point object.");
default -> System.out.printf("default output.");
}
}

在循环和异常处理代码块中的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void unnamedVariablesTest() {
Queue<Integer> list = new LinkedList<>(List.of(1, 2, 3));
int sum = 0;
for (Integer _ : list) {
sum += 10;
}

while (list.size() > 1) {
var _ = list.remove();
}

int _ = sum;
try {
Point _ = new Point(1, 2);
} catch (Exception _) {
throw new RuntimeException();
}

}

Unanmed class and instance Main method 未命名类和实例的 | Main 方法

未命名类和实例的 Main 方法由于还是预览特性,IDEA 也还没有支持,可以用命令行来测试。

新建一个 Main.java 文件,编辑内容:

1
2
3
4
5
void main() {
System.out.println(str);
}

String str = "Unanmed class and instance main method.";

使用命令编译:

1
2
3
$ ~/app/jdk/jdk-21/bin/javac --release 21 --enable-preview Main.java
Note: Main.java uses preview features of Java SE 21.
Note: Recompile with -Xlint:preview for details.

输出:

1
2
$ ~/app/jdk/jdk-21/bin/java --enable-preview Main                   
Unanmed class and instance main method.

参考