Java 21于2023年9月19日发布,这是一个LTS(长期支持)版本,到此为止,目前有Java 8、Java 11、Java 17和Java 21这四个LTS版本。
Java 21此次推出了15个新特性,本节就介绍其中重要的几个特性:
JEP 430:String Templates (Preview)字符串模板(第一次预览)
JEP 431:Sequenced Collections 有序集合
JEP 439:Generational ZGC 分代ZGC
JEP 440:Record Patterns Record模式(转正)
JEP 441:Pattern Matching for switch switch的模式匹配(转正)
JEP 442:Foreign Function & Memory API (Third Preview) 外部函数和内存 API (第三次预览)
JEP 444:Virtual Threads 虚拟线程(转正)
JEP 445:Unnamed Classes and Instance Main Methods (Preview) 未命名类和实例主要方法(第一次预览)
JEP 446:Scoped Values (Preview) 作用域值(第一次预览)
JEP 448:Vector API (Sixth Incubator)向量 API(第六轮孵化)
JEP 453:Structured Concurrency (Preview)结构化并发(第一次预览)
更多内容读者可自行阅读:OpenJDK Java 21文档
一、JEP 430:字符串模板(第一次预览)
先卖个关子,我们平常作字符串拼接无非是以下几种方式:
public void concatStr(int x, int y) {String result="";//1、直接拼result= x + " plus " + y + " equals " + (x + y);//2、StringBuilderresult = new StringBuilder().append(x).append(" plus ").append(y).append(" equals ").append(x + y).toString();//3、format()result = String.format("%2$d plus %1$d equals %3$d", x, y, x + y);//4、MessageFormatMessageFormat mf = new MessageFormat("{0} plus {1} equals {2}");result = mf.format(result, x, y);
}
这或多或少都有缺点,比如难以阅读、复杂、冗长等。
Java 21引入的String Template就是为了解决这些问题的:
String Template提供了一种更简洁、更直观的方式来动态构建字符串的方式。通过"\ {}"占位符,我们就可以将变量的值嵌入到字符串中,在运行时会将占位符替换为实际的变量值。
talk is cheap,show me the code:
result= STR."\{x} plus \{y} equals \{x + y}";
是不是更直观、简洁、容易阅读了,我们再来看下它多行模板表达式的效果:
public static void main(String[] args) {String title = "My Web Page";String text = "Hello, world";String html = STR."""<html><head><title>\{title}</title></head><body><p>\{text}</p></body></html>""";System.out.println(html);//<html>// <head>// <title>My Web Page</title>// </head>// <body>// <p>Hello, world</p>// </body>//</html>
}
STR只是其中一个模板处理器,本次一共推出了三个:
STR:将模板中的每个嵌入表达式替换为该表达式的(字符串化)值来执行字符串插值。
FMT:类似于STR,但是它还可以接受格式说明符,这些格式说明符出现在嵌入式表达式的左边,用来控制输出的样式。
RAW:它会返回一个未处理的StringTemplate对象,这个对象包含了模板中的文本和表达式信息。
FMT处理器就贴个官方的例子吧:
RAW案例:
public static void Str(){String name = "橡皮人";StringTemplate st=RAW."Hello, \{name}";String result = STR.process(st);//Hello, 橡皮人
}
除了这三个模板处理器,你还可以实现StringTemplate.Processor接口,并实现其process()方法即可自定义模板处理器。
二、JEP 431:有序集合
Java 21引入了一个新的集合接口Sequenced Collections,用于处理具有序列顺序的集合,目的是弥补现有集合接口中对顺序操作的支持不足。
ps:这里的有序指的是元素的存取顺序。
Sequenced Collections包含下面三个接口:
SequencedCollection
SequencedSet
SequencedMap
SequencedCollection继承自Collection接口,且List和Deque接口都继承了SequencedCollection:
public interface SequencedCollection<E> extends Collection<E> {//方法的功能见名知意,不再赘述SequencedCollection<E> reversed();default void addFirst(E e);default void addLast(E e);default E getFirst();default E getLast();default E removeFirst();default E removeLast();
}
举个例子:
public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("A"); //[A]list.addFirst("0");//[0,A]list.addLast("1");//[0,A,1]String first = list.getFirst();//0String last = list.getLast();//1List<String> reversed = list.reversed();//[1,A,0]
}
剩下两个就不做介绍了,也基本是这些方法,这里贴个官方文档中这三个集合接口的层次结构:
三、JEP 439:分代ZGC
ZGC 通过 JEP 333 集成到 Java 11 中,当时还是处于实验阶段。
经过多个版本的迭代,ZGC在Java 15终于可以正式使用了,不过默认的垃圾回收器还是G1。
我们先回顾下Java 11时ZGC的特性:
低停顿:GC停顿时间不超过10ms,通常是亚毫秒级别。
高吞吐量:ZGC是一个并发垃圾回收器,意味着大部分垃圾收集工作都是在Java线程继续执行的同时完成的。
兼容性:ZGC于现在Java应用程序完全兼容,但由于还处于实验阶段,目前只能在Linux/x64上使用。
支持大堆:能处理8MB到16TB大小的堆,适用于大规模内存需要的应用程序。
不分代回收:在垃圾回收时堆全量内存进行标记,但回收时仅针对分内存回收,优先回收垃圾比较多的页面。
在Java 17的HostSpot调优指南提到了weak generational hypothesis,指出年轻代的对象往往朝生夕死,而老年代的对象却长时间存在,因此收集年轻代对象所需的资源较少,并且回收的内存较多,而收集老年代则需要更多资源,回收的内存较少。因此,通过更频繁地收集年轻对象,可以提高使用ZGC的应用程序的性能。
因此,在Java 21提供了分代ZGC的功能,在未来版本中打算将非分代ZGC移除并把分代ZGC作为默认值。
目前使用分代ZGC需要以下参数启动:
java -XX:+UseZGC -XX:+ZGenerational
四、JEP 440:Record模式(转正)
记录模式由 JEP 405 提议作为预览功能,并在 JDK 19 中提供。
JEP 432 再次预览并在 JDK 20 中提供。
在Java 21成为正式特性。
本次除了一些细微的编辑更改外,自第二次预览以来的主要更改是删除了对出现在增强的 for 语句的标题中的记录模式的支持。此功能可能会在未来的 JEP 中重新提出。
也就是不再支持这种写法:
void dump(Point[] points){for (Point(var x, var y) : points) {System.out.println(x + " " + y);}
}
五、JEP 441:Switch的模式匹配(转正)
此功能最初由 JEP 406 (Java 17) 提出。
随后由 JEP 420 (Java 18)、427 (Java 19) 和 433 (Java 20) 改进。
在Java 21成为正式特性。
内容较多,就不粘贴了,可以参考Java 17、18、19、20对这一特性的介绍。
六、JEP 442:外部函数和内存API(第三次预览)
外部函数和内存(FFM)API首先在JDK 19中通过JEP 424预览。
然后在JDK 20中通过JEP 434再次预览。
此JEP提出第三次预览。
这次改动主要对相关API进行了调整。内容较多,就不粘贴了,详细内容请参考Java 19对这一特性的介绍。
七、JEP 444:虚拟线程(转正)
参考Java 21中虚拟线程是怎么回事。
八、JEP 445:未命名类和实例主要方法(第一次预览)
这个特性在Java 21没正式发布之前就被众多开发者吐槽为"最没用的特性",那这是怎么回事呢?下面我们看看。
这个特性主要简化了main方法的声明。对于 Java 初学者来说,这个 main
方法的声明引入了太多的 Java 语法概念,不利于初学者快速上手。
没有该特性之前的Hello World程序:
public class HelloWorld {public static void main(String[] args) {System.out.println("Hello World!");}
}
在Java 21增强了启动Java程序协议后,允许main方法不是static的,那也意味着类不需要是public,也不需要具有 String[]参数:
class HelloWorld {void main() {System.out.println("Hello World!");}
}
再一次精简,未命名的类允许我们不定义类名:
void main() {System.out.println("Hello World!");
}
九、JEP 446:作用域值(第一次预览)
通过 JEP 429 在 JDK 20 中首次孵化。
在 JDK 21 中,进行第一次预览,并无改动点。
作用域值允许在大型程序中的组件之间安全有效地共享数据,而无需求助于方法参数。它是ScopedValue 类型的变量。通常,它被声明为final static字段,因此可以很容易地从许多组件访问它。
像线程局部变量一样,作用域值(scoped value)在每个线程中都有多个实例。使用哪个具体的实例取决于哪个线程调用其方法。与线程局部变量不同,作用域值在写入后是不可变的,并且仅在线程执行的有限时间内可用。
下面的代码示例中使用了作用域值。一些代码会调用ScopedValue.where(…),提供一个作用域值和它想要绑定的对象。对run(…)的调用会绑定作用域值,提供一个特定于当前线程的版本,然后执行作为参数传递的lambda表达式。在run(…)方法执行期间,直接或间接从Lambda表达式调用的任何方法,都可以通过值get()方法读取作用域值。当run(…)方法完成后,绑定就会被销毁:
final static ScopedValue<...> V = ScopedValue.newInstance();// In some method
ScopedValue.where(V, <value>).run(() -> { ... V.get() ... call methods ... });// In a method called directly or indirectly from the lambda expression
... V.get() ...
这乍一看好像和ThreadLoacl差不多,但与ThreadLocal相比,它有以下几个优点:
作用域值仅在run(...)方法的Runnable的生命周期内有效,并会在run方法执行完之后立即销毁(进行垃圾回收),而ThreadLocal会将value保存在内存中,直到线程被销毁(如果是线程池则永远不会销毁)或者调用remove方法,才能被垃圾回收。
作用域值是不可变的,他只能通过重新绑定来重置新的值,这可以提高了代码的可读性,而ThreadLocal随时都可以使用set()更改(如果要溯源可能需要看好几处代码)。
由StructuredTaskScope创建的子线程可以访问父线程的scoped值,而ThreadLocal也可以通过InheritableThreadLocal达到这个效果,不过它是通过创建副本的方式,这样内存占用相对来说也比较大。
再一个比较重要的就是虚拟线程,由于虚拟线程是Thread的实例,所以也有线程局部变量的问题,我们知道虚拟线程可以大量创建,如果有数千或数百万个虚拟子线程时,访问父线程的scoped值(而不是通过创建副本的方式)可以节省大量内存。
十、JEP 448:向量API(第六轮孵化)
Vector API 最初由 JEP 338 提出,并作为孵化 API 集成到 JDK 16 中。
JEP 414(集成到 JDK 17 中)、JEP 417 (JDK 18)、JEP 426 (JDK 19) 和 JEP 438 (JDK 20) 。
此JEP建议在JDK 21中进行第六轮孵化,与JDK 20相比,仅对相关API做了细微调整。
内容较多,就不粘贴了,可以参考Java 16、17、18、19、20对这一特性的介绍。
十一、JEP 453:结构化并发(第一次预览)
结构化并发由 JEP 428 提出,并在 JDK 19 中作为孵化 API 提供。
它在 JDK 20 中由 JEP 437 重新孵化。
在JDK 21中进行第一次预览,与JDK 20相比,唯一的变化是StructuredTaskScope的fork()方法的返回值,由Future变为SubTask。
以下内容摘自Java 19对该特性的介绍:
Java 19引入了结构化并发,一种多线程编程方式,目的是为了通过结构化并发API来简化多线程编程,目前处于孵化阶段。
结构化并发 API 的主要类是 StructuredTaskScope。这个类允许开发者将一个任务组织成一组并发子任务,并将它们作为一个整体进行协调。子任务在各自的线程中执行,通过分别分叉(fork)它们,然后作为一个整体进行合并(join),并且可能作为一个整体进行取消。子任务的成功结果或异常由父任务进行聚合和处理。StructuredTaskScope 将子任务或分叉的生命周期限制在一个明确的词法范围内,在这个范围内,任务与其子任务的所有交互——包括分叉、合并、取消、处理错误和结果组合——都发生在此范围内。
StructuredTaskScope一般工作流程如下:
创建一个作用域(scope)。创建作用域的线程是它的所有者。
在作用域中分叉(fork)并发子任务。
作用域中的任何分叉或作用域的所有者都可以调用作用域的 shutdown() 方法来请求取消所有剩余的子任务。
作用域的所有者作为一个整体加入作用域,即所有分叉。所有者可以调用作用域的
join()
方法,该方法会阻塞直到所有分叉要么完成(成功或失败),要么通过 shutdown() 被取消。或者,所有者可以调用作用域的 joinUntil(java.time.Instant) 方法,该方法接受一个截止时间。加入后,处理任何分叉中的错误并处理它们的结果。
关闭作用域,通常通过 try-with-resources 隐式完成。这会关闭作用域并等待任何滞后的分叉完成。
这是官方给的一个示例:
Response handle() throws ExecutionException, InterruptedException {try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {//使用fork方法派生线程来执行任务Future<String> user = scope.fork(() -> findUser());Future<Integer> order = scope.fork(() -> fetchOrder());
scope.join(); // 将两个fork合并scope.throwIfFailed(); // ...出现异常(抛异常)
// 代表这两个fork都成功了,组合它们的结果。return new Response(user.resultNow(), order.resultNow());}
}
End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。