首页 > 编程语言 >Java 21新增的语法特性

Java 21新增的语法特性

时间:2024-06-13 18:10:32浏览次数:28  
标签:Java 21 System name 语法 虚拟 线程 println out

Java 21新增的语法特性

目录

说明1:本文大量参考了JEP文档与Oracle官方文档,部分文字直接引自这两个文档并进行了适当的修改。
说明2:本文代码详见《面向实践的Java程序设计教程》教材的代码仓库

引言

Java 21于2023年9月19日发布。Java 21新增了许多语法特性,有的语法特性会成为 Java 语言的“永久”部分,即它们会从预览或试验状态转变为正式的、长期支持的语言特性。而有的语法特性则只是短期试验性质的,可能会在未来的某个时间点被移除或修改。本文只介绍那些成为永久性语言特性的语法特性。

record模式 [JEP 440]

record模式(record pattern)可以让Java更方便地解构(destruct)record类型的值。record类型常用于保存数据,因此当拿到一个record类型的值时,我们通常需要将其进行解析取出其中的值。我们可以将存储在record类型实例中的各个值称为该实例中的各个部件(component)。比如对于一个Person类型的实例,可以将它的name属性和age属性称为部件。
现有两个record类型PersonAddress与一个Person类型实例,如下所示。

record Address(String city, String street, String zip){}
record Person(String name, int age, Address address) {
};

Person p = new Person("小王", 19, new Address("厦门","集美", "361021"));

在Java 16中,可使用访问器方法(accessor method)获取Person类型实例p的部件。
如下代码通过person的name()、age()、address()分别获得person的部件name, age, address

if (p instanceof Person person) {
    String name = person.name();
    int age = person.age();
    Address address = person.address();
    System.out.println("name=" + name + ", age=" + age);
    System.out.println("city=" + address.city() + ", street=" + address.street() + ", zip=" + address.zip());
}

到了Java 21,则可通过如下方式获得各部件的值:

if (p instanceof Person(String name, int age, Address(String c, String s, String z))){
    System.out.println("name=" + name + ", age=" + age);
    System.out.println("city=" + c + ", street=" + s + ", zip=" + z)
}

可以看到,在Java 21中无需定义局部变量nameageaddress,只需在Person(String name, int age, Address(String c, String s, String z))这个模式(pattern)中定义各个部件的名称,就可方便地获取各个部件的值。
从上面的代码中还可以看到,record模式还支持模式的嵌套。比如,在上面的代码中,Person模式中嵌套了一个Address模式,通过模式嵌套就可以很方便地获取模式中的每个值(部件)。

用于switch的模式匹配 [JEP 441]

在Java 21中switch的case标签(case label)除了支持枚举常量、字符串常量、整数常量(包括char类型),还支持类型模式(type pattern)与null值。
现编写一个方法process(Object obj),可以根据传入对象的类型进行不同的处理,其可以处理null、Person、String、int等类型。

public static void process(Object obj) {
    switch (obj) {
        case null -> System.out.println("null");
        case Person p -> System.out.println("name=" + p.name() + ", age=" + p.age());
        case String s -> System.out.println("String=" + s);
        case Integer i -> System.out.println("int=" + i);
        default -> System.out.println("未知对象");
    }
}

如果执行如下代码,将输出name=zhangsan, age=18

Person p = new Person("zhangsan", 18, new Address("xiamen", "jimei", "361001"));
process(p);

如果执行process(null),则输出null。case标签支持null,让我们无需专门为null情况编写特殊的处理逻辑。

如果执行process("hello"),则输出String=hello

如果执行process(123),则输出int=123

case标签支持嵌套记录模式:

Java 21的case标签还支持多重嵌套记录模式(multiple nested record pattern)。参考代码如下:

public static void nestedRecordPatternCaseTest(Person p){
    switch (p) {
        case Person(String name, int age, Address(String c, String s, String z)) -> {
            System.out.println("name=" + name + ", age=" + age);
            System.out.println("city=" + c + ", streed=" + s);
        }
        default -> System.out.println("未知");
    }
}

守卫标签(guarded label):

守卫标签指的是在case标签中使用布尔表达式语句。比如,在上述代码中,case标签case Person(String name, int age, Address(String c, String s, String z))中,可以使用when子句指定守卫(guard)。这里的守卫,可以理解为只有当满足条件时才允许执行下去

public static void guardedLabelTest(Person p) {
    switch (p) {
        case null -> System.out.println("null");
        case Person(String name, int age, Address address)
                when age < 18 -> {
            System.out.println(name + "是未成年人!");
        }
        case Person(String name, int age, Address address)
                when (name != null) && (name.length()>2)-> { // age>=18时才会执行
            System.out.println(name+"的姓名长度 = "+name.length());
        }
        default -> System.out.println("其他情况");
    }
}

对于上述代码,进行如下测试,输出结果见注释:

Person p1 = new Person("Alice", 17, null);
guardedLabelTest(p1); // 输出:alice是未成年人!
Person p2 = new Person("ZhangSan", 19, null);
guardedLabelTest(p2); // 输出:ZhangSan的姓名长度 = 8
Person p3 = new Person("ET", 19, null);
guardedLabelTest(p3); // 输出:其他情况

Java 21中switch对enum常量的增强:

在Java 21中,switch对enum常量的增强,使得case标签可包含不同的enum类型。现定义enum类型SeasonDirection,如下所示:

enum Season {
    SPRING, SUMMER, AUTUMN, WINTER
}
enum Direction {
    EAST, WEST, SOUTH, NORTH
}

现在编写一个方法enumConstantCaseTest,可以针对传入的枚举常量进行不同的处理。

public static void enumConstantCaseTest(Object s) {
    switch (s) {
        case Season.SPRING -> System.out.println("春天");
        case Season.SUMMER -> System.out.println("夏天");
        case Season.AUTUMN -> System.out.println("秋天");
        case Season.WINTER -> System.out.println("冬天");
        case Direction d -> System.out.println(d + "不是Season");
        default -> System.out.println("其他类型");
    }
}

对于上述代码,进行如下测试,输出结果见注释:

enumConstantCaseTest(Season.AUTUMN);  // 输出:秋天
enumConstantCaseTest(Direction.EAST); // 输出:EAST不是Season
enumConstantCaseTest("abc");          // 输出:其他类型

序列集合 [JEP 431]

JEP 431引入了序列集合(sequential collections)。在这个JEP中这样介绍序列集合这个特性:

引入新的接口来表示具有明确定义的遍历顺序的集合。每个这样的集合都有一个明确定义的第一元素、第二元素,依此类推,直到最后一个元素。它还提供了统一的API来访问其第一个和最后一个元素,以及用于反向处理其元素。

虽然现有的Java集合框架,有些集合已经是有序的,比如ListTreeSetLinkedHashSetLinkedHashMap等。但它们并没有提供一个统一的接口来表示这种有序性,也没有一种标准的方式来访问它们的第一个和最后一个元素,或者在反向顺序中处理它们的元素

新引入的三个接口分别是SequencedCollection、SequencedSet<E>、SequencedMap<K,V>,这些接口在Java集合框架中的位置如下图所示:
JEP 431网页上的图

从图中可以看到,这三个新的接口在整个框架的位置决定了ListTreeSetLinkedHashSet都有着统一的接口SequencedCollection,其中因为TreeSetSortedSet子接口,所以也是SequencedCollection的子接口。而LinkedHashMap则是SequencedMap的子接口。

SequencedCollection接口定义了如下方法:

interface SequencedCollection<E> extends Collection<E> {
    // methods promoted from Deque
    void addFirst(E);  // 在集合的第一个位置添加元素
    void addLast(E);   // 在集合的最后一个位置添加元素
    E getFirst();    // 返回集合的第一个元素
    E getLast();     // 返回集合的最后一个元素
    E removeFirst(); // 移除集合的第一个元素
    E removeLast();  // 移除集合的最后一个元素

    // new method
    SequencedCollection<E> reversed();  // 返回集合的一个逆序试图
}

代码中// new method表示的是新增的方法,// methods promoted from Deque表示的是从Deque接口中提升的方法。所谓的提升,指的是这些方法本来是定义在Deque方法中,现在被提升到Deque的父接口SequencedCollection中。

现以最常用的ArrayList来做一个实验,如下代码所示,输出结果见注释:

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.addFirst("begin");
list.addLast("end"); // 同list.add("end")
for(String e: list){ // 输出:begin a b c end
    System.out.print(e+" ");
}
System.out.println();
List<String> reversed = list.reversed();
for(String e: reversed){ // 输出:end c b a begin 
    System.out.print(e+" ");
}
System.out.println();

再以TreeSet做一个实验,如下代码所示,输出结果见注释:

TreeSet<String> set = new TreeSet<>(); // 注意:set变量类型必须是TreeSet,不能是Set。因为只有TreeSet才是SequencedCollection的子接口
set.add("a");
set.add("b");
set.add("c");
//set.addFirst("begin"); // UnsupportedOperationException,因为TreeSet中元素的位置由比较顺序决定
//set.addLast("end"); // UnsupportedOperationException,原因同上
for(String e: set){ // 输出:a b c
    System.out.print(e+" ");
}
System.out.println();
Set<String> reversed = set.reversed();
for(String e: reversed){ // 输出:c b a
    System.out.print(e+" ");
}
System.out.println();

从输出结果可以看出,虽然TreeSet也有addFirstaddLast方法,编译的时候也不会出错,但执行的时候会抛出UnsupportedOperationException,这是因为TreeSet中元素的位置由比较顺序决定,调用addFirstaddLast并不能真正的实现相应的操作。

至于实现逆序视图。从上面两段程序可以看出,无论是ArrayList还是TreeSet,它们都实现了SequencedCollection接口,所以它们都具备了reversed方法,所以都可以通过reversed方法来获取逆序视图。

大家可以自己实验测试getFirstgetLastremoveFirstremoveLast方法。

再看看SequencedMap<K,V>接口:

interface SequencedMap<K,V> extends Map<K,V> {
    // new methods
    SequencedMap<K,V> reversed();
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    V putFirst(K, V);
    V putLast(K, V);

    // methods promoted from NavigableMap
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}

然后,我们以LinkedHashedMap(该类可以保持插入顺序)做实验来展示SequencedMap的特性,如下代码所示:

 LinkedHashMap<String,String> map = new LinkedHashMap<>();
map.put("a","1");
map.put("b","2");
map.put("c","3");
map.putFirst("d","4");
map.putLast("e","5");
System.out.println("Map EntrySet:");
for(Map.Entry<String,String> e: map.entrySet()){
    System.out.println(e.getKey()+" "+e.getValue());
}
System.out.println("Reversed Map EntrySet:");
SequencedMap<String, String> reversed = map.reversed();
for(Map.Entry<String,String> e:map.reversed().entrySet()){
    System.out.println(e.getKey()+" "+e.getValue());
}

输出结果如下:

Map EntrySet:
d 4
a 1
b 2
c 3
e 5
Reversed Map EntrySet:
e 5
c 3
b 2
a 1
d 4

可以看到,LinkedHashMap的putFirstputLast方法可以保证插入顺序,而reversed方法可以获取逆序。
大家可以自己实验测试SequencedMap中的其他新增方法。

虚拟线程 [JEP 444]

平台线程、虚拟线程和OS线程:
在Java中,当你使用传统的多线程技术启动线程时,实际上启动的是一个平台线程(Platform Thread),该线程是操作系统线程(OS线程)的一个轻型包装器。一个平台线程在其整个生命周期中都和某个OS线程绑定。平台线程的的数量为OS线程所限。OS线程由操作系统管理,它们的创建、调度和管理都需要消耗一定的系统资源。
虚拟线程(Virtual Thread)则是一种轻量级线程。虚拟线程中的代码虽然也需要在OS线程上运行,但虚拟线程并不和OS线程绑定。当虚拟线程挂起时,OS线程可以被其他虚拟线程所使用。因为没有和OS线程绑定,因此创建、销毁、调度和管理虚拟线程的系统资源开销要小得多。

虚拟线程的适用范围:
虚拟线程的主要优势在于它们可以极大地简化并发编程,提高程序的可扩展性和性能。然而,需要注意的是,虚拟线程并不适合计算密集型任务,因为密集型计算始终需要CPU资源作为支持。虚拟线程非常适合于需要处理大量并发任务和请求的场景,尤其是对于 Web 应用程序和需要高并发处理能力的系统来说,它们可以提供显著的性能提升。
在Web应用程序中每个客户端对服务器端的请求任务可能不需要占用多少CPU资源,但却要花费大量时间在网络传输和处理请求上。如果使用传统线程技术,那么每个请求任务都会占用一个操作系统原生线程(thread-per-request),这会导致操作系统线程的过度消耗。而使用虚拟线程,每个请求任务都可以由一个虚拟线程来处理,从而大大提高系统的并发处理能力。

虚拟线程使用示例:
如下演示代码中,分别使用虚拟线程和传统线程的方式执行10万个任务,任务为线程休眠1秒钟。线程休眠时不会耗费CPU时间,因此这个任务不是CPU密集型任务。一般来说,CPU密集型任务通常是指需要大量计算和处理的任务,会占用大量CPU资源。由于这里的任务主要是等待,而不是进行大量计算,因此它不属于CPU密集型任务。因此,这个程序可以一定程度上模拟Web服务器要高并发处理多个请求的情况。

import java.time.Instant;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class VirtualThreadTest {
    public static final int N = 10_0000;
    public static final int TIMEOUT = 100; // 100秒的超时时间
    // 传统线程测试
    public static void traditionalThreadTest() throws InterruptedException {
        System.out.println("Traditional Thread Test Begin:");
        Instant startTime;
        try (ExecutorService executor = Executors.newCachedThreadPool()) {
            startTime = Instant.now();
            for (int i = 0; i < N; i++) {
                executor.execute(() -> {
                    try {
                        Thread.sleep(1000); // 休眠1秒
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
            executor.shutdown();
            // TIMEOUT秒以后线程池的任务仍未全部执行完毕,则立即关闭线程池并尝试中断所有正在执行的任务
            if (!executor.awaitTermination(TIMEOUT, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        }

        Instant endTime = Instant.now();
        long duration = endTime.toEpochMilli() - startTime.toEpochMilli();
        System.out.println("All threads have finished.");
        System.out.println("Duration: " + duration + " milliseconds");
    }
    // 虚拟线程测试
    public static void virtualThreadTest() throws InterruptedException {
        System.out.println("Virtual Thread Test Begin:");
        Instant startTime;
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            startTime = Instant.now();
            for (int i = 0; i < N; i++) {
                executor.execute(() -> {
                    try {
                        Thread.sleep(1000); // 休眠1秒
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
            executor.shutdown();
            if (!executor.awaitTermination(TIMEOUT, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        }
        Instant endTime = Instant.now();
        long duration = endTime.toEpochMilli() - startTime.toEpochMilli();
        System.out.println("All virtual threads have finished.");
        System.out.println("Duration: " + duration + " milliseconds");
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("Total: "+ N +" tasks!");
        System.out.println("----------------------");
        virtualThreadTest();
        System.out.println("----------------------");
        traditionalThreadTest();
    }
}

程序输出:

Total: 100000 tasks!
----------------------
Virtual Thread Test Begin:
All virtual threads have finished.
Duration: 2287 milliseconds
----------------------
Traditional Thread Test Begin:
All threads have finished.
Duration: 8466 milliseconds

从输出结果可以看出,分别使用虚拟线程和传统线程方式执行10_0000个任务,虚拟线程的执行时间明显少于传统线程。
代码中使用了Executors.newVirtualThreadPerTaskExecutor()创建了一个执行器,该执行器会为每个任务创建一个虚拟线程来执行。任务提交后,JVM负责对虚拟线程的调度,用户无需操心。

创建与运行虚拟线程的方法:

除了上面的使用Executors.newVirtualThreadPerTaskExecutor()来创建与启动虚拟线程外,还可使用Thread.ofVirtual()方法,该方法返回一个用于创建虚拟线程或创建虚拟线程的线程工厂的构建器。示例代码如下:

    
// 方法1:创建一个不会自动启动的虚拟线程,然后手动启动
Thread.Builder builder = Thread.ofVirtual().name("vt1Unstarted"); // 虚拟线程构建器
Thread thread = builder.unstarted(() -> {
    String name = Thread.currentThread().getName();
    System.out.println("I'm " + name + ".");
    
});
thread.start();
thread.join(); // 让主线程等待该线程中止,这样才能看到该虚拟线程打印的信息

// 方法2:创建一个会自动启动的虚拟线程
Thread thread = Thread.ofVirtual().name("vt2Start").start(() -> { // 相较于方法1,是简化写法
    String name = Thread.currentThread().getName();
    System.out.println("I'm " + name + ".");
});
thread.join();

// 方法3:使用虚拟线程工厂创建一个虚拟线程
ThreadFactory vtFactory = Thread.ofVirtual().factory();
Thread thread = vtFactory.newThread(() -> {
    String name = Thread.currentThread().getName();
    System.out.println("I'm " + name + ".");
});
thread.setName("vt3ThreadFactory");
thread.start();
thread.join();

分析:

  1. 三种创建虚拟线程的方法创建出的虚拟线程都是Thread类型。但是执行start()方法后,启动的是虚拟线程,而不是平台线程。
  2. 虚拟线程本质上还是java.lang.Thread的一个实例,不过它和平台线程不同,它没有和OS线程绑定。
  3. 也可使用Thread.startVirtualThread(thread)启动虚拟线程,该方法等同于Thread.ofVirtual().start(task)

虚拟线程的自动调度与I/O任务:

虚拟线程使用的是M:N调度策略,即大量的(M)虚拟线程被调度到少量的(N)操作系统线程上。而这种调度是由JVM本身来完成的,程序员无需关心虚拟线程的调度。
再来看看JDK调用一个阻塞的I/O操作时,会发生什么?这时,JVM会进行一次非阻塞的系统调用,然后虚拟线程会自动被挂起,并等待I/O操作完成。当I/O操作完成后,虚拟线程会被重新调度,并继续执行。这种自动调度,让程序员无需关心虚拟线程的调度,从而简化了并发编程的复杂性。所以说,为什么虚拟线程适用于Web应用程序,这是因为Web应用程序通常需要处理大量并发请求,而这些并发请求可能需要进行大量的I/O操作(磁盘I/O,网络I/O等),当虚拟线程进行I/O操作时可以被挂起,不再占用OS线程,OS线程可以去完成其他任务。因此说,虚拟线程适用于I/O密集型任务。

虚拟线程使用建议:
对于Java开发人员来说,虚拟线程使用起来和传统的Thread类非常相似。并且,即使是大量虚拟线程的创建和调度开销非常小。开发人员可以根据任务需求,创建大量的虚拟线程而无需担心系统资源的消耗。使用虚拟线程的时候不应对他们进行池化(即,无需使用线程池)。大部分虚拟线程生命周期很短并且应只有很浅的调用栈,一个HTTP客户端调用,一个JDBC查询调用,或者一个文件读写调用都可以使用虚拟线程

关于虚拟线程的更多使用方法,可参考Oracle官方文档:Virtual Threads

参考文章:

Java 21的其他改进

  • JEP 439:分代 ZGC(Generational ZGC)。简单地说,可以提高应用程序性能。
  • JEP 452:密钥封装机制API。提供了一种用于密钥封装机制(Key Encapsulation Mechanism,简称 KEM)的 API,这是一种使用公钥加密来保护对称密钥的加密技术。详细信息请查看KEM相关文档

标签:Java,21,System,name,语法,虚拟,线程,println,out
From: https://www.cnblogs.com/zhrb/p/18246478

相关文章

  • Spring Junit 测试报错 java.lang.IllegalStateException
    写测试代码的时候出现了java.lang.IllegalStateException:CouldnotloadTestContextBootstrapper[null].Specify@BootstrapWith's'value'attributeormakethedefaultbootstrapperclassavailable.代码如下:packagecom.example.service;importcom.example.c......
  • Java优雅统计耗时【工具类】
    任务耗时如何优雅的打印,看完本文你就明白了!~importcn.hutool.core.date.StopWatch;importcn.hutool.core.lang.Console;/***优雅打印出任务耗时*/publicclassMain{publicstaticvoidmain(String[]args)throwsException{StopWatchstopWat......
  • ABC 321 F #(subset sum = K) with Add and Erase
    题意有一个箱子,每次可以向里面添加或者拿走一个数,问每次操作过后,任选箱子里的数相加,总和等于k的方案数是多少。思路萌新算是学到新东西了,这其实是个可撤销背包的板题。我们先考虑一个问题:对于普通计数类dp,我们现在禁用某一个数i,我们现在想知道某一个数j有多少种方式表示(即dp......
  • JAVA面向对象练习题2
    题目要求:        定义一个Student实体类,成员变量:name、age。静态成员变量:在线人数。在测试类中:创建集合,存储学生对象,每创建一个学生对象,在线人数+1,删除一个学生对象,在线人数-1定义方法完成:请给集合中存储3个学生对象,并遍历集合,并输出在线人数。定义方法完成:请判断......
  • SVRF Statement Syntax Conventions (SVRF 语法约束)
    SVRFStatementSyntaxConventionsSVRF语法语句约束ParameterOrder参数顺序Casesensitivity大小写敏感区分Literalkeywordsversusvariableparameters原文关键字和变量的特征Whitespaceconsiderations空白区域考虑(不太明白这个是是什么意思)Reserved......
  • 一起来学javascript-axios
       <!--//AJAX的封装插件——Axios。  //什么是Axios  //Axios是一个基于Promise的HTTP库,可以用于浏览器和Node.js,支持VanillaJS、Angular、React、Vue等框架。  //简单的理解就是对Ajax的封装,且具有易用、简洁、高效等特点。  ......
  • COMP9021 Principles of Programming
    COMP9021PrinciplesofProgrammingTerm2,2024CodingQuiz1Worth4marksanddueWeek3Thursday@9pmDescriptionYouareprovidedwithastubinwhichyouneedtoinsertyourcodewhereindicatedwithoutdoinganychangestotheexistingcodetocomplete......
  • 使用 JavaScript 中的 DeviceOrientationEvent
    在前端开发中,DeviceOrientationEvent是一个非常有用的API,它允许我们访问设备的物理方向信息,如设备的倾斜和旋转。这个API可以在移动设备上获取设备的方向,可以用来创建各种有趣和交互性强的应用程序,比如游戏、增强现实体验等。本文将介绍如何使用DeviceOrientationEventAP......
  • java学习笔记(八):多态、包、权限修饰符、修饰方法、final
    目录一、多态1.1多态的形式1.2多态的使用场景1.3多态的定义和前提1.4多态的运行特点1.5多态的弊端1.6引用类型转换1.7综合练习二、包2.1包名的命名规范:2.2导包2.3使用不同包下的相同类怎么办?三、权限修饰符3.1权限修饰符3.2不同权限的访问能力四、......
  • JavaScript-DOM
    DOM全称:DOM(DocumentObjectModel--文档对象类型) 作用:用来操控网页类容的功能,开发网页特效和实现用户交互DOM结构将HTML文档以树形结构表现出来称之为DOM树获取DOM 语法:document.querySelector('css选择器')参数:包含一个或多个css选择器字符串返回值:CSS选......