2023年9月19日,Oracle公司发布了Java的下一个LTS版本21,在这次更新中带来了15个JEPs

官方文档地址 JDK 21 Release Notes

顺序集合(Sequenced Collections)

新的接口定义了一个有顺序的集合(encounter order),提供方法获取第一个元素和最后一个元素,在集合的头部或者尾部插入和删除元素,以及逆序遍历集合等,在一定程度上减少了部分不必要的操作

这样的接口有三种SequencedCollectionSequencedSetSequencedMap

这是一个有关List的示例

//使用List接口
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
//获取第一个元素
list.get(0);
//获取最后一个元素
list.get(list.size() - 1);
//使用SequencedCollection接口
SequencedCollection<Integer> list1 = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
//获取第一个元素
list1.getFirst();
//获取最后一个元素
list1.getLast();

当然,加入了新的类之后,Java中有关集合的结构图也有所改变

记录模式匹配(Record Patterns)

该特征主要是对JDK 16中引入的Record类进行的补充,可以让我们更方便的提取记录中的值,具体用法如下

record Point(int x, int y) {}
//Java 16
static void printSum(Object obj) {
    if (obj instanceof Point p) {
        int x = p.x();
        int y = p.y();
        System.out.println(x + y);
    }
}
//Java 21
static void printSum2(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println(x + y);
    }
}

Switch模式匹配(Pattern Matching for switch)

在日常的开发中,我们时常需要将一个对象与多个可能值进行匹配,在之前,我们可能会通过以下方式进行比较

// Prior to Java 21
static String formatter(Object obj) {
    String formatted = "unknown";
    if (obj instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (obj instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (obj instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (obj instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}

这样的代码样式受益与JDK 16中引入的模式匹配,但是这远远不够优美,因为我们使用了一种过于普通的结构if-else,这种结构带来的后果就是可能会产生某些隐式的错误,编辑器只会按照顺序来执行,其中的某些代码块可能很少执行,或者甚至没有执行,也就是说这段代码会有O(n)的复杂度,即使很多时候我们只需要O(1)

而现在,在JDK 21中,switch语句允许与模式匹配

// As of Java 21
static String formatterPatternSwitch(Object obj) {
    return switch (obj) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> obj.toString();
    };
}

但是这样会带来新的问题,使用模式匹配会带来更大的结果集范围,比如匹配到了String类型,如果还需要匹配String的值,我们可以使用这样的代码

// As of Java 21
static void testStringOld(String response) {
    switch (response) {
        case null -> { }
        case String s -> {
            if (s.equalsIgnoreCase("YES"))
                System.out.println("You got it");
            else if (s.equalsIgnoreCase("NO"))
                System.out.println("Shame");
            else
                System.out.println("Sorry?");
        }
    }
}

显然,这不够优雅,我们有更好的选择

// As of Java 21
static void testStringNew(String response) {
    switch (response) {
        case null -> { }
        case String s
        when s.equalsIgnoreCase("YES") -> {
            System.out.println("You got it");
        }
        case String s
        when s.equalsIgnoreCase("NO") -> {
            System.out.println("Shame");
        }
        case String s -> {
            System.out.println("Sorry?");
        }
    }
}

或者可以直接给case增加更多的选择

// As of Java 21
static void testStringEnhanced(String response) {
    switch (response) {
        case null -> { }
        case "y", "Y" -> {
            System.out.println("You got it");
        }
        case "n", "N" -> {
            System.out.println("Shame");
        }
        case String s
        when s.equalsIgnoreCase("YES") -> {
            System.out.println("You got it");
        }
        case String s
        when s.equalsIgnoreCase("NO") -> {
            System.out.println("Shame");
        }
        case String s -> {
            System.out.println("Sorry?");
        }
    }
}

虚拟线程(Virtual Threads)

在之前版本中Java的多线程基于的是操作系统线程,而操作系统线程的数量是非常有限的,过高的成本,使我们不能有太多的线程。如果每个请求都消耗一个线程,从而消耗一个操作系统线程,则线程数通常会在其他资源(如 CPU 或网络连接)耗尽之前很久就成为限制因素。即使将线程池化,也会发生这种情况,因为池化虽然有助于避免启动新线程的高成本,但不会增加线程总数

而最新引入的虚拟线程是由JDK提供,你可以把它看作是在操作系统线程基础上创建的“一批”线程,它们有效地共享操作系统线程的资源,从而提升系统利用率,并不受数量限制,由于成本降低,所以虚拟线程不需要池化

参考以下代码,对比虚拟线程与传统线程的区别,使用ThreadMXBean接口获取当前系统线程数

在使用虚拟线程调用10000个线程时,系统级线程的数量为17

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
    System.out.println(ManagementFactory.getThreadMXBean().getThreadCount());
}

在使用传统方法调用10000个线程时,系统级线程的数量为10008

try (var executor = Executors.newCachedThreadPool()) {
    IntStream.range(0, 10000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
    System.out.println(ManagementFactory.getThreadMXBean().getThreadCount());
}

ZGC垃圾回收机制(Generational ZGC)

简单的说就是性能更加好的垃圾回收机制,加入了“分代”。但是在目前版本中默认的垃圾回收机制还是非分代式,想要使用新特征要手动添加启动参数-XX:+UseZGC -XX:+ZGenerational ...,但是在未来的版本中,ZGC将会成为默认的垃圾回收器