目录
概述
变动说明
重要变更和信息
下载地址
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 21
于 2023 年 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: 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 443: Unnamed Patterns and Variables (Preview) 未命名模式和变量 (预览)
-
JEP 444: Virtual Threads 虚拟线程(正式特性)
-
JEP 445: Unnamed Classes and Instance Main Methods (Preview) 未命名类和 main 方法 (预览)
-
JEP 446: Scoped Values (Preview) 作用域值 (预览)
-
JEP 448: Vector API (Sixth Incubator) 向量 API(第六次孵化)
-
JEP 449: Deprecate the Windows 32-bit x86 Port for Removal 弃用 Windows 32 位 x86的移植
-
JEP 451: Prepare to Disallow the Dynamic Loading of Agents 准备禁止动态加载代理
-
JEP 452: Key Encapsulation Mechanism API 密钥封装机制 API 安全库
-
JEP 453: Structured Concurrency (Preview) 结构化并发(预览)
而其中与开发过程中直接相关的特性主要包括:JEP 430(字符串模板(预览特性))、JEP 431(有序集合)、JEP 440(Record 模式)、JEP 441(switch 模式匹配)等。
下载地址
你可以从这个链接下载Oracle JDK
版本,更多版本下载。
也可以从这个链接下载生产就绪的OpenJDK
版本。文件为压缩包,解压并设置环境变量就可以使用。
Java21新特性总结
1、JEP 441: Switch 的模式匹配(正式特性)
JEP 441 (specification/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 表达式主要功能包括:
-
简化的语法:
switch
表达式使用更简洁的箭头语法 (->
)编写,可以直接返回一个值,且不再需要break
语句。 -
多值匹配:每个
case
分支可以同时匹配多个值,使用逗号分隔。 -
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 394
将 instanceof
的模式匹配发布为正式属性。虽然可以不需要强制转换了,但是仍然需要大量的 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
来使用,避免了显式的类型检查和强制类型转换。
举个例子:
@Testpublic 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
表达式来改造下:
@Testpublic 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
选择器表达式只支持特定类型,即基本整型数据类型byte
、short
、char
和int
;对应的装箱形式Byte
、Short
、Character
和Integer
;String
类;枚举类型。现在有了类型模式,Switch
表达式可以是任何类型啦。
null标签
当switch
允许任何引用类型的选择器表达式,那么我们需要留意null
的情况,在Java17
之前,向switch
语句传递一个null
值,会抛出一个NullPointerException
,现在可以通过类型模式,将 null 检查作为一个单独的case标签来处理,如下:
@Testpublic 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
规则右侧出现条件语句。
根据字符串长度判断诗句是五言绝句还是七言绝句,代码如下:
@Testpublic 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);elseSystem.out.printf("不知道是啥:%s%n", s);}}}}
这里的问题是,使用单一模式(即类型)来区分case
就只能判断一种情况。我们只能在模式匹配中再通过if……else
判断来区分不同的情况,来对一个模式的细化。这时,我们可以是使用switch
中的when
子句指定模式case
标签的条件,例如,case String s when if (s.length() == 5)
。表示当类型为String
并且字符串长度为5的时候,我们将这种case
标签称为守卫case
标签,将布尔表达式称为保护。
@Testpublic 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@Testpublic 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);elseSystem.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 模式直接检查和提取 x
和 y
值:
// 创建Record类,As of Java 16record 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 length
和float 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
;嵌套record
是static
的 -
record
可以实现接口 -
使用
new
关键字实例化record
-
您可以在
record
的主体中声明静态方法、静态字段、静态初始值设定项、构造函数、实例方法和嵌套类型 -
可以对
record
和record
的属性进行注释
与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 :继承自
SequencedCollection
和Set
-
SequencedSet
这些接口附带了一些新方法,以提供改进的集合访问和操作功能。
下面让我们看一下使用JDK 21
之前的集合取第一个和最后一个元素的方法:
访问位置 | List | Deque | SortedSet |
---|---|---|---|
取第一个元素 | 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集合。
测试代码:
@Testpublic 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); //菊 竹 梅 松 兰 }@Testpublic 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 444 (core-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
中的goroutines
和Erlang
中的processes
)。虚拟线程可以比传统线程创建更多数量,并且开销要少得多。这使得在自己的线程中运行单独任务或请求变得更加实用,即使在高吞吐量的程序中也是如此。
它的资源分配和调度由VM
实现,而不是操作系统。虚拟线程的主要特点包括:
-
轻量级:与传统线程相比,它更轻量,创建和销毁的成本较低。
-
资源消耗更少:由于不是直接映射到操作系统线程,虚拟线程显著降低了内存和其他资源的消耗。这使得在有限资源下可以创建更多的线程。
-
上下文切换开销更低:由于虚拟线程在用户空间,而不是通过操作系统,所以它的上下文切换开销更低。
-
简化并发编程:由于不受操作系统线程数量的限制,我们可以为每个独立的任务创建一个虚拟线程,简化并发编程模型。
-
提升性能:在 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.startVirtualThread
或 Thread.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::format
和String::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部分:
-
模板处理器:
STR
; -
用
\{}
包裹的嵌入表达式,大括号内是变量名。 -
用
.
(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 usefulDateTimeFormatter.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.73Total 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++)的互操作性。此API
与Foreign-Memory API
(JEP 393)一起,将大大简化绑定到本机库的错误处理过程。
一般Java想要调用本地代码需要使用Java Native Interface (JNI)
,但是JNI
操作比较复杂而且性能有限。
外部函数和内存API
提供了一套更简洁的API
,用于调用本地函数和处理本地内存,降低了复杂性,而且还设计了更多的安全保护措施,降低了内存泄露和应用崩溃的风险。
主要是通过两个组件实现的:
-
Foreign Function Interface (FFI): 允许 Java 代码直接调用非 Java 代码,比如 C/C++ 代码。
-
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!");} }
对于一个入门程序来说,不得不说这段代码有点略显复杂。我们逐行看一下
-
public class HelloWorld
-
public
:这是一个访问修饰符,表示这个类是公开可见的,所有类都可以访问到。 -
class
:这是一个关键字,用来定义一个类。 -
HelloWorld
:这是类的名称。
-
-
public static void main(String[] args)
-
public
:访问修饰符,表示这个方法公开可见。 -
static
:表示静态调用,即不需要实例化对象就可以调用。 -
void
:方法返回值类型,void
表示这个方法不返回任何值。 -
main
:方法名称,main()
是Java程序的执行入口。 -
String[] args
:方法参数,args
是一个字符串数组,可以从命令行接收参数。
-
-
System.out.println("Hello, World!")
-
System
:Java
库中封装好的一个工具类,包含用于标准输入、输出等的方法和变量。 -
out
:System
类的一个静态成员变量(PrintStream
类型),表示标准输出流。 -
println
:PrintStream
类的一个方法,用于输出信息并换行。 -
"Hello, World!"
:输出到控制台的字符串。
-
怎么样,这么一说,是不是感觉还挺复杂的。这还是java
最简单的入门代码。
Java 21增强了启动Java程序的协议,允许实例直接使用main方法。它做了下精简:
-
实例main方法,就意味着可以是
non-static
的。 -
main()
的访问修饰符可以不必是public
的,只需要是non-private
(也即public
,protected
和package-protected
)的即可。 -
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、工具、容器
参考:
-
Java SE 21中移除的API
-
Java SE 21中移除的工具和容器
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-8303018(core-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表情
@Testvoid testEmoji() {String str = "赵钱孙李周吴郑王。大家好😃我是一只程序🙈,🧙•?";System.out.println(str);if (str.codePoints().anyMatch(Character::isEmoji)) {System.out.println("内容中包含表情");}}
(3)String
和java.util.regex.Pattern
中增加splitWithDelimiters()
方法(JDK-8305486)
JDK-8305486 (core-libs/java.lang
)
与split()
方法只返回分割后的字符串不同,splitWithDelimiters()
方法返回字符串和匹配分隔符的交替,而不仅仅是字符串。
示例代码:
@Testvoid 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
标准的更新版本,使GB18030
与Unicode 11.0
版本同步。这个新标准的Charset实现现在已经取代了之前的2000标准。然而,这一新标准与之前的实施相比有一些不兼容的变化。对于那些需要使用旧映射的人,可以使用新的系统属性jdk.charset.GB18030
,将其值设置为2000,可以使用以前JDK版本的GB18030
字符集映射,这些映射基于2000标准。
(6)StringBuilder
和StringBuffer
中新增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 = 2023cavailable 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
可能在double
和 float
返回不同结果(JDK-8300869)
double
和float
通过java.util.Formatter
转换为十进制('e'
, 'E'
, 'f'
, 'g'
, 'G'
)的实现与在与Double.toString(double)
实现保持一致,后者在JDK 19中进行了更改。
因此,在某些特殊情况下,结果可能与早期版本中的结果略有不同。
比如double
值2e23
格式化为%.16e
,新版本中结果为200000000000000e+23
,而早期版本产生的结果为1.9999999999999998e+23
。但是,如果精确到没有这么高(例如%.15e
),那么他们的结果是相同的。
再比如double
值9.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)); }
执行结果
//java212.0E232.0000000000000000e+23//早期版本1.9999999999999998E231.9999999999999998e+23 //这个如果改成%.15e,那么结果是 2.000000000000000e+23
Oracle JDK和OpenJDK之间的差异
尽管官方已经声明了让OpenJDK
和Oracle JDK
二进制文件尽可能接近的目标,但两者之间仍然存在一些差异。
目前的差异是:
-
Oracle JDK
提供了安装程序(msi
、rpm
、deb
等),它们不仅将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 JDK
有Java cup
和steam
图标,而OpenJDK有Duke
图标。 -
Oracle JDK
源代码包括ORACLE PROPRIETARY/CONFIDENTIAL. 使用受许可条款约束
的说明(OTN(Oracle Technology Network License Agreement for Oracle Java SE )协议),OpenJDK源代码包含GPL
协议。