Java 21新特性概述

Java 21于2023年9月19日发布,这是一个LTS(长期支持)版本,到此为止,目前有Java 8、Java 11、Java 17和Java 21这四个LTS版本。

Java 21此次推出了15个新特性,本节就介绍其中重要的几个特性:

更多内容读者可自行阅读:OpenJDK Java 21文档

一、JEP 430:字符串模板(第一次预览)

先卖个关子,我们平常作字符串拼接无非是以下几种方式:

public void concatStr(int x, int y) {
    String result="";
    //1、直接拼
    result= x + " plus " + y + " equals " + (x + y);
    //2、StringBuilder
    result = 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、MessageFormat
    MessageFormat 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();//0
    String last = list.getLast();//1
    List<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 1718、1920对这一特性的介绍。

六、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对这一特性的介绍。

十一、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:更多新特性的介绍,推荐移步至👉 Java新特性学习导航(8~24 持续更新)👈 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橡 皮 人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值