更多学习资料,可以关注下方微信公众号,回复关键字:Java 高级资料
即可免费获取完整的 Java 高级开发工程师视频资料。
注意
从哪几个方面学习新特性
关注 语法层面(学习其他语言的语法)
关注 API 层面(新增 / 过时 / 废弃 )
底层优化(JVM 优化) 面试 / 了解底层原理
JDK 版本使用者占比
根据国外网站 2019 年统计,使用 Java 8 的开发者比重最大,其次就是 Java 11。其他版本的比重不大。这主要是由于 Java 8 和 Java 11 是 LTS 版本。其他的都是非 LTS 版本,意思就是其他版本 Oracle 不会一直维护。
版本更新时间
JDK14 概述
2020 年 3 月 17 日,Java 14 正式 GA(General Available)。这是自从 Java 宣布采用六个月一次的发布周期后的第五次发布。
此次发布的版本包含 16 个 JEP(Java Enhancement Proposals,JDK 增强提案)。包括两个孵化器模块丶三个预览特性丶两个废弃功能丶两个删除功能。
总特性概述
中文版本
英文版本
实用特性
JDK 14 的新特性中,对于语法层面的只有 5 个。所以下面将对这 5 个新特性进行详细讲解。
JEP 305:instanceof 的模式匹配(预览)
这个特性目前在 JDK 14 中还是预览版本。通过为开发人员提供 模式匹配 来增强 Java 编程语言 instanceof。模式匹配使程序中的通用逻辑(如从对象中有条件的提取组件)更加简洁安全。
几乎所有程序都会包含一种特殊逻辑,就是先用表达式判断是否满足某种条件。然后有条件的做进一步处理。如:instanceof 表达式判断。
Object obj = "string";
if (obj instanceof String) {
String s = (String) obj;
// 使用 s 对象
System.out.println(s);
}
复制代码
这一段代码需要处理三件事情:
JEP 305 写法
Object obj = "String";
if (obj instanceof String s) {
// 使用 s 对象
System.out.println(s);
} else {
// 不可以使用 s 对象
}
复制代码
如果 obj 是一个 String 实例,那么它被转换为 String 并分配给 String 类型变量 s 。绑定变量在 if 语句的 true 代码块中,而不是在 if 语句的 false 代码块中。
与局部变量的作用范围不同,绑定变量的作用域由包含的表达式和语句的语义决定。例如下面的代码中
Object obj = "String";
if (!(obj instanceof String s)) {
// 报错,不可以使用 s
System.out.println(s.trim());
} else {
// 可以使用 s
System.out.println(s.trim());
}
// 复杂的判断条件写法
Object obj = "String";
// 由于obj instanceof String s条件成立,那么这个if域就可以使用s变量,所以&&条件可以正常
if (obj instanceof String s && s.length() == 5) {
System.out.println("obj对象是String类型,并且长度为:5");
}
// 错误的判断条件写法
Object obj = "String";
// 如果obj instanceof String s条件不成立,那么if域就不可以使用s变量,s.length() == 5中不可以继续使用s变量。变量s不在||右侧的范围内。
if (obj instanceof String s || s.length() == 5) {
System.out.println("obj对象是String类型,并且长度为:5");
}
// 类型转换和条件判断,简化代码
public boolean strNotBlank(Object obj) {
return obj instanceof String s && s.length() > 0;
}
复制代码
JEP 358:有用的 NullPointerExceptions
官方描述
目标
向开发人员和支持人员提供有关程序提前终止的有用信息。
通过更清晰地将动态异常与静态程序代码关联起来,提高对程序的理解。
减少新开发人员对 NullPointerException 的混淆和关注。
任何一个 Java 开发人员都会遇到 NullPointerException(NPE)。由于 NPE 几乎会出现在程序中的任何位置,因此尝试捕捉和恢复它们通常都不切实际。
即使开发人员依赖 JVM 查明 NPE 的实际来源。但是对于复杂的代码,不适用调试器就无法确定那个变量为空。假设下面的代码出现一个 NPE:
Object obj = null;
System.out.println(obj.toString().trim().contains(""));
复制代码
JVM 将打印出导致 NPE 的方法丶文件名和行号:
java.lang.NullPointerException
at com.zhichunqiu.features.java14.Features02.test(Features02.java:16)
复制代码
文件名称和行号无法精准的定位到那个变量丶方法为空。obj
丶obj.toString()
还是obj.toString().trim()
。
如果 JVM 可以提供所需的信息以查明 NPE 的来源,然后确定其根本原因,而无需使用额外的工具或改组代码,则整个 Java 生态系统都将受益。自 2006 年以来,SAP 的商业 JVM 就已经做到了这一点,获得了开发人员和支持工程师的一致好评。
JDK 14 优化 JVM 处理 NPE
JVM NullPointerException
在程序试图取消引用引用的位置处抛出(NPE)null
。通过分析程序的字节码指令,JVM 将精确地确定哪个变量是null
,并在 NPE 中用空细节消息描述该变量(根据源代码)。然后,null-detail 消息将显示在 JVM 的消息中,以及方法,文件名和行号。
注意
在 JDK 14 中并没有默认开启 Helpful NullPointerException 特性。需要自己配置 JVM 参数,可能以后的版本会默认开启。开启参数: -XX:+ShowCodeDetailsInExceptionMessages
还是用上面的代码执行,看报错效果:
Object obj = null;
System.out.println(obj.toString().trim().contains(""));
复制代码
JVM 消息将剖析该语句并通过显示导致以下内容的完整访问路径来查明原因null
:
java.lang.NullPointerException: Cannot invoke "Object.toString()" because "obj" is null
at com.zhichunqiu.features.java14.Features02.test(Features02.java:16)
复制代码
JVM 打印的消息除了说明 NPE 产生的类丶方法还说明了是 obj 为空导致产生了 NPE。
JEP 359:Records(预览)
通过Records
增强 Java 编程语言。Records 提供了一种紧凑的语法来声明类,这些类都是浅拷贝不可变数据。
动机和目标
早在 2019 年 2 月份,Java 语言架构师 Brian Goetz 曾经写过一篇文章,详细的说明并吐槽了 Java 语言,他和许多程序员一样抱怨"Java 太罗嗦太繁琐"。他提到开发人员想要创建纯数据载体类,通常都必须编写大量低价值丶重复丶容易出错的代码。比如:构造函数丶 getter / setter 丶 equals()丶 hashCode()丶 toString()等方法。
官方说明如下
描述
Records 是 Java 语言中一种新的类型声明。就像enum
一样,record
是一种受限制的Class
形式。record
的引入获得了极大程度的语言简洁性,但是也没有了类的自由度:将 API 与表示分离的能力。
record
是包含名称和状态描述的,这个状态描述是声明record
组件的。record
的主体是可选的。record
的定义如下:
public record Features03(String name, int age) { }
复制代码
隐藏特征
由于records
的声明非常简单,所以records
将会默认的获得很多标准的成员变量或方法。
每一个组成部分(变量 )都有一个隐藏的final
字段声明;
每个成员变量都有一个与变量名称相同的获取值的方法;例如:name
属性的获取值方法不是instance.getName()
而是instance.name()
方法。这些都是编译后默认生成的。
一个public
的全参构造方法,构造方法的参数和顺序与类名称后面的()
中声明的一致。
实现了equals
和hashCode
方法。如果两个records
类型的类具有相同的类型和相同的状态,那么他们一定是相等。(其实就是说明实现的equals
和hashCode
方法是标准的)
实现了toString
方法,会打印类名称和相关的属性说明。(其实就是说明实现的toString
方法是标准的)
反编译后的 Features03 解读
// 特点1:record定义的类,默认都继承了Record,由于Java是单继承,所以record定义的类不允许在继承其他类。
// 错误示范:public record Features03 extends Object(String name, int age) { }
// 特点2:record都是final类,不允许被继承。
public final class Features03 extends java.lang.Record {
// 特点3:所有属性都是final类型的
private final java.lang.String name;
private final int age;
// 特点4:默认生成全参数构造函数
public Features03(java.lang.String name, int age) { /* compiled code */ }
// 特点5:默认生成toString hashCode equals方法
public java.lang.String toString() { /* compiled code */ }
public final int hashCode() { /* compiled code */ }
public final boolean equals(java.lang.Object o) { /* compiled code */ }
// 特点6:所有字段都提供getter方法,但是方法名称和属性名称一致
public java.lang.String name() { /* compiled code */ }
public int age() { /* compiled code */ }
}
复制代码
Records 的限制
records
不能继承任何的其他类,除了头部定义的属性(字段)外,声明的任何其他属性都必须是static
的。
records
是隐式final
类,不可以是abstract
抽象类。
records
的组件是隐式final
。因此可以将records
作为一个数据的集合,因为不用担心数据会被修改。records
只提供了属性的getter
方法,属性都是 final 的赋值后无法修改。
除了以上的限制外,records
的其他行为和正常的类是一样的。具体如下:
可以嵌套类丶声明泛型丶实现接口丶通过 new 关键字实例化;
可以声明静态方法丶静态字段丶静态初始化容器丶构造函数丶实例方法和嵌套类型;
// 特征1:定义泛型 / 实现接口
public record Features03<T>(String name, int age) implements Serializable {
// 特征2:定义的成员变量,必须是静态的,可以初始化值
private static String address;
// 定义公共静态常量
private static final String desc = null;
//特征3:实例方法
public String getDesc() {
return null;
}
//特征4:静态方法
public static String getAddress() {
return null;
}
// 特征5:可以自定义构造函数
public Features03 {
if (age > 10) {
throw new IllegalArgumentException("年龄大于10岁");
}
}
// 特征6:嵌套的records
record MyFeatures(String dept, String desc) {
}
}
复制代码
Features03 反编译后的文件
public final class Features03 <T> extends java.lang.Record implements java.io.Serializable {
private final java.lang.String name;
private final int age;
private static java.lang.String address;
private static final java.lang.String desc;
public Features03(java.lang.String name, int age) { /* compiled code */ }
public java.lang.String getDesc() { /* compiled code */ }
public static java.lang.String getAddress() { /* compiled code */ }
public java.lang.String toString() { /* compiled code */ }
public final int hashCode() { /* compiled code */ }
public final boolean equals(java.lang.Object o) { /* compiled code */ }
public java.lang.String name() { /* compiled code */ }
public int age() { /* compiled code */ }
static final class MyFeatures extends java.lang.Record {
private final java.lang.String dept;
private final java.lang.String desc;
public MyFeatures(java.lang.String dept, java.lang.String desc) { /* compiled code */ }
public java.lang.String toString() { /* compiled code */ }
public final int hashCode() { /* compiled code */ }
public final boolean equals(java.lang.Object o) { /* compiled code */ }
public java.lang.String dept() { /* compiled code */ }
public java.lang.String desc() { /* compiled code */ }
}
}
复制代码
JEP 361:Switch 表达式(标准)
switch 表达式并不是在 JDK 14 才出现的,早在 JDK 12(JEP 325) 和 13(JEP 354) 中 switch 就作为预览语言特性出现。JDK 12 中出现了 ->
操作符,JDK 13 中出现了yield
关键字。而 JDK 14 是将 12 / 13 的预览特性最终确定下来作为标准特性发布。
JDK 12 之前的写法
public void test() {
Week day = Week.FRIDAY;
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
System.out.println(6);
break;
case TUESDAY:
System.out.println(7);
break;
case THURSDAY:
case SATURDAY:
System.out.println(8);
break;
case WEDNESDAY:
System.out.println(9);
break;
}
}
// 将匹配条件后的值赋值给一个变量
public void test2() {
int numLetters;
Week day = Week.FRIDAY;
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
numLetters = 6;
break;
case TUESDAY:
numLetters = 7;
break;
case THURSDAY:
case SATURDAY:
numLetters = 8;
break;
case WEDNESDAY:
numLetters = 9;
break;
}
}
enum Week {
MONDAY,
FRIDAY,
SUNDAY,
TUESDAY,
THURSDAY,
SATURDAY,
WEDNESDAY
}
复制代码
JDK 12 写法
在 JDK 12 中引入一种新形式的开关标签 ”case L ->“,以表示如果标签匹配则仅执行标签右边的代码。而且还允许使用逗号分隔多个常量。
public void test1() {
Week day = Week.FRIDAY;
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
case TUESDAY -> {
// case 后面是一个代码块,包含多行代码
System.out.println("输入的日期为:" + TUESDAY);
System.out.println(7);
}
case THURSDAY, SATURDAY -> System.out.println(8);
case WEDNESDAY -> System.out.println(9);
// case 后面抛出异常
default -> throw new IllegalArgumentException("输入的星期有误");
}
}
// 将匹配条件后的值赋值给一个变量,然后将结果输出
// 即将结果赋值给一个变量,作为表达式使用,switch需要以分号结尾
// 方式一:赋值变量后输出结果
@Test
public void test3() {
Week day = Week.FRIDAY;
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
// case 后面抛出异常
default -> throw new IllegalArgumentException("输入的星期有误");
};// 末尾需要以分号结束,因为这个时候switch作为表达式使用
System.out.println(numLetters);
}
// 方式二:将switch作为表达式直接输出
public void test4() {
Week day = Week.FRIDAY;
System.out.println(
// 将表达式结果输出
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
// case 后面抛出异常
default -> throw new IllegalArgumentException("输入的星期有误");
}
);
}
enum Week {
MONDAY,
FRIDAY,
SUNDAY,
TUESDAY,
THURSDAY,
SATURDAY,
WEDNESDAY
}
复制代码
JDK 13 写法
在 JDK 13 中为了解决 JDK 12 中 switch 作为表达式如果返回结果时候,case -> 右边如果是一个代码块,那么就不知道何时返回结果。因此引入了一个关键字:yield
// yield关键字的用法,返回结果
// 方法一: case -> 写法
public void test5() {
Week day = Week.FRIDAY;
// 将表达式结果输出,
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
// case后面如果是代码块,返回结果需要使用关键字:yield,不可以使用return返回结果
case WEDNESDAY -> {
System.out.println("case -> 后面是代码块时候返回结果");
yield 9;
}
// case 后面抛出异常
default -> throw new IllegalArgumentException("输入的星期有误");
};
}
// 方法二:case: 写法
public void test6() {
Week day = Week.FRIDAY;
// 将表达式结果输出
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY: yield 6;
case TUESDAY: yield 7;
case THURSDAY, SATURDAY: yield 8;
// case后面如果是代码块,返回结果需要使用关键字:yield,不可以使用return返回结果
case WEDNESDAY: {
System.out.println("case -> 后面是代码块时候返回结果");
yield 9;
}
// case 后面抛出异常
default: throw new IllegalArgumentException("输入的星期有误");
};
}
enum Week {
MONDAY,
FRIDAY,
SUNDAY,
TUESDAY,
THURSDAY,
SATURDAY,
WEDNESDAY
}
复制代码
警告
对于控制语句丶 break 丶 yield 丶 return 和 continue,不能跳过 switch 表达式。
for (int i = 0; i < Integer.MAX_VALUE; ++i) {
int k = switch (e) {
case 0:
yield 1;
case 1:
yield 2;
default:
// 错误的写法,不能跳过switch表达式
continue z;
};
}
复制代码
JEP 368:文本块(第二次预览)
一句话含义
文本块是多行字符串文字,它可以有效避免字符串特殊字符需要转义序列。
JEP 目标
通过简化跨行源代码的字符串工作,从而简化了编写 Java 程序的任务,同时避免了常见情况下的转义序列。
增强 Java 程序中表示用非 Java 语言编写的代码字符串的可读性。
通过规定相同的转义序列并且以字符串文字的形式操作,从而支持字符串文字的迁移。
病症所在
在 Java 中,在字符串文字中嵌入 HTML 丶 XML 丶 SQL 和 JSON 等片段,通常需要先进行转义和大量的串联,然后才能编译包含该片段的代码。通常这些代码都是难以维护的。
因此,通过具有一种语言学机制,可以将多行文字更直观的表示字符串,从而跨越多行也不会出现转义,从而提高了 Java 类程序的可读性和可写性。这就是文本块诞生的原因。
语法规则
文本块的定义由开始定界符(三个双引号字符)和结束定界符(三个双引号字符)确定文本块的边界String str = """ 文本内容 """
。同时引入了\
表示取消换行和\s
表示一个空格。
文本块示例
// 注意文本块每一行末尾都有一个换行符,所以普通的字符串长度要多1个单位。
public void testBlock() {
// 每一行字符串后面都有\n转义,可读性太差
String html = "<html>\n" +
"\t<body>\n" +
"\t\t<p>Hello, world</p>\n" +
"\t</body>\n" +
"</html>";
// 文本块写法,简单清晰,可读性好
String htmlBlock = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
System.out.println(html.length()); // 结果:53
System.out.println(htmlBlock.length()); // 结果:54
}
复制代码
语法示例
// 正确的定义文本块
String sql = """ // 开始分隔符一行
文本块内容
"""; // 结束分隔符一行
// 文本块中三个"字符的序列,至少需要转义一个字符,以避免和结束定界符冲突
String code =
"""
String text = \"""
A text block inside a text block
\""";
""";
// 错误语法示范
String a = """"""; // 错误,在开始分隔符后面没有换行符
String b = """ """; // 错误,在开始分隔符后面没有换行符
String c = """
"; // 错误,并没有结束分隔符
String d = """
abc \ def
"""; // 错误,斜杆没有转义
复制代码
新的转义序列
为了更好的处理换行符和空格的处理,JDK 14 引入了两个新的转义序列。
// 对于一个很长的字符串内容,为了更加方便查看,通常会拆分为较小的子字符串进行拼接,然后将结果的字符串表达式分散到几行中:
String literal = "Lorem ipsum dolor sit amet, consectetur adipiscing " +
"elit, sed do eiusmod tempor incididunt ut labore " +
"et dolore magna aliqua.";
// 使用\<line-terminator>转义序列,可以将其表示为:
// \ 表示后面是没有换行符的,所以字符串会是一行输出
String text = """
Lorem ipsum dolor sit amet, consectetur adipiscing \
elit, sed do eiusmod tempor incididunt ut labore \
et dolore magna aliqua.\
""";
复制代码
// \s在这个示例中,在每行的末尾使用可以确保每行正好是六个字符长度
String colors = """
red \s
green\s
blue \s
""";
// 可以在任何使用字符串文字的地方使用文本块。例如,文本块和字符串文字可以互换使用:
String code = "public void print(Object o) {" +
"""
System.out.println(Objects.toString(o));
}
""";
// 不建议的做法:使用文本块拼接,这样会显得很笨拙
String code = """
public void print(""" + type + """
o) {
System.out.println(Objects.toString(o));
}
""";
复制代码
API 对文本块的支持:附加方法
String::stripIndent()
:用于从文本块内容中去除附带的空白
String::translateEscapes()
:用于翻译转义序列
String::formatted(Object... args)
:简化文本块中的值替换
更多学习资料,可以关注下方微信公众号,回复关键字:Java 高级资料
即可免费获取完整的 Java 高级开发工程师视频资料。
注意
评论