Java21的主要新特性总结

目录

概述

变动说明

重要变更和信息

下载地址

Java21新特性总结

1、JEP 441: Switch 的模式匹配(正式特性)

功能进化

Switch 模式匹配

类型标签

null标签

守卫标签

使用enum常量作值

语法总结

2、JEP 440:Record模式(正式特性)

功能进化

Record历史

一个示例类

紧凑型构造函数

使用限制

与record相关的API

3、JEP 431:有序集合

4、JEP 444:虚拟线程(正式特性)

功能进化

创建和使用虚拟线程

1. 使用静态方法

3. 与ExecutorService结合使用

3. 使用虚拟线程工厂

5、JEP 430:字符串模板 (预览)

基本语法

多行模板表达式

FMT 模版处理器

6、JEP 439:分代 ZGC

功能进化

7、JEP 442:外部函数和内存 API (第三次预览)

功能进化

8、JEP 443:未命名模式和变量 (预览)

功能进化

9、JEP 445:未命名类和 main 方法 (预览)

功能进化

未命名类

10、JEP 446:作用域值 (预览)

功能进化

11、JEP 448:向量 API(第六次孵化)

功能进化

12、JEP 449:弃用 Windows 32 位 x86 的移植

13、JEP 451:准备禁止动态加载代理

14、JEP 452:密钥封装机制 API 安全库

15、JEP 453:结构化并发(预览)

功能进化

16、移除的APIs、工具、容器

17、细微改动

(1)String中增加2个indexOf方法

(2)Emoji表情字符支持(JDK-8303018)

(3)String和java.util.regex.Pattern中增加splitWithDelimiters()方法(JDK-8305486)

(4)java.net.http.HttpClient自动关闭 (JDK-8267140)

(5)支持GB18030-2022编码 (JDK-8301119)

(6)StringBuilder 和StringBuffer中新增repeat()方法 (JDK-8302323)

(7)正则表达式中支持Emoji表情判断 (JDK-8305107)

(8)使用-XshowSettings:locale 查看Tzdata版本 (JDK-8305950)

(9)java.util.Formatter可能在double 和 float返回不同结果(JDK-8300869)

Oracle JDK和OpenJDK之间的差异


概述

JDK 212023 年 9 月 19 日正式发布。该版本是继JDK 17之后最新的长期支持(LTS)版本,将获得至少 8 年的支持。

JEP(Java Enhancement Proposal)Java增强提案

CSR(Compatibility & Specification Review) 兼容性和规范审查

孵化特性:JEP 11 预览特性:JEP 12

变动说明

官网:

Java Platform, Standard Edition Java Language Updates, Release 21

JDK 21 Release Notes, Important Changes, and Information

JDK 21 Features

https://blogs.oracle.com/java/post/the-arrival-of-java-21

JDK 21 Release Notes

更多参考:

JDK 21 Documentation - Home 更多版本:Java Platform, Standard Edition Documentation - Releases

Java Platform, Standard Edition Oracle JDK Migration Guide, Release 21

重要变更和信息

JDK 21 包含 15 个 新特性 ,分别为:

而其中与开发过程中直接相关的特性主要包括:JEP 430(字符串模板(预览特性))、JEP 431(有序集合)、JEP 440(Record 模式)、JEP 441(switch 模式匹配)等。

下载地址

你可以从这个链接下载Oracle JDK版本,更多版本下载

也可以从这个链接下载生产就绪的OpenJDK版本。文件为压缩包,解压并设置环境变量就可以使用。

Java21新特性总结

1、JEP 441: Switch 的模式匹配(正式特性)

JEP 441specification/language

在Java21中,Switch 的模式匹配终于成为一个正式特性。

功能进化

switch的功能进化

java版本特性类型JEP特性
Java 5首次引入,仅支持整型数据类型(如 byte, short, char, 和 int)及枚举类型
Java 7支持 String 类型
Java 12预览特性JEP 325支持Switch表达式(箭头函数)
Java 13预览特性JEP 354加入 yield 语句来替代 break 语句,用于从 switch 表达式返回值
Java 14正式特性JEP 361前2个版本的新特性转为正式特性
Java 17预览特性JEP 406引入Switch的模式匹配作为预览特性
Java 18第二次预览JEP 420调整优化
Java 19第三次预览JEP 427调整优化
Java 20第四次预览JEP 433调整优化
Java 21正式特性JEP 441成为正式特性

Switch 表达式是在 Java 12 中首次作为预览特性引入,而在 Java 13 中对 Switch 表达式做了增强改进:在块中引入了 yield 语句来返回值,而不是使用 break。在Java 14中成为一个标准特性。

Switch 表达式主要功能包括:

  1. 简化的语法switch 表达式使用更简洁的箭头语法 (->)编写,可以直接返回一个值,且不再需要 break 语句。

  2. 多值匹配:每个case分支可以同时匹配多个值,使用逗号分隔。

  3. yield关键字:当使用 switch 表达式处理复杂逻辑时,可以根据情况使用 yield在代码中返回一个值。

示例代码:

 // 旧写法:冗长,切容易出错。漏掉break会穿透到下一条件。
 public static String getTypeOfDay0(String name) {
     String desp;
     switch (name) {
         case "稻":
             desp = "dào,俗称水稻、大米";
             break;
         case "黍":
             desp = "shǔ,俗称黄米";
             break;
         case "稷":
             desp = "jì,又称粟,俗称小米";
             break; // 可以注释掉看看
         case "麦":
             desp = "mài,俗称小麦";
             break;
         case "菽":
         case "豆":
             desp = "shū,俗称大豆";
             break;
         default:
             throw new IllegalStateException("不是五谷之一: " + name);
     }
 ​
     return desp;
 }
 ​
 // java12写法
 public static String getTypeOfDay1(String name) {
     return switch (name) {
         case "稻" -> "dào,俗称水稻、大米";
         case "黍" -> "shǔ,俗称黄米";
         case "稷" -> "jì,又称粟,俗称小米";
         case "麦" -> "mài,俗称小麦";
         case "菽", "豆" -> "shū,俗称大豆";
         default -> {
             throw new IllegalStateException("不是五谷之一: " + name);
         }
     };
 }
 ​
 // java12写法:条件中需要特殊处理的情况,需要在外部单独定义一个变量接收处理值
 public static String getTypeOfDay2_1(String name) {
     // 如果不需要特殊处理,可以直接返回
     String desp;
     switch (name) {
         case "稻" -> desp = "dào,俗称水稻、大米";
         case "黍" -> desp = "shǔ,俗称黄米";
         case "稷" -> desp = "jì,又称粟,俗称小米";
         case "麦" -> desp = "mài,俗称小麦";
         case "菽", "豆" -> desp = "shū,俗称大豆";
         default -> {
             // 处理复杂逻辑
             if (name == null || name.isEmpty()) {
                 desp = "名称为空";
             } else {
                 throw new IllegalStateException("不是五谷之一: " + name);
             }
         }
     }
     return desp;
 }
 ​
 // java13写法,即java14写法
 public static String getTypeOfDay2(String name) {
 ​
     return switch (name) {
         case "稻" -> "dào,俗称水稻、大米";
         case "黍" -> "shǔ,俗称黄米";
         case "稷" -> "jì,又称粟,俗称小米";
         case "麦" -> "mài,俗称小麦";
         case "菽", "豆" -> "shū,俗称大豆";
         default -> {
             // 处理复杂逻辑
             if (name == null || name.isEmpty()) {
                 yield "名称为空";
             } else {
                 throw new IllegalStateException("不是五谷之一: " + name);
             }
         }
     };
 }
 ​
 @Test(priority = 0) // 不指定顺序时默认按字母顺序执行
 public void test() {
     String name = "稷";
     System.out.printf("%s:%s%n", name, getTypeOfDay0(name));
     System.out.printf("%s:%s%n", name, getTypeOfDay1(name));
     System.out.printf("%s:%s%n", name, getTypeOfDay2(name));
 }

在 Java 16 中, JEP 394instanceof的模式匹配发布为正式属性。虽然可以不需要强制转换了,但是仍然需要大量的 if...else。而 Switch 表达式虽然简化了if...else,但是它无法像instanceof一样不需要强制转换。为了解决这个痛点,Java 17引入模式匹配的Switch表达式特性 ,目前该特性为预览特性。

该特性扩展了 switch 表达式和语句,允许它们使用模式匹配,这就意味着我们可以在 switch 的 case 标签中使用模式,如类型模式,使得代码更加灵活和表达性更强。而且也无需进行显式的类型转换了。例如,可以使用 case Integer i 这样的语法来匹配并自动转换类型。

但是,不知道小伙伴们注意没有,Switch 表达式只有一种类型,比如:我有一个诗人类(Poet),它有3个实现类:唐朝诗人(TangPoet)、宋朝诗人(SongPoet)、汉朝诗人(HanPoet),我要根据诗人的类型进行不同处理 :

 Poet poet = ... // 诗人
 ​
 switch (poet.getClass().getName()) {
     case "my.poet.TangPoet":
         TangPoet tp = (TangPoet) obj;
         // 处理唐朝诗人
         break;
     case "my.poet.SongPoet":
         SongPoet sp = (SongPoet) obj;
         // 处理宋朝诗人
         break;
     case "my.poet.HanPoet":
         HanPoet hp = (HanPoet) obj;
         // 处理汉朝诗人
         break;
     // 其他类型
 }

这个强转显然比较麻烦。所以,参考Java 17中,参考instanceof的模式匹配,为switch表达式引入了模式匹配功能作为预览特性。

Switch 模式匹配

在 Java 17 中,switch 表达式允许使用模式匹配来处理对象类型,这样就可以直接在 switch 语句中检查和转换类型,而不需要额外的 if...else 结构和显式类型转换。

case后面可以跟的标签主要有:

  • 类型标签

  • null标签

  • 守卫标签

  • enum或常量值

类型标签

允许在 switch 语句的 case 分支中直接匹配对象的类型。例如,case String s 允许你在该分支中直接作为字符串类型的 s 来使用,避免了显式的类型检查和强制类型转换。

举个例子:

 @Test
 public void switchTest() {
     // 不是用switch根据类型判断
     Object[] objects = { "Hello", "Java", "17", 666, 0.618 };
     for (Object obj : objects) {
         if (obj instanceof Integer v) {
             System.out.printf("为整型 :%s %n", v);
         } else if (obj instanceof Float v) {
             System.out.printf("为Float:%s %n", v);
         } else if (obj instanceof Double v) {
             System.out.printf("为Double:%s %n", v);
         } else if (obj instanceof String v) {
             System.out.printf("为字符串:%s %n", v);
         } else {
             System.out.printf("其他类型:%s %n", obj);
         }
     }
 }

我们用 Switch 表达式来改造下:

 @Test
 public void switchTest() {
     Object[] objects = { "Hello", 123, "World", "Java", 3.14, "skjava" };
     for (Object obj: objects) {
         switch (obj) {
             case Integer v -> System.out.println("为整数型:" + v);
             case Float v -> System.out.println("为浮点型:" + v);
             case Double v -> System.out.println("为双精度浮点数:" + v);
             case String v -> System.out.println("为字符串:" + v);
             default -> System.out.println("其他类型:" + obj);
         }
     }
 }

相比上面的 if...else 简洁了很多。同时在 Java 17之前,Switch选择器表达式只支持特定类型,即基本整型数据类型byteshortcharint;对应的装箱形式ByteShortCharacterIntegerString类;枚举类型。现在有了类型模式,Switch 表达式可以是任何类型啦。

null标签

switch允许任何引用类型的选择器表达式,那么我们需要留意null的情况,在Java17之前,向switch语句传递一个null值,会抛出一个NullPointerException,现在可以通过类型模式,将 null 检查作为一个单独的case标签来处理,如下:

 @Test
 public void switchTest() {
     Object[] objects = { "Hello", "Java", "17", 142857, 0.618 };
     for (Object obj: objects) {
         switch (obj) {
             case Integer v -> System.out.println("为整数型:" + v);
             case Float v -> System.out.println("为浮点型:" + v);
             case Double v -> System.out.println("为双精度浮点数:" + v);
             case String v -> System.out.println("为字符串:" + v);
             case null -> System.out.println("为空值");
             default -> System.out.println("其他类型:" + obj);
         }
     }
 }

case null 可以直接匹配值为 null 的情况。

守卫标签

与匹配常量值的case标签不同,模式case标签可以对应多个变量值。这通常会导致switch规则右侧出现条件语句。

根据字符串长度判断诗句是五言绝句还是七言绝句,代码如下:

 @Test
 public void switchCaseCaseTest() {
     String[] poems = { "千山鸟飞绝", "春城无处不飞花", "红豆生南国", "二月春风似剪刀","念奴娇" };
     for (String poem : poems) {
         switch (poem) {
             case null -> System.out.println("为空值");
             case String s -> {
                 if (s.length() == 5)
                     System.out.printf("五言绝句:%s%n", s);
                 else if (s.length() == 7)
                     System.out.printf("七言绝句:%s%n", s);
                 else
                     System.out.printf("不知道是啥:%s%n", s);
             }
         }
     }
 }

这里的问题是,使用单一模式(即类型)来区分case就只能判断一种情况。我们只能在模式匹配中再通过if……else判断来区分不同的情况,来对一个模式的细化。这时,我们可以是使用switch中的when子句指定模式case标签的条件,例如,case String s when if (s.length() == 5)。表示当类型为String并且字符串长度为5的时候,我们将这种case标签称为守卫case标签,将布尔表达式称为保护。

 @Test
 public void switchCaseCaseTest() {
     String[] poems = { "千山鸟飞绝", "春城无处不飞花", "红豆生南国", "二月春风似剪刀","念奴娇" };
     for (String poem : poems) {
         switch (poem) {
             case null -> System.out.println("为空值");
             case String s when s.length() == 5 -> System.out.printf("五言绝句:%s%n", s);
             case String s when s.length() == 7 -> System.out.printf("七言绝句:%s%n", s);
             case String s -> System.out.printf("不知道是啥:%s%n", s); //剩余情况,仍然走这个
         }
     }
 }

使用守卫标签,我们可以编写更灵活和表达性强的代码。

如果类型确定的情况下,模式匹配可以和常量混合使用,如下:

 // 测试enum
 @Test
 public void switchCaseCaseTest() {
 ​
     String[] poems = { "千山鸟飞绝", "春城无处不飞花", "红豆生南国", "二月春风似剪刀", "念奴娇", "元曲" };
     for (String poem : poems) {
         switch (poem) {
             case null -> System.out.println("为空值");
             // 这里可以使用常量值处理
             case "宋词", "元曲" -> System.out.printf("勿忘我:%s%n", poem);
             case String s -> {
                 if (s.length() == 5)
                     System.out.printf("五言绝句:%s%n", s);
                 else if (s.length() == 7)
                     System.out.printf("七言绝句:%s%n", s);
                 else
                     System.out.printf("不知道是啥:%s%n", s);
             }
         }
         
         switch (poem) {
             case null -> System.out.println("为空值");
             case "宋词", "元曲" -> System.out.printf("勿忘我:%s%n", poem);
             case String s when s.length() == 5 -> System.out.printf("五言绝句:%s%n", s);
             case String s when s.length() == 7 -> System.out.printf("七言绝句:%s%n", s);
             case String s -> System.out.printf("不知道是啥:%s%n", s);
         }
     }
 }

需要注意的是类型确定的时候,可以不使用default语句。

但是如果switch中对象类型是Object类型,则default语句是必须有的。

使用enum常量作值

这里使用中文做变量名只是演示用,正式开发时,我不建议你使用中文做变量名。

// 测试enum
@Test
public void switchEnumTest() {
    WuGu name = WuGu.稷;
    System.out.printf("%s:%s%n", name, getWuguByName(name));
}

public String getWuguByName(WuGu name) {
    return switch (name) {
        case 稻 -> "dào,俗称水稻、大米";
        case 黍 -> "shǔ,俗称黄米";
        case 稷 -> "jì,又称粟,俗称小米";
        case 麦 -> "mài,俗称小麦";
        case 菽, 豆 -> "shū,俗称大豆";
        default -> {
            throw new IllegalStateException("不是五谷之一: " + name);
        }
    };
}
// 定义枚举类
public enum WuGu {
    稻, 黍, 稷, 麦, 菽, 豆;
}
语法总结
switch:
  case CaseConstant { , CaseConstant }[常量值,可以有多个]
  case null [, default] [null或默认处理]
  case Pattern [ Guard ] [模式匹配,可加守护标签]
  default [默认处理]

2、JEP 440:Record模式(正式特性)

JEP 440

功能进化
Java版本特性类型JEP特性
Java 14预览特性JEP 359引入Record类作为预览特性
Java 15预览特性JEP 384修正及优化,语法上同上一版没有区别
Java 16正式特性JEP 395成为正式特性
Java 19预览特性JEP 405引入Record模式匹配作为预览特性
Java 20第二次预览JEP 432调整优化
Java 21正式特性JEP 440成为正式特性

Java 14引入预览特性 Record类提供一种简洁的语法来声明数据载体的不可变对象,主要是为了解决长期以来在Java中定义纯数据载体类时,代码过于繁琐的问题。在 Java 16 中转为正式特性。

模式匹配最初是用在instanceof上,在 Java 14 作为预览特性引入的,为了解决 instanceof 在做类型匹配时需要进行强制类型转换而导致的代码冗余。

Java 20 引入 Record 模式作为预览特性,它允许在instanceof操作中使用记录模式,直接解构和匹配记录中的字段。

比如有一个记录Record Point(int x, int y),可以使用 Record 模式直接检查和提取 xy 值:

 // 创建Record类,As of Java 16
 record Point(int x, int y) {}
 ​
 public class RecordTest {
 ​
     @Test(priority = 0) // 不指定顺序时默认按字母顺序执行
     public void test() {
         Point point = new Point(2, 3);
         System.out.printf("Point类:%s%n", point);
         printSum(point);
     }
 ​
     private void printSum(Object obj) {
         if (obj instanceof Point p) {
             int x = p.x();
             int y = p.y();
             System.out.println("方法1:" + (x + y));
         }
 ​
         if (obj instanceof Point(int x, int y)) {
             System.out.println("方法2:" + (x + y));
         }
     }
 }

可以对比一下方法1和方法2,可以看出,方法2处理类型转换外,更进一步,直接将Record的变量赋值完成了,极大地简化了代码结构。

该特性使Java 模式匹配能力得到进一步扩展。

模式匹配的真正威力在于它可以优雅地缩放以匹配更复杂的对象图。例如,考虑以下声明:

定义好下面4个类:

//As of Java 16
record Point(int x, int y) {
}

enum Color {
    RED, GREEN, BLUE
}

record ColoredPoint(Point point, Color color) {}

record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

测试:

@Test(priority = 0) // 不指定顺序时默认按字母顺序执行
public void test1() {
    ColoredPoint leftPoint = new ColoredPoint(new Point(0, 100), Color.BLUE);
    ColoredPoint rightPoint = new ColoredPoint(new Point(100, 100), Color.GREEN);
    Rectangle rectangle = new Rectangle(leftPoint, rightPoint);
    System.out.printf("Rectangle类:%s%n", rectangle);
    printUpperLeftColoredPoint(rectangle);
}

private void printUpperLeftColoredPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
        System.out.println(ul.color());
    }
}

执行结果:

Rectangle类:Rectangle[upperLeft=ColoredPoint[p=Point[x=0, y=100], c=BLUE], lowerRight=ColoredPoint[p=Point[x=100, y=100], c=GREEN]]
BLUE

ColoredPoint值ul本身就是一个Record类,我们可以进一步分解它。因此,Record模式匹配支持嵌套,这允许Record类里面的组件进一步匹配和分解。我们可以在Record模式匹配中成员Record类,并进行模式匹配,代码如下:

private void printUpperLeftColoredPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
        System.out.println(ul.color());
    }
    // 对ColoredPoint的值进一步分解。同理lr也可以进一步分解
    if (r instanceof Rectangle(ColoredPoint(Point p, Color c), ColoredPoint lr)) {
        System.out.println(c);
    }
}

可以进一步使用var解构属性

private void printUpperLeftColoredPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
        System.out.println(ul.color());
    }
    // ColoredPoint值ul本身就是一个记录值,我们可能需要进一步分解它。同理lr也可以进一步分解
    if (r instanceof Rectangle(ColoredPoint(Point p, Color c), ColoredPoint lr)) {
        System.out.println(c);
    }

    if (r instanceof Rectangle(ColoredPoint(Point(var ulx, var uly), var ulc), var lr)) {
        System.out.printf("左上角:X轴坐标: %s,Y轴坐标: %s,颜色: %s,%n", ulx, uly, ulc);
    }
}

Record历史

JDK14中,引入了一个新类java.lang.Record。这是一种新的类型声明。Records 允许我们以一种简洁的方式定义一个类,我们只需要指定其数据内容。对于每个Record类,Java 都会自动地为其成员变量生成 equals(), hashCode(), toString() 方法,以及所有字段的访问器方法(getter),为什么没有 setter方法呢?因为Record的实例是不可变的,它所有的字段都是 final 的,这就意味着一旦构造了一个Record实例,其状态就不能更改了。

与枚举一样,记录也是类的受限形式。它非常适合作为“数据载体”,即包含不想更改的数据的类,以及只包含最基本的方法(如构造函数和访问器)的类。

与前面介绍的其他预览特性一样,这个预览特性也顺应了减少Java冗余代码的趋势,能帮助开发者编写更精炼的代码。

一个示例类

定义一个长方形类

final class Rectangle implements Shape {
    final double length;
    final double width;
    
    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    
    double length() { return length; }
    double width() { return width; }
}

它具有以下特点:

  • 所有字段都是final

  • 只包含构造器:Rectangle(double length, double width)和2个访问器方法:length()width()

您可以用record表示此类:

record Rectangle(float length, float width) { }

一个record由一个类名称(在本例中为Rectangle)和一个record属性列表(在本示例中为float lengthfloat width)组成。

record会自动生成以下内容:

  • 为每个属性生成一个private final的字段

  • 为每个属性生成一个与组件名相同的访问方法;在本例中,这些方法是Rectangle::length()Rectangle::width()

  • 一个公开的构造函数,参数包括所有属性。构造函数的参数与字段对应。

  • equals()hashCode()方法的实现,如果两个record类型相同并且属性值相等,那么它们是相等的

  • toString()方法的实现,包括所有字段名和他们的值。

紧凑型构造函数

如果你想在record自定义一个构造函数。那么注意,它与普通的类构造函数不同,record的构造函数没有参数列表:这被称为紧凑型构造函数。

例如,下面的record``HelloWorld有一个字段message。它的自定义构造函数调用Objects.requireNonNull(message),如果message字段是用null值初始化的,则抛出NullPointerException。(自定义记录构造函数仍然会初始化所有字段)

record HelloWorld(String message) {
    public HelloWorld {
        java.util.Objects.requireNonNull(message);
    }
}

测试代码:

@Test
public void test() {
    HelloWorld h1 = new HelloWorld(null); // new HelloWorld("天地玄黄宇宙洪荒"); //用这个测试,可以发现字段还是会初始化的
    System.out.println(h1);
}

这个测试代码执行报java.lang.NullPointerException异常。

使用限制

以下是record类使用的限制:

  • Record类不能继承任何类

  • Record类不能声明实例字段(与record组件相对应的 private final字段除外);任何其他声明的字段都必须是静态的

  • Record类不能是抽象的;它是final

  • Record类的成员变量是final

除了这些限制之外,record类的行为类似于常规类:

  • 可以在类中声明record;嵌套recordstatic

  • record可以实现接口

  • 使用new关键字实例化record

  • 您可以在record的主体中声明静态方法、静态字段、静态初始值设定项、构造函数、实例方法和嵌套类型

  • 可以对recordrecord的属性进行注释

record相关的API

java.lang.Class类有2个方法与record相关:

  • RecordComponent[] 返回类型getRecordComponents(): 返回record的所有字段列表。

  • boolean isRecord(): 与isEnum()类似,如果是record则返回true

3、JEP 431:有序集合

JEP 431

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

“生活只能向后理解,但必须向前生活。”——克尔凯郭尔

原文:"Life can only be understood backwards; but it must be lived forwards."— Kierkegaard

它新增了三个新接口:

  • SequencedCollection

  • SequencedMap :继承自SequencedCollectionSet

  • SequencedSet

这些接口附带了一些新方法,以提供改进的集合访问和操作功能。

下面让我们看一下使用JDK 21之前的集合取第一个和最后一个元素的方法:

访问位置ListDequeSortedSet
取第一个元素list.get(0)deque.getFirst()set.first()
取最后一个元素list.get(list.size()-1)deque.getLast()set.last()

三个集合提供了三类不同的使用方法,非常混乱。但在JDK 21之后,访问第一个和最后一个元素就方法多了:

对于List, Deque, Set这些有序的集合,访问方法变得统一起来:

  • 第一个元素:collection.getFirst()

  • 最后一个元素:collection.getLast()

SequencedCollection 接口定义了如下方法:

  • addFirst():将元素添加为此集合的第一个元素。

  • addLast():将元素添加为此集合的最后一个元素。

  • getFirst():获取此集合的第一个元素。

  • getLast():获取此集合的最后一个元素。

  • removeFirst():移除并返回此集合的第一个元素。

  • removeLast():移除并返回此集合的最后一个元素。

  • reversed():倒序此集合。

SequencedMap 接口定义了如下方法:

  • firstEntry():返回此 Map 中的第一个 Entry,如果为空,返回 null。

  • lastEntry():返回此 Map 中的最后一个 Entry,如果为空,返回 null。

  • pollFirstEntry():移除并返回此 Map 中的第一个 Entry。

  • pollLastEntry():移除并返回此 Map 中的最后一个 Entry。

  • putFirst():将 key-value 插入此 Map 中开始位置,如果该 key 已存在则会替换。

  • putLast():将 key-value 插入此 Map 中结尾位置,如果该 key 已存在则会替换。

  • reversed():倒序此Map。

  • sequencedEntrySet():返回此 Map 的 Entry。

  • sequencedKeySet():返回此 Map 的keySet的SequencedSet集合。

  • sequencedValues():返回此 Map 的 value集合的SequencedCollection集合。

测试代码:

 @Test
 public void sequencedCollectionTest() {
     List<String> baseList = List.of("梅", "兰", "竹", "菊", "松");
 ​
     List<String> list = new ArrayList<>(baseList);
     Deque<String> deque = new ArrayDeque<>(baseList); // 队列
     LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>(baseList);
     TreeSet<String> sortedSet = new TreeSet<>(baseList);
     System.out.println("list:" + list); //list:[梅, 兰, 竹, 菊, 松]
     System.out.println("deque:" + deque); //deque:[梅, 兰, 竹, 菊, 松]
     System.out.println("linkedHashSet:" + linkedHashSet); //linkedHashSet:[梅, 兰, 竹, 菊, 松]
     System.out.println("sortedSet:" + sortedSet); //sortedSet:[兰, 松, 梅, 竹, 菊]
 ​
     System.out.println("===== 取第一个元素 =====");
     System.out.println("list.getFirst():" + list.getFirst()); //list.getFirst():梅
     System.out.println("deque.getFirst():" + deque.getFirst()); //deque.getFirst():梅
     System.out.println("linkedHashSet.getFirst():" + linkedHashSet.getFirst()); //linkedHashSet.getFirst():梅
     System.out.println("sortedSet.getFirst():" + sortedSet.getFirst()); //sortedSet.getFirst():兰
 ​
     System.out.println("===== 取最后一个元素 =====");
     System.out.println("list.getLast():" + list.getLast()); //list.getLast():松
     System.out.println("deque.getLast():" + deque.getLast()); //deque.getLast():松
     System.out.println("linkedHashSet.getLast():" + linkedHashSet.getLast()); //linkedHashSet.getLast():松
     System.out.println("sortedSet.getLast():" + sortedSet.getLast()); //sortedSet.getLast():菊
 ​
     Consumer<SequencedCollection<String>> reversedPrint = sequencedCollection -> {
         sequencedCollection.reversed().forEach(x -> System.out.printf("%-2s", x));
         System.out.println();
     };
 ​
     System.out.println("===== 倒序 =====");
     reversedPrint.accept(list); //松 菊 竹 兰 梅 
     reversedPrint.accept(deque); //松 菊 竹 兰 梅 
     reversedPrint.accept(linkedHashSet); //松 菊 竹 兰 梅 
     reversedPrint.accept(sortedSet); //菊 竹 梅 松 兰 
 }
 ​
 @Test
 public void sequencedMapTest() {
     LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
     linkedHashMap.put("诗仙", "李白");
     linkedHashMap.put("诗圣", "杜甫");
     linkedHashMap.put("诗鬼", "李贺");
     linkedHashMap.put("诗魔", "白居易");
     linkedHashMap.put("诗佛", "王维");
     linkedHashMap.put("诗杰", "王勃");
     linkedHashMap.put("诗骨", "陈子昂");
     linkedHashMap.put("诗狂", "王维");
     linkedHashMap.put("诗佛", "贺知章");
     linkedHashMap.put("诗家天子", "王昌龄");
 ​
     Consumer<SequencedMap<String, String>> consumer = sequencedMap -> {
         sequencedMap.forEach((k, v) -> System.out.printf("%s:%-4s", k, v));
         System.out.println();
     };
      // 诗仙:李白  诗圣:杜甫  诗鬼:李贺  诗魔:白居易 诗佛:贺知章 诗杰:王勃  诗骨:陈子昂 诗狂:王维  诗家天子:王昌龄 
     consumer.accept(linkedHashMap);
 ​
     System.out.println("===== 添加到开始 =====");
     linkedHashMap.putFirst("诗神", "陆游");
     // 诗神:陆游  诗仙:李白  诗圣:杜甫  诗鬼:李贺  诗魔:白居易 诗佛:贺知章 诗杰:王勃  诗骨:陈子昂 诗狂:王维  诗家天子:王昌龄 
     consumer.accept(linkedHashMap);
 ​
     System.out.println("===== 添加到最后 =====");
     linkedHashMap.putLast("诗奴", "贾岛");
     // 诗神:陆游  诗仙:李白  诗圣:杜甫  诗鬼:李贺  诗魔:白居易 诗佛:贺知章 诗杰:王勃  诗骨:陈子昂 诗狂:王维  诗家天子:王昌龄 诗奴:贾岛  
     consumer.accept(linkedHashMap);
 ​
     //keys:[诗神, 诗仙, 诗圣, 诗鬼, 诗魔, 诗佛, 诗杰, 诗骨, 诗狂, 诗家天子, 诗奴]
     System.out.printf("keys:%s%n", linkedHashMap.sequencedKeySet());
     //values:[陆游, 李白, 杜甫, 李贺, 白居易, 贺知章, 王勃, 陈子昂, 王维, 王昌龄, 贾岛]
     System.out.printf("values:%s%n", linkedHashMap.sequencedValues());
 }

4、JEP 444:虚拟线程(正式特性)

JEP 444core-libs/java.lang

功能进化
java版本特性类型JEP特性
Java 19预览特性JEP 425引入了虚拟线程作为预览特性
Java 20第二次预览JEP 436优化调整
Java 21正式特性JEP 444作为正式特性发布

Java 19中初次将虚拟线程引入Java平台。虚拟线程是轻量级线程,可以显著减少编写、维护和观察高吞吐量并发应用程序的工作量。Java 21中,虚拟线程作为正式特性发布。

虚拟线程是JDK而不是系统提供的线程的轻量级实现。它们是用户模式线程((user-mode threads))的一种形式,在其他多线程语言中也很成功(例如Go中的goroutinesErlang中的processes)。虚拟线程可以比传统线程创建更多数量,并且开销要少得多。这使得在自己的线程中运行单独任务或请求变得更加实用,即使在高吞吐量的程序中也是如此。

它的资源分配和调度由VM实现,而不是操作系统。虚拟线程的主要特点包括:

  1. 轻量级:与传统线程相比,它更轻量,创建和销毁的成本较低。

  2. 资源消耗更少:由于不是直接映射到操作系统线程,虚拟线程显著降低了内存和其他资源的消耗。这使得在有限资源下可以创建更多的线程。

  3. 上下文切换开销更低:由于虚拟线程在用户空间,而不是通过操作系统,所以它的上下文切换开销更低。

  4. 简化并发编程:由于不受操作系统线程数量的限制,我们可以为每个独立的任务创建一个虚拟线程,简化并发编程模型。

  5. 提升性能:在 I/O 密集型应用中,虚拟线程能够显著地提升性能。而且由于它们的创建和销毁成本低,能够更加高效地利用系统资源。

但是需要注意:

  • 不替代传统线程:虚拟线程不能完全替代传统的操作系统线程,只是一个补充。对于需要密集计算和精细控制线程行为的场景,传统线程仍然是主流。

  • 不适用低延迟场景:虚拟线程主要针对高并发和高吞吐量,而不是低延迟。对于需要极低延迟的应用,传统线程可能是更好的选择。

开发人员可以选择使用虚拟线程还是系统线程。下面是一个创建大量虚拟线程的示例程序。该程序首先获得一个ExecutorService,它将为每个提交的任务创建一个新的虚拟线程。然后,它提交10000个任务,并等待所有任务完成:

@Test
public void test() {
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        IntStream.range(0, 1_0000).forEach(i -> {
            executor.submit(() -> {
                Thread.sleep(Duration.ofSeconds(1));
                return i;
            });
        });
    } // executor.close() is called implicitly, and waits

}

这个例子中的任务是简单的代码——睡眠一秒钟——现代硬件可以轻松地支持10000个虚拟线程同时运行这样的代码。在幕后,JDK在少数操作系统线程上运行代码,可能只有一个线程那么少。

如果这个程序使用为每个任务创建一个新平台线程的ExecutorService,比如Executors.newCachedThreadPool(),情况会大不相同。ExecutorService将尝试创建10000个平台线程,从而创建10000个操作系统线程,程序可能会崩溃,具体取决于机器和操作系统。

如果程序使用从池中获取平台线程的ExecutorService,比如Executors.newFixedThreadPool(200),情况也不会好到哪里去。ExecutorService将创建200个平台线程,供所有10000个任务共享,因此许多任务将按顺序运行,而不是并发运行,程序将需要很长时间才能完成。对于该程序,具有200个平台线程的池只能实现每秒200个任务的吞吐量,而虚拟线程(在足够的预热之后)可以实现每秒约10000个任务的流量。此外,如果示例程序中的1_0000更改为100_0000,则该程序将提交1000000个任务,创建1000000个并发运行的虚拟线程,并(在充分预热后)实现每秒约1000000个任务的吞吐量。

如果这个程序中的任务执行一秒钟的计算(例如,对一个巨大的数组进行排序),而不仅仅是睡眠,那么将线程数量增加到处理器内核数量之外将没有帮助,无论它们是虚拟线程还是平台线程。虚拟线程不是更快的线程——它们运行代码的速度并不比平台线程快。它们的存在是为了提高规模(更高的吞吐量),而不是速度(更低的延迟)。它们可能比平台线程多得多,因此利特尔法则(Little's law),它们能够实现更高吞吐量所需的更高并发性。

创建和使用虚拟线程
1. 使用静态方法
public static Thread startVirtualThread(Runnable task) 

使用Thread.startVirtualThread方法立即启动虚拟线程,要求传入Runnable对象作为参数,具体如下代码:

@Test
public void test() {   
    // 1、使用静态构建器方法
    Thread.startVirtualThread(() -> {
        System.out.println("人之初,性本善");
    });
}

2、使用Thread Builder

也可以使用Thread.ofVirtual()来创建,使用start()方法启动

public static Builder.OfVirtual ofVirtual()

这个方法可以设置一些属性,比如:线程名称、未捕获异常处理器等。具体如下代码:

@Test
public void test() {
     Thread.ofVirtual().name("kevin-virtual-thread")
         .uncaughtExceptionHandler((t, e) -> System.out.println("线程[" + t.getName() + "发生了异常。message:" + e.getMessage()))
         .start(() -> {
             System.out.println("黎明即起,洒扫庭除");
         });
}

也可以定义后先不启动,需要时再手动启动

	var vt = Thread.ofVirtual().name("kevin-virtual-thread")
        .uncaughtExceptionHandler((t, e) -> System.out.println("线程[" + t.getName() + "发生了异常。message:" + e.getMessage()))
        .unstarted(() -> {
            System.out.println("黎明即起,洒扫庭除");
        });
	vt.start();

3. 与ExecutorService结合使用

传统线程使用时,一般使用线程池ExecutorServices而不是直接使用Thread类。虚拟线程也支持线程池,也有对应的ExecutorService来适配。

我们可以使用Executors.newVirtualThreadPerTaskExecutor() 创建一个线程池,该线程池会给每个任务分配一个虚拟线程。这意味着每个提交给线程池的任务都会在自己的虚拟线程上异步执行。

示例:

@Test
public void test1() {

    ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
    for (int i = 0; i < 100; i++) {
        executorService.submit(() -> {
            System.out.println("黎明即起,洒扫庭除");
        });
    }
    // 关闭线程池。它会等待正在执行的任务完成,但不会接受新的任务。如果需要立即停止所有任务,可以使用 shutdownNow()。
    executorService.shutdown();
}

上述代码在try代码块中创建了一个ExecutorServices,用来为每个提交的任务创建虚拟线程。

开发人员习惯常会将应用程序代码从传统的基于线程池的ExecutorService迁移到虚拟线程上。但是,不要忘记,与任何资源池一样,线程池旨在共享昂贵的资源,但虚拟线程并不昂贵,因此它并不需要池化。 直接使用 Thread.startVirtualThreadThread.ofVirtual().start() 来创建和启动虚拟线程可能还更加简单些。

3. 使用虚拟线程工厂

也可以创建一个虚拟线程的工厂ThreadFactory,使用newThread(Runnable r)方法来创建线程:

示例:

@Test
public void test2() {

    ThreadFactory vtFactory = Thread.ofVirtual().name("kevin-vt-test", 0).factory();

    Thread factoryThread = vtFactory.newThread(() -> {
        System.out.println("黎明即起,洒扫庭除");
    });
    factoryThread.start();
}

这段代码创建了一个虚拟线程工厂,创建的虚拟线程名称以kevin-vt-test为前缀、以数字结尾(从0开始累加)的名称。

5、JEP 430:字符串模板 (预览)

JEP 430

这是一个预览功能。

开发人员通常会根据文本和表达式的组合来编写字符串。Java提供了几种字符串组合机制,但不幸的是,它们都有缺点。

  • 使用+号 连接字符串(代码可读性较差):

    String s = x + " plus " + y + " equals " + (x + y);
  • StringBuilder 拼接(比如冗长):

    String s = new StringBuilder()
                     .append(x)
                     .append(" plus ")
                     .append(y)
                     .append(" equals ")
                     .append(x + y)
                     .toString();
  • String::formatString::formatted 会将格式化字符串与参数分开,可能导致参数个数或类型不匹配:

    String s = String.format("%2$d plus %1$d equals %3$d", x, y, x + y);
    String t = "%2$d plus %1$d equals %3$d".formatted(x, y, x + y);
  • java.text.MessageFormat 需要太多的仪式,并且在格式字符串中使用了不熟悉的语法:requires too much ceremony and uses an unfamiliar syntax in the format string:

    MessageFormat mf = new MessageFormat("{0} plus {1} equals {2}");
    String s = mf.format(x, y, x + y);

许多编程语言提供字符串插值作为字符串串联的替代方案。通常,它采用字符串文本的形式,其中包含嵌入的表达式和文本。将表达式嵌入原位意味着读者可以很容易地辨别预期结果。在运行时,嵌入的表达式被替换为它们的值——这些值被称为插值到字符串中。以下是其他语言中的一些插值示例:

C#             $"{x} plus {y} equals {x + y}"
Visual Basic   $"{x} plus {y} equals {x + y}"
Python         f"{x} plus {y} equals {x + y}"
Scala          s"$x plus $y equals ${x + y}"
Groovy         "$x plus $y equals ${x + y}"
Kotlin         "$x plus $y equals ${x + y}"
JavaScript     `${x} plus ${y} equals ${x + y}`
Ruby           "#{x} plus #{y} equals #{x + y}"
Swift          "\(x) plus \(y) equals \(x + y)"

插值有其便利性,但同时也隐藏了一个缺点:很容易构造出可以被其他系统解释的字符串,可能造成注入风险。

包含SQL语句、HTML/XML文档、JSON片段、shell脚本和自然语言文本的字符串都需要根据特定于域的规则进行验证和净化。由于Java编程语言不可能强制执行所有这些规则,所以由开发人员使用插值来验证和净化。

插值对于SQL语句来说尤其危险,因为它可能导致注入攻击。例如,考虑这个带有嵌入表达式${name}的假设Java代码:

String query = "SELECT * FROM Person p WHERE p.last_name = '${name}'";
ResultSet rs = connection.createStatement().executeQuery(query);

如果name 的值如下:

Smith' OR p.last_name <> 'Smith

那么最终生成的sql语句将是这样:

SELECT * FROM Person p WHERE p.last_name = 'Smith' OR p.last_name <> 'Smith'

并且该代码将查询所有行,可能暴露机密信息。使用简单的插值编写查询字符串与使用传统的字符串拼接一样不安全:

String query = "SELECT * FROM Person p WHERE p.last_name = '" + name + "'";

也就是我们常说的,存在SQL注入风险。

总之,如果我们有设计良好的基于模板的字符串编写机制,来提高每个Java程序的可读性和可靠性。与其他编程语言一样,这种功能既能提供插值的好处,又不太容易引入安全漏洞。

String name = "李白";
String info = STR."诗仙是\{name}";
assert info.equals("诗仙是李白");   // true

模板表达式包括3部分:

  1. 模板处理器:STR

  2. \{}包裹的嵌入表达式,大括号内是变量名。

  3. .U+002E)连接上面3部分一种点字符()

基本语法

1、嵌入表达式内部可以使用双引号。

为了帮助重构,可以在嵌入表达式中使用双引号字符,而无需将其转义为\"。这意味着嵌入表达式可以在模板表达式中与在模板表达式外完全相同,从而简化了从字符串拼接(+)到模板表达式的切换。例如:

String filePath = "tmp.dat";
File   file     = new File(filePath);
String old = "The file " + filePath + " " + (file.exists() ? "does" : "does not") + " exist";
String msg = STR."The file \{filePath} \{file.exists() ? "does" : "does not"} exist";
// "The file tmp.dat does exist" or "The file tmp.dat does not exist"

2、无需引入换行符

为了提高可读性,嵌入的表达式可以分布在源文件中的多行中,而无需在结果中引入换行符。嵌入表达式的值在嵌入表达式的位置插入到结果中;然后,该模板被认为与\在同一行上继续。例如

String time = STR."The time is \{
    // The java.time.format package is very useful
    DateTimeFormatter
      .ofPattern("HH:mm:ss")
      .format(LocalTime.now())
} right now";
// "The time is 12:34:56 right now"

3、从左到右计算

字符串模板表达式中嵌入的表达式的数量没有限制。嵌入的表达式是从左到右计算的,就像方法调用表达式中的参数一样。例如

// Embedded expressions can be postfix increment expressions
int index = 0;
String data = STR."\{index++}, \{index++}, \{index++}, \{index++}";
// "0, 1, 2, 3"

4、任何Java表达式都可以用作嵌入表达式,甚至可以用作模板表达式。

对象的方法,数学运算都可以。

// /嵌入式表达式是一个(嵌套的)模板表达式
String[] fruit = { "apples", "oranges", "peaches" };
String s = STR."\{fruit[0]}, \{STR."\{fruit[1]}, \{fruit[2]}"}";
// "apples, oranges, peaches"

这里,模板表达式STR."\{fruit[1]}, \{fruit[2]}"嵌入另一个模板表达式的模板中。由于"\{}比较多,可读性较差,建议格式化为:

String s = STR."\{fruit[0]}, \{
    STR."\{fruit[1]}, \{fruit[2]}"
}";

或者,可以将其重构为单独的模板表达式:

String tmp = STR."\{fruit[1]}, \{fruit[2]}";
String s = STR."\{fruit[0]}, \{tmp}";
多行模板表达式

模板表达式的模板可以跨越多行源代码,使用与文本块类似的语法。(我们在上面看到了一个跨越多行的嵌入表达式,但包含嵌入表达式的模板在逻辑上是一行。)

以下是表示多行HTML文本、JSON文本和区域表的模板表达式的示例:

 String title = "My Web Page";
 String text  = "Hello, world";
 String html = STR."""
         <html>
           <head>
             <title>\{title}</title>
           </head>
           <body>
             <p>\{text}</p>
           </body>
         </html>
         """;
 // """
 // <html>
 //   <head>
 //     <title>My Web Page</title>
 //   </head>
 //   <body>
 //     <p>Hello, world</p>
 //   </body>
 // </html>
 // """
 ​
 String name    = "Joan Smith";
 String phone   = "555-123-4567";
 String address = "1 Maple Drive, Anytown";
 String json = STR."""
     {
         "name":    "\{name}",
         "phone":   "\{phone}",
         "address": "\{address}"
     }
     """;
 // """
 // {
 //     "name":    "Joan Smith",
 //     "phone":   "555-123-4567",
 //     "address": "1 Maple Drive, Anytown"
 // }
 // """
 ​
 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 table = 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()}
     """;
 // """
 // Description  Width  Height  Area
 // Alfa  17.8  31.4     558.92
 // Bravo  9.6  12.4     119.03999999999999
 // Charlie  7.1  11.23     79.733
 // Total 757.693
 // """

FMT 模版处理器

FMT是Java平台中定义的另一个模板处理器。FMT与STR类似,它除了可以执行插值外,还可以对左侧进行格式化处理。格式化符号与java.util.Formatter中定义的符号相同。以下是区域表示例,根据模板中的格式说明符进行整理:

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 table = 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()}
    """;

执行结果

Description     Width    Height     Area
Alfa            17.80    31.40      558.92
Bravo            9.60    12.40      119.04
Charlie          7.10    11.23       79.73
                              Total  757.69

6、JEP 439:分代 ZGC

JEP 439

功能进化
java版本特性类型JEP特性
Java 11预览特性JEP 333引入 ZGC 作为实验特性
Java 15正式特性JEP 377成为正式特性
Java 21正式特性JEP 439引入分代 ZGC

ZGC 是Java 11引入的新的垃圾收集器,经过了多个实验阶段,在 Java 15 终于成为正式特性,已经被证明是一个高效的、可扩展的、低延迟的垃圾收集器。然而,它在处理大量的小对象和短生命周期的对象时效率不是最优的。Java 21 引入分代 ZGC ,通过采用分代收集策略,提高垃圾收集的效率和应用程序的性能。

在 JDK 21 中,通过扩展Z垃圾回收器(ZGC)为年轻对象和老对象维护单独的代,从而提高应用程序性能。这将使得 ZGC 可以更频繁地收集趋于早亡的年轻对象。

7、JEP 442:外部函数和内存 API (第三次预览)

JEP 442 (core-libs)

这是一个预览特性。

功能进化
java版本特性类型JEP特性
Java 14孵化特性JEP 370引入了外部内存访问 API作为孵化特性
Java 15第二次孵化JEP 383优化外部内存访问 API
Java 16孵化特性JEP 389引入了外部链接器 API
Java 16第三次孵化JEP 393功能优化
Java 17孵化特性JEP 412引入了外部函数和内存 API
Java 18第二次孵化JEP 419改进优化
Java 19预览特性JEP 424改进优化
Java 20第二次预览JEP 434改进优化
Java 21第三次预览JEP 442改进优化

外部函数和内存 API 是在 Java 17 中作为孵化器引入的,它提供对本机代码的静态类型的纯Java访问,其主要目的是改善 Java 与本地代码(如 C 或 C++)的互操作性。此APIForeign-Memory API(JEP 393)一起,将大大简化绑定到本机库的错误处理过程。

一般Java想要调用本地代码需要使用Java Native Interface (JNI),但是JNI操作比较复杂而且性能有限。

外部函数和内存API提供了一套更简洁的API,用于调用本地函数和处理本地内存,降低了复杂性,而且还设计了更多的安全保护措施,降低了内存泄露和应用崩溃的风险。

主要是通过两个组件实现的:

  1. Foreign Function Interface (FFI): 允许 Java 代码直接调用非 Java 代码,比如 C/C++ 代码。

  2. Foreign Memory Access API:提供了一种安全的方法来访问不受JVM管理的内存。

8、JEP 443:未命名模式和变量 (预览)

JEP 443

这是一个预览特性。

功能进化
java版本特性类型JEP特性
Java 21预览特性JEP 443引入未命名模式和变量作为预览特性

使用未命名模式和未命名变量来增强Java语言,未命名模式来匹配Record组件而不说明它的类型和名称,未命名变量可以初始化但不能使用。两者都用下划线字符_表示。这是一个预览语言功能。

简而言之,如果您在代码中声明了一个变量,但是不打算使用它。这个时候,就现在可以将其替换为下划线字符_

示例如下:

... instanceof Point(int x, _)
case Point(int x, _)

... instanceof Point(int x, int _)
case Point(int x, int _)

9、JEP 445:未命名类和 main 方法 (预览)

JEP 445

这是一个预览特性。

功能进化
java版本特性类型JEP特性
Java 21预览特性JEP 445引入未命名类和 main 方法作为预览特性

java程序员写的第一个程序一般都是这个

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

对于一个入门程序来说,不得不说这段代码有点略显复杂。我们逐行看一下

  1. public class HelloWorld
    1. public:这是一个访问修饰符,表示这个类是公开可见的,所有类都可以访问到。

    2. class:这是一个关键字,用来定义一个类。

    3. HelloWorld:这是类的名称。

  2. public static void main(String[] args)
    1. public:访问修饰符,表示这个方法公开可见。

    2. static:表示静态调用,即不需要实例化对象就可以调用。

    3. void:方法返回值类型,void表示这个方法不返回任何值。

    4. main:方法名称,main() 是Java程序的执行入口。

    5. String[] args:方法参数,args是一个字符串数组,可以从命令行接收参数。

  3. System.out.println("Hello, World!")
    1. SystemJava库中封装好的一个工具类,包含用于标准输入、输出等的方法和变量。

    2. outSystem类的一个静态成员变量(PrintStream类型),表示标准输出流。

    3. printlnPrintStream类的一个方法,用于输出信息并换行。

    4. "Hello, World!":输出到控制台的字符串。

怎么样,这么一说,是不是感觉还挺复杂的。这还是java最简单的入门代码。

Java 21增强了启动Java程序的协议,允许实例直接使用main方法。它做了下精简:

  1. 实例main方法,就意味着可以是 non-static 的。

  2. main() 的访问修饰符可以不必是 public 的,只需要是 non-private(也即public, protectedpackage-protected)的即可。

  3. main() 中的 String[] args 将是可选传入的。

所以一个 main() 可以精简成这样:

class HelloWorld {
    void main() {
        System.out.println("Hello, World!");
    }
}
未命名类

其次,Java 21还引入未命名的类来使声明隐式,可以写类名,也可以不写类名,上面的代码可以进一步精简:

void main() { 
	System.out.println("Hello, World!");
}

未命名类可以定义变量。也可以定义方法。

String name = "朱子家训";
 
void main() {
    System.out.println(name);
}

或者:

String name = "朱子家训";
 
String getName() { return name; }
 
void main() {
    System.out.println(getName());
}

10、JEP 446:作用域值 (预览)

JEP 446

功能进化
java版本特性类型JEP特性
Java 20孵化特性JEP 429作用域值作为孵化特性引入
Java 21预览特性JEP 446转为预览特性

引入作用域值,使线程内部和线程之间能够共享不可变的数据。它们优先于线程化局部变量,尤其是在使用大量虚拟线程时。

11、JEP 448:向量 API(第六次孵化)

JEP 448 (core-libs)

功能进化
java版本特性类型JEP特性
Java 16第一次孵化JEP 338提供一个平台无关的方式来表达向量计算,能够充分利用现代处理器上的向量硬件指令。
Java 17第二次孵化JEP 414改进
Java 18第三次孵化JEP 417进一步增强
Java 19第四次孵化JEP 426进一步增强
Java 20第五次孵化JEP 438进一步增强
Java 21第六次孵化JEP 448进一步增强

向量 API 是在 Java 16 中作为孵化特性引入的。

引入API来表示向量计算,这些向量计算在运行时可靠地编译为支持的CPU架构上的最佳向量指令,从而实现优于等效标量计算的性能。Java 18 对向量 API 进行了进一步的改进和增强,以更好地利用硬件功能,提高 Java 在数值计算和机器学习等领域的性能。

12、JEP 449:弃用 Windows 32 位 x86 的移植

JEP 449

弃用 Windows 32 位 x86的JDK移植,将来的版本中移除,即在Windows平台上,32位版本的 Java 虚拟机(JVM)将被弃用。

Windows 10是最后一个支持 32 位操作的 Windows 操作系统,将于 2025 年 10 月终止生命周期。虚拟线程在x86-32上也无法实现。

13、JEP 451:准备禁止动态加载代理

JEP 451

在某些情况下动态加载代理可能导致安全问题,比如未经授权的代码执行和权限提升。而且还存在一定的性能影响。为了增强 Java 平台的安全性和稳定性,特别是在处理代理类时,Java 21 准备禁止动态加载代理,未来当代理程序动态加载到正在运行的 JVM 中时,将会发出警告。

14、JEP 452:密钥封装机制 API 安全库

JEP 452

密钥封装是保护密钥不被非法访问的重要手段,在传输和存储密钥时尤其重要。该特性提供了一种标准化的方法来封装(加密)和解封(解密)密钥。

该API 是一种针对密钥封装机制(KEMs)的 API,是一种使用公钥密码学来保护对称密钥的加密技术。

15、JEP 453:结构化并发(预览)

JEP 453

这是一个孵化特性。

功能进化
java版本特性类型JEP特性
Java 19孵化特性JEP 428引入了外部内存访问 API作为孵化特性
Java 20第二次孵化JEP 437改进优化
Java 21预览特性JEP 453转为预览特性

通过引入用于结构化并发的API来简化多线程编程。结构化并发将在不同线程中运行的多个任务视为一个工作单元,从而简化错误处理和消除,提高可靠性,并增强可观察性。

16、移除的APIs、工具、容器

参考:

17、细微改动

(1)String中增加2个indexOf方法

core-libs/java.lang

public int indexOf(String str, int beginIndex, int endIndex)
public int indexOf(int ch, int beginIndex, int endIndex)    

增加了在指定索引位置范围内 character ch, and of String str的方法。

indexOf(int ch, int beginIndex, int endIndex) 方法参考 JDK-8302590, indexOf(String str, int beginIndex, int endIndex) 方法参考 JDK-8303648.

(2)Emoji表情字符支持(JDK-8303018)

JDK-8303018core-libs/java.lang

java.lang.Character类中增加了以下六种新方法,用于获取在emoji表情符号技术标准UTS#51)中定义的emoji符号:

 public static boolean isEmoji(int codePoint)
 public static boolean isEmojiPresentation(int codePoint)
 public static boolean isEmojiModifier(int codePoint)
 public static boolean isEmojiModifierBase(int codePoint)
 public static boolean isEmojiComponent(int codePoint)
 public static boolean isEmojiComponent(int codePoint)

码位(码点),对应编码术语中英文中的code point,指的是一个编码标准中为某个字符设定的数值,具有唯一性与一一对应性。码位只规定了一个字符对应的数值,并没有规定这个数值如何存储,视编码方案不同有不同的存储方式。

可以使用下面方法判断内容中是否包含emoji表情

 @Test
 void testEmoji() {
     String str = "赵钱孙李周吴郑王。大家好😃我是一只程序🙈,🧙•?";
     System.out.println(str);
     if (str.codePoints().anyMatch(Character::isEmoji)) {
         System.out.println("内容中包含表情");
     }
 }
(3)Stringjava.util.regex.Pattern中增加splitWithDelimiters()方法(JDK-8305486)

JDK-8305486core-libs/java.lang

split()方法只返回分割后的字符串不同,splitWithDelimiters()方法返回字符串和匹配分隔符的交替,而不仅仅是字符串。

示例代码:

 @Test
 void test4() {
     var str = "赵钱孙李,周吴郑王。";
     var list = Arrays.asList(str.splitWithDelimiters(",|。", 10));
     System.out.printf("splitWithDelimiters:list共%s条记录,%s%n", list.size(), list);
 ​
     list = Arrays.asList(str.split(",|。", 10));
     System.out.printf("split:list共%s条记录,%s%n", list.size(), list); 
 ​
 }

执行结果:

splitWithDelimiters:list共5条记录,[赵钱孙李, ,, 周吴郑王, 。, ]
split:list共3条记录,[赵钱孙李, 周吴郑王, ]

可以看到splitWithDelimiters返回了分隔符。

(4)java.net.http.HttpClient自动关闭 (JDK-8267140)

增加了以下方法:

  • void close(): 等待提交的请求完成后优雅地关闭客户端。

  • void shutdown(): 等待正在进行的任务完成后关闭,不再接收新任务。

  • void shutdownNow(): 立刻关闭。

  • boolean awaitTermination(Duration duration): 在给定的持续时间内等待客户端终止;如果客户端终止,则返回true,否则返回false。

  • boolean isTerminated(): 如果客户端已终止,则返回true。

(5)支持GB18030-2022编码 (JDK-8301119)

中国国家标准局(CESI)最近发布了GB18030-2022,这是GB18030标准的更新版本,使GB18030Unicode 11.0版本同步。这个新标准的Charset实现现在已经取代了之前的2000标准。然而,这一新标准与之前的实施相比有一些不兼容的变化。对于那些需要使用旧映射的人,可以使用新的系统属性jdk.charset.GB18030,将其值设置为2000,可以使用以前JDK版本的GB18030字符集映射,这些映射基于2000标准。

(6)StringBuilderStringBuffer中新增repeat()方法 (JDK-8302323)
public StringBuilder repeat(int codePoint, int count)
public StringBuilder repeat(CharSequence cs, int count)    

增加了上面2个方法

@Test(priority = 4)
void test4() {
    StringBuilder sb = new StringBuilder("赵钱孙李,周吴郑王。");
    sb.repeat('-', 10); //增加10个-
    sb.repeat(128584, 10); //增加10个🙈
    System.out.println(sb); // 赵钱孙李,周吴郑王。----------🙈🙈🙈🙈🙈🙈🙈🙈🙈🙈
}
(7)正则表达式中支持Emoji表情判断 (JDK-8305107)

JDK-8303018中支持的Emoji表情判断,在正则表达式中也支持判断,使用\p{IsXXX} 结构进行判断。代码如下:

Pattern.compile("\\p{IsEmoji}").matcher("🉐").matches() //true
Pattern.compile("\\p{IsEmoji_Modifier_Base}").matcher("🉐").matches() //false
(8)使用-XshowSettings:locale 查看Tzdata版本 (JDK-8305950)

-XshowSettings启动器选项得到了增强,可以打印使用JDK配置的tzdata版本。tzdata版本显示为local showSettings选项的一部分。

C:\Users\LD_001>java -XshowSettings:locale -version
Locale settings:
    default locale = 中文 (中国)
    default display locale = 中文 (中国)
    default format locale = 中文 (中国)
    tzdata version = 2023c
    available locales = , af, af_NA, af_ZA, af_ZA_#Latn, agq, agq_CM, agq_CM_#Latn,
        ak, ak_GH, ak_GH_#Latn, am, am_ET, am_ET_#Ethi, ann, ann_NG,
…………
        zh__#Hans, zh__#Hant, zu, zu_ZA, zu_ZA_#Latn
java version "21.0.2" 2024-01-16 LTS
Java(TM) SE Runtime Environment (build 21.0.2+13-LTS-58)
Java HotSpot(TM) 64-Bit Server VM (build 21.0.2+13-LTS-58, mixed mode, sharing)
(9)java.util.Formatter可能在doublefloat返回不同结果(JDK-8300869)

doublefloat通过java.util.Formatter转换为十进制('e', 'E', 'f', 'g', 'G')的实现与在与Double.toString(double)实现保持一致,后者在JDK 19中进行了更改。

因此,在某些特殊情况下,结果可能与早期版本中的结果略有不同。

比如double2e23格式化为%.16e,新版本中结果为200000000000000e+23,而早期版本产生的结果为1.9999999999999998e+23。但是,如果精确到没有这么高(例如%.15e),那么他们的结果是相同的。

再比如double9.9e-324通过%.2g格式化,新的结果是9.9e-324,但早期版本生成的结果是1.0e-323

@Test
void test() {
    double d1 = 2e23;
    System.out.println(Double.toString(d1));
    System.out.println(String.format("%.16e", d1)); 
}

执行结果

 //java21
 2.0E23
 2.0000000000000000e+23
 //早期版本
 1.9999999999999998E23
 1.9999999999999998e+23 //这个如果改成%.15e,那么结果是 2.000000000000000e+23

Oracle JDK和OpenJDK之间的差异

尽管官方已经声明了让OpenJDKOracle JDK二进制文件尽可能接近的目标,但两者之间仍然存在一些差异。

目前的差异是:

  • Oracle JDK提供了安装程序(msirpmdeb等),它们不仅将JDK二进制文件放置在系统中,还包含更新规则,在某些情况下还可以处理一些常见的配置,如设置常见的环境变量(如Windows中的JAVA_HOME)和建立文件关联(如使用JAVA启动.jar文件)。OpenJDK仅提供压缩格式(tar.gz.zip)文件。

  • Usage Logging仅在Oracle JDK中可用。

  • Oracle JDK要求使用Java加密扩展(JCE(Java Cryptography Extension ))代码签名证书对第三方加密提供程序进行签名。OpenJDK继续允许使用未签名的第三方加密提供程序。

  • java -version命令输出结果不同。Oracle JDK将输出java并包含LTS。Oracle生成的OpenJDK将显示OpenJDK,不包括Oracle特定的LTS标识符。

  • Oracle JDK 17及之后的版本在Oracle No-Fee Terms and Conditions License协议下发布。OpenJDK将在GPLv2wCP下发布,并将包括GPL许可证。因此,许可证文件是不同的。

  • Oracle JDK将在FreeType许可证下分发FreeType,而OpenJDK则将在GPLv2下分发。因此,\legal\java.desktop\freetype.md的内容将有所不同。

  • Oracle JDKJava cupsteam图标,而OpenJDK有Duke图标。

  • Oracle JDK源代码包括ORACLE PROPRIETARY/CONFIDENTIAL. 使用受许可条款约束的说明(OTN(Oracle Technology Network License Agreement for Oracle Java SE )协议),OpenJDK源代码包含GPL协议。

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值