JDK 14新特性预览
JDK 14一共发行了16个JEP(JDK Enhancement Proposals,JDK 增强提案),即是筛选出的JDK 14新特性。
- 305: instanceof 的模式匹配 (预览,预览版本意味着暂时可以先尝试)
- 343: 打包工具 (Incubator)
- 345: G1的NUMA内存分配优化
- 349: JFR事件流
- 352: 非原子性的字节缓冲区映射
- 358: 友好的空指针异常
- 359: Records (预览)
- 361: Switch表达式 (标准)
- 362: 弃用Solaris和SPARC端口
- 363: 移除CMS(Concurrent Mark Sweep)垃圾收集器
- 364: macOS系统上的ZGC
- 365: Windows系统上的ZGC
- 366: 弃用ParallelScavenge + SerialOld GC组合
- 367: 移除Pack200 Tools 和 API
- 368: 文本块 (第二个预览版)
- 370: 外部存储器API (Incubator)
JEP 305: instanceof的模式匹配 (预览)
305: Pattern Matching for instanceof (Preview)
引入
JEP 305新增了使instanceof运算符具有模式匹配的能力。模式匹配能够使程序的通用逻辑更加简洁,代码更加简单,同时在做类型判断和类型转换的时候也更加安全。
设计初衷
几乎每个程序员都见过如下代码,在包含判断表达式是否具有某种类型的逻辑时,程序会对不同类型进行不同的处理。
熟悉的instanceof-and-cast用法:
// 在方法的入口接收一个对象
public void beforeWay(Object obj) {
// 通过instanceof判断obj对象的真实数据类型是否是String类型
if (obj instanceof String) {
// 如果进来了,说明obj的类型是String类型,直接进行强制类型转换。
String str = (String) obj;
// 输出字符串的长度
System.out.println(str.length());
}
}
这段程序做了3件事:
- 先判断obj的真实数据类型
- 判断成立后进行了强制类型转换(将对象obj强制类型转换为String)
- 声明一个新的本地变量str,指向上面的obj
这种模式的逻辑并不复杂,并且几乎所有Java程序员都可以理解。但是出于以下原因,上述做法并不是最理想的:
- 语法臃肿乏味
- 同时执行类型检测校验和类型转换。
- String类型在程序中出现了3次,但是最终要的可能只是一个字符串类型的对象变量而已。
- 重复的代码过多,冗余度较高。
JDK 14提供了新的解决方案:新的instanceof模式匹配 ,新的模式匹配的用法如下所示,在instanceof的类型之后添加了变量str。如果instanceof对obj的类型检查通过,obj会被转换成str表示的String类型。在新的用法中,String类型仅出现一次。
public void patternMatching(Object obj) {
if (obj instanceof String str) {
// can use str here
System.out.println(str.length());
} else {
// can't use str here
}
}
上述代码需要注意:
如果obj是String的实例,则将其强制转换为String并分配给绑定变量s。绑定变量在if语句的true块中,而不在if语句的false块中。绑定的变量作用域为if语句内部,并不在false语句块内。
if (obj instanceof String s) {
// 使用s
} else {
// 不能使用s
}
与局部变量的范围不同,绑定变量的范围由包含的表达式和语句的语义确定。
接下来我们看一下模式匹配帮助我们简化案例的经典做法:
通常equals()方法的实现都会先检查目标对象的类型。instanceof的模式匹配可以简化equals()方法的实现逻辑。下面代码中的Student类展示了相关的用法。
public class Student {
private String name ;
public Student(String name) {
this.name = name;
}
// @Override
// public boolean equals(Object o) {
// if (this == o) return true;
// if (o == null || getClass() != o.getClass()) return false;
// Student student = (Student) o;
// return Objects.equals(name, student.name);
// }
// 简化后做法!
@Override
public boolean equals(Object obj) {
return (obj instanceof Student s) && Objects.equals(this.name, s.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
JEP 361: Switch表达式 (标准)
361: Switch Expressions (Standard)
引入
扩展switch分支选择语句的写法。Switch表达式在经过JDK 12 和JDK 13的预览之后,在JDK 14中已经稳定可用。
设计初衷
Java的switch语句是一个变化较大的语法(可能是因为Java的switch语句一直不够强大、熟悉swift或者js语言的同学可与swift的switch语句对比一下,就会发现Java的switch相对较弱),因为Java的很多版本都在不断地改进switch语句:JDK 12扩展了switch语句,使其可以用作语句或者表达式,并且传统的和扩展的简化版switch都可以使用。
JDK 12对于switch的增强主要在于简化书写形式,提升功能点
- 从Java 5+开始,Java的switch语句可使用枚举了。
- 从Java 7+开始,Java的switch语句支持使用String类型的变量和表达式了。
- 从Java 11+开始,Java的switch语句会自动对省略break导致的贯穿提示警告。
- 但从JDK12开始,Java的switch语句有了很大程度的增强。
- JDK 14的该JEP是从JEP 325和JEP 354演变而来的。但是,此JEP 361 Switch表达式 (标准)是独立的,并且不依赖于这两个JEP。
以前的switch程序
public class Demo{
public static void main(String[] args){
// 声明变量score,并为其赋值为'C'
var score = 'C';
// 执行switch分支语句
switch (score) {
case 'A':
System.out.println("优秀");
break;
case 'B':
System.out.println("良好");
break;
case 'C':
System.out.println("中");
break;
case 'D':
System.out.println("及格");
break;
case 'E':
System.out.println("不及格");
break;
default:
System.out.println("数据非法!");
}
}
}
这是经典的Java 11以前的switch写法 ,这里不能忘记写break,否则switch就会贯穿、导致程序出现错误(JDK 11会提示警告)。
JDK 14不需要break了
在JDK 12之前如果switch忘记写break将导致贯穿,在JDK 12对switch的这一贯穿性做了改进。你只要将case后面的冒号(:)改成箭头,那么你即使不写break也不会贯穿了,因此上面程序可改写如下形式:
public class Demo{
public static void main(String[] args){
// 声明变量score,并为其赋值为'C'
var score = 'C';
// 执行switch分支语句
switch (score){
case 'A' -> System.out.println("优秀");
case 'B' -> System.out.println("良好");
case 'C' -> System.out.println("中");
case 'D' -> System.out.println("及格");
case 'E' -> System.out.println("不及格");
default -> System.out.println("成绩数据非法!");
}
}
}
JDK 14的switch表达式
JDK 12之后的switch甚至可作为表达式了——不再是单独的语句。
public class Demo {
public static void main(String[] args) {
// 声明变量score,并为其赋值为'C'
var score = 'C';
// 执行switch分支语句
String s = switch (score)
{
case 'A' -> "优秀";
case 'B' -> "良好";
case 'C' -> "中";
case 'D' -> "及格";
case 'F' -> "不及格";
default -> "成绩输入错误";
};
System.out.println(s);
}
}
上面程序直接将switch表达式的值赋值给s变量,这样switch不再是一个语句,而是一个表达式.。
JDK 14中switch的多值匹配
当你把switch中的case后的冒号改为箭头之后,此时switch就不会贯穿了,但在某些情况下,程序本来就希望贯穿比如我就希望两个case共用一个执行体!JDK 12之后的switch中的case也支持多值匹配,这样程序就变得更加简洁了。
public class Demo{
public static void main(String[] args){
// 声明变量score,并为其赋值为'C'
var score = 'C';
// 执行switch分支语句
String s = switch (score)
{
case 'A', 'B' -> "上等";
case 'C' -> "中等";
case 'D', 'E' -> "下等";
default -> "成绩数据输入非法!";
};
System.out.println(s);
}
}
JDK 14的Yielding a value
当使用箭头标签时,箭头标签右边可以是表达式、throw语句或是代码块。如果是代码块,需要使用yield语句来返回值。下面代码中的print方法中的default语句的右边是一个代码块。在代码块中使用yield来返回值。,JDK 14引入了一个新的yield语句来产生一个值,该值成为封闭的switch表达式的值。
public class Demo{
public static void main(String[] args){
// 声明变量score,并为其赋值为'C'
var score = 'C';
String result = switch (score) {
case 'A', 'B' -> "上等";
case 'C' -> "中等";
case 'D', 'E' -> "下等";
default -> {
if (score > 100) {
yield "数据不能超过100";
} else {
yield score + "此分数低于0分";
}
}
};
System.out.println(result);
}
}
在switch表达式中不能使用break。switch表达式的每个标签都必须产生一个值,或者抛出异常。switch表达式必须穷尽所有可能的值。这意味着通常需要一个default语句。一个例外是枚举类型。如果穷尽了枚举类型的所有可能值,则不需要使用default。在这种情况下,编译器会自动生成一个default语句。这是因为枚举类型中的枚举值可能发生变化。比如,枚举类型Color 中原来只有3个值:RED、GREEN和BLUE。使用该枚举类型的switch表达式穷尽了3种情况并完成编译。之后Color中增加了一个新的值YELLOW,当用这个新的值调用之前的代码时,由于不能匹配已有的值,编译器产生的default会被调用,告知枚举类型发生改变。
JEP 368: Text Blocks(二次预览)
368: Text Blocks (Second Preview)
引入
在Java中,在字符串文字中嵌入HTML,XML,SQL或JSON片段"…"通常需要先进行转义和串联的大量编辑,然后才能编译包含该片段的代码。该代码段通常难以阅读且难以维护,因此,如果具有一种语言学机制,可以比多行文字更直观地表示字符串,而且可以跨越多行,而且不会出现转义的视觉混乱,那么这将提高广泛Java类程序的可读性和可写性。从JDK 13到JDK 14开始文本块新特性的提出,提高了Java程序书写大段字符串文本的可读性和方便性。
设计初衷
JEP 368:文本块(Text Blocks,第二次预览版)— 文本块作为预览特性首次引入Java 13后收到了众多最终用户的反馈。现在,文本块得到了增强,再次作为预览特性出现在Java 14中,目标成为未来JDK版本的标准特性。使用文本块可以轻松表达跨多行源代码的字符串。它提高了Java程序中以非Java语言编写的代码的字符串的可读性;它约定,任何新构造的文本块都可以用字符串相同的字符集表示,解释相同的转义序列并以与字符串相同的方式进行操作。
HTML示例
使用“一维”字符串文字
String html = "<html>\n" +
" <body>\n" +
" <p>Hello, world</p>\n" +
" </body>\n" +
"</html>\n";
使用“二维”文本块
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
System.out.println("""
Hello,
Java
text blocks!
""");
文本块是Java语言的新语法,可以用来表示任何字符串,具有更高的表达能力和更少的复杂度。文本块的开头定界符是由三个双引号 “”" 开始,从新的一行开始字符串的内容。这里的新起的这行不属于字符串,只表示内容开始,是语法的一部分。以 “”" 结束。 “”" 可以紧跟字符串内容,也可以另起一行。另起一行时,字符串内容最后会留有一新行。
"""
line 1
line 2
line 3
"""
等效于字符串文字:
"line 1\nline 2\nline 3\n"
或字符串文字的串联:
"line 1\n" +
"line 2\n" +
"line 3\n"
如果在字符串的末尾不需要行终止符,则可以将结束定界符放在内容的最后一行。例如,文本块:
"""
line 1
line 2
line 3"""
等效于字符串文字:
"line 1\nline 2\nline 3"
文本块可以表示空字符串,尽管不建议这样做,因为它需要两行源代码:
String empty = """
""";
以下是一些格式错误的文本块的示例:
String a = """"""; // no line terminator after opening delimiter
String b = """ """; // no line terminator after opening delimiter
String c = """
"; // no closing delimiter (text block continues to EOF)
String d = """
abc \ def
"""; // unescaped backslash (see below for escape processing)
HTML
使用原始字符串语法:
String html = "<html>\n" +
" <body>\n" +
" <p>Hello, world</p>\n" +
" </body>\n" +
"</html>\n";
使用文本块文本块语法:
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
SQL
使用原始的字符串语法:
String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
"WHERE `CITY` = 'INDIANAPOLIS'\n" +
"ORDER BY `EMP_ID`, `LAST_NAME`;\n";
使用文本块语法:
String query = """
SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
WHERE `CITY` = 'INDIANAPOLIS'
ORDER BY `EMP_ID`, `LAST_NAME`;
""";
多语言示例
使用原始的字符串语法:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("function hello() {\n" +
" print('\"Hello, world\"');\n" +
"}\n" +
"\n" +
"hello();\n");
使用文本块语法:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("""
function hello() {
print('"Hello, world"');
}
hello();
""");
缩进
java编译器会自动删除不需要的缩进:
- 每行结尾的空格都会删除
- 每行开始的共有的空格会自动删除
- 只保留相对缩进
public class Demo{
public static void main(String[] args){
// 1、自动删除不必要的缩进。
System.out.println("""
hello
java
text
"""
);
// 2、会保留相对缩进
System.out.println("""
hello
java
text
"""
);
// 3.整体索进
System.out.println("""
hello
java
text
"""
);
// 4、结束行符在最后面的右边是无效的
System.out.println("""
hello
java
text
"""
);
}
}
JEP 358: 友好的空指针异常
358: Helpful NullPointerExceptions
引入
NullPointerException是Java开发中经常会遇到的异常。在JDK 14之前的版本中,NullPointerException异常的消息只是简单的null,并不会告诉你任何有用的信息,只能根据异常产生的源文件的行号来查找。对于很长的引用链来说,很难定位到底是哪个对象为null。比如,类似a.b.c.d这样的引用方式,a、b和c中的任何一个为null,都会出现NullPointerException异常。仅靠行号无法快速定位问题所在。
在下面的代码中,对p.address.go();的调用会出现NullPointerException异常。
public class Demo {
public static void main(String[] args) {
People p = new People();
p.address.go();
}
}
class People{
Address address;
}
class Address{
public void go(){
System.out.println("我们出去旅游吧");
}
}
直接运行该文件的错误信息如下所示,从中我们只可以知道错误出现在6行,但是并不清楚错误的具体对象是谁为null
Exception in thread "main" java.lang.NullPointerException
at cn.com.javakf.Demo.main(Demo.java:6)
详解
JEP 358增强了对NullPointerException异常的处理,可以显示详细的信息。这个功能需要通过选项-XX:+ShowCodeDetailsInExceptionMessages
启用,如下所示:
java -XX:+ShowCodeDetailsInExceptionMessages Demo
输出的信息如下所示。错误消息明确的指出了a为null。
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Address.go()" because "<local1>.address" is null
at Demo.main(Demo.java:4)
JEP 359: Records记录类型 (预览)
359: Records (Preview)
通过record增强Java编程语言。record提供了一种紧凑的语法来声明类,这些类是浅层不可变数据的透明持有者。
动机
我们经常听到这样的抱怨:“Java太冗长”、“Java规则过多”。首当其冲的就是充当简单集合的“数据载体”的类。为了写一个数据类,开发人员必须编写许多低价值、重复且容易出错的代码:构造函数、访问器、equals()、hashCode()和toString()等等。
尽管IDE可以帮助开发人员编写数据载体类的绝大多数编码,但是这些代码仍然冗长。
传统数据类 我们先来看看现在我们如何声明一个数据类:
package cn.com.javakf;
import java.util.Objects;
public class People {
private String name;
private int age;
public People(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
People people = (People) o;
return age == people.age &&
Objects.equals(name, people.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
我们来看看这个类的特点:
没有无参构造方法,需要初始化时对成员变量赋值 成员变量只有 getter 方法。 覆写了 超类 Object 的 equals 、hashCode、toString 方法。 虽然我们可以借助于第三方框架或者 IDE 很容易编写这些样板代码,但是总归要写这些样板代码不是吗?
从表面上看,将Record是为了简化模板编码而生的,但是它还有“远大”的目标:modeling data as data(将数据建模为数据)。record应该更简单、简洁、数据不可变。
详解
record是Java的一种新的类型。同枚举一样,record也是对类的一种限制。record放弃了类通常享有的特性:将API和表示解耦。但是作为回报,record使数据类变得非常简洁。
一个record具有名称和状态描述。状态描述声明了record的组成部分。例如:
record People(String name, int age) { }
生成的.class反编译得到
Compiled from "Demo.java"
final class People extends java.lang.Record {
public People(java.lang.String, int);
public java.lang.String toString();
public final int hashCode();
public final boolean equals(java.lang.Object);
public java.lang.String name();
public int age();
}
因为record在语义上是数据的简单透明持有者,所以记录会自动获取很多标准成员:
- 状态声明中的每个成员,都有一个 private final的字段;
- 状态声明中的每个组件的公共读取访问方法,该方法和组件具有相同的名字;
- 一个公共的构造函数,其签名与状态声明相同;
- equals和hashCode的实现;
- toString的实现。
限制
records不能扩展任何类,并且不能声明私有字段以外的实例字段。声明的任何其他字段都必须是静态的。
records类都是隐含的final类,并且不能是抽象类。这些限制使得records的API仅由其状态描述定义,并且以后不能被其他类实现或继承。
public class Demo {
public static void main(String[] args){
// 1、判断记录类型中是否存在无参数构造器 :没有无参数构造器的,存在有参数构造器
People p = new People("java",10);
// 2、重写了toString()方法
System.out.println(p);
// 3、判断是否存在set方法:没有提供
// p.setName("c++");
// 4、提供了一些获取内容的方法,这些方法的名称与成员名称一致
System.out.println(p.name());
System.out.println(p.age());
}
}
// JDK 14中的记录类型
record People(String name , int age){}
在record中额外声明变量
也可以显式声明从状态描述自动派生的任何成员。可以在没有正式参数列表的情况下声明构造函数(这种情况下,假定与状态描述相同),并且在正常构造函数主体正常完成时调用隐式初始化(this.x=x)。这样就可以在显式构造函数中仅执行其参数的验证等逻辑,并省略字段的初始化,例如:
public class Demo{
public static void main(String[] args){
Test t = new Test(10 , 2);
}
}
record Test(int x , int y){
//不允许直接定义实例成员变量。 可以定义静态成员变量。
public static String name ;
//可以直接定义不带参的构造器形式用于增强已有有参构造器的代码形式
public Test{
if(x > y){
System.out.println("第一个参数值大于第二个参数值!");
}
}
//可以定义实例和静态方法的。
public void test(){
}
public static void test2(){
}
}