0%

Java语言新特性漫谈:Java 14篇

本文是《Java语言新特性漫谈》系列文章中的一篇,该系列文章主要探讨各 Java 版本的语言特性方面的增强更新。

Switch表达式

Switch 表达式的相关改进特性是从 Java 12 开始引入的预览特性,经过 Java 13 的一点微调,Java 14 终于成为正式版本。

主要的改进包括:

  1. 新的 case 分支语法:case ... -> labels(该写法不存在“条件贯穿”);
  2. switch 既可以作为语句,也可以作为表达式。作为表达式时,使用 yield 返回表达式的值。

可以分别参考之前的两篇介绍《Java语言新特性漫谈:Java 12之switch》以及《Java语言新特性漫谈:Java 13篇》

文本域(预览特性第2版)

Java 14 在 Java 13 文本域的预览特性基础上,加入了更多的转义字符支持:

  • \ :续行符。提供源代码级别的换行。
  • \s:提供保留行尾空格的能力。

文本块中的换行会导致在代表的文本中插入换行,那么,如果文本行超长,仅仅想在源代码级别换行该怎么办呢?答案是使用“续行符”。

在行末插入“续行符”,以 \ 反斜杠表示。如:

1
2
3
4
String singleLine = """
Hello \
World
""";

大多数语言的续行符都是反斜杠。

另一方面,文本域在自动格式化的时,会移除行尾的空格。

如果不想结尾空白字符被移除,可以 \s\040 结尾。

注意,不能使用 \u0020,因为 Unicode 转义解析发生在词法分析之前。
如果不想结尾空白字符被意外移除,-Xlint:text-blocks 同样可做该项检查,移除发生时会警告 trailing white space will be removed

instanceof操作符的模式匹配(预览特性)

这个特性名称听起来很高大上,其实涉及的只是 instanceof 操作符的很小一个改进。

语法

通常,instanceof 操作符用以判断一个变量是否是某种类型,如果是,就转型为该类型进行处理——这就是大家熟悉的“instanceof-and-cast 习惯用法”。比如下面这段计算矩形周长的代码:

1
2
3
4
if (shape instanceof Rectangle) {
Rectangle s = (Rectangle) shape;
return 2 * s.length() + 2 * s.width();
}

很容易发现,第 2 行那条转型语句通常就是那样固定,写了还可能写错,那有什么必要写呢?所以,那就简化掉吧。现在可以这样写了:

1
2
3
if (shape instanceof Rectangle s) {
return 2 * s.length() + 2 * s.width();
}

可以看到,现在我们在使用 instanceof 进行类型测试的同时,就声明了一个变量——如果类型测试的结果为 true,就会将被测对象转型后赋值给新声明的变量——这样就可以直接使用,而不再需要手动的类型转换了。

用法就介绍完了!是不是很简单!

变量作用域

一个值得讨论的问题是:新声明的变量作用域有多大?简单来说就两点:

  1. instanceof 语句所在的代码块内;
  2. 在类型测试结果为 true 的条件下能执行到的代码中。

一个容易忽略的场景是在短路逻辑中,比如下面这代码是合法的:

1
2
3
if (shape instanceof Rectangle s && s.length() > 5) {
// ...
}

因此,只有当 shape instanceof Rectangle strue 时,才会执行 s.length() > 5

反过来,如果换成“或”就不合法了,比如:

1
2
3
if (shape instanceof Rectangle s || s.length() > 0) { // error
// ...
}

此时,即使类型测试结果为 false,也是会执行后面的代码的。

模式匹配为何?

该特性叫“模式匹配(pattern match)”,很学术的一个概念,那到底什么是模式匹配呢?

模式匹配用于测试一个对象是否有某种特定结构,如有则从中提取数据。

这不正是 instanceof 操作符的作用么!

模式(pattern),是一个谓词和一系列绑定变量的组合。其中,谓词可以应用于一个目标。而只要谓词匹配,就可以从目标中提取绑定变量。
谓词(predicate),是一个仅带一个参数的布尔函数。
在本特性中,谓词就是 instanceof 测试变量类型,目标即是被测变量,绑定变量显然就是新声明的变量。只要测试结果为 true,被测变量就会被转型赋值给新声明的变量。

记录(Record,预览特性)

终于,Java 又新增了一种新的类型声明——记录(Record)。跟 enum 一样,record 也是一种受限的类。
通常,它用作数据载体。

与其说 record 是一种新类型,不如说它是一种类模板,使用者仅需提供少量与数据相关的信息,编译器就会根据这些信息自动生成相关方法。因此,其语法很简洁,看起来是这样子的:

1
record Rectangle(float length, float width) { }

注意,紧随类名后,就罗列了所有的数据字段。
看起来很像声明了一个方法,但实际上不是。

从上述 Rectangle 类声明中,编译器会自动生成些什么呢?

  • private final 修饰的数据字段(这里有两个字段 lengthwidth
  • 与数据字段同名返回类型一致getter 访问器(也是两个)
  • 从数据字段推断出的公共构造器(显然包含两个参数)
  • 实现 equals()hashCode() 方法,判等逻辑是所有数据字段类型及值均相等
  • 实现 toString() 方法,包含类名及所有数据字段的名称及对应的值

关于访问器注意两点:

  • 没有 setter 访问器,因此可以认为记录是不可变的。但是,“不可变”不是绝对的,如果数据字段是某些引用类型,比如集合,其中的内容是可以改变的。
  • getter 访问器允许自定义。

另外,构造器是可以重载的,只是重载的构造器第一行必需要调用另一个构造器。比如:

1
2
3
4
5
6
7
8
9
record Rectangle(float length, float width) {
public Rectangle(float length) {
this(length, 100);
}

public Rectangle() {
this(100);
}
}

Compact构造器

如果没有编写构造器,构造器是由编译器推断生成的。但并不是说不可以自定义构造器,只是在构造器中不能再给数据字段赋值,只能做诸如数据字段的合法性校验等操作。

或许是因为能力有限,才称为“Compact 构造器”吧!
尝试在自定义的构造器中给数据字段赋值,会得到一个“无法为最终变量xxx分配值”的错误。

构造器声明语法如下:

1
2
3
4
5
6
7
record Rectangle(float length, float width) {
public Rectangle {
if (length <= 0 || width <= 0) {
throw new IllegalArgumentException("length or width must be greater than 0.");
}
}
}

呃……类声明语法像方法,构造器语法像类声明,也是够了!

限制

一开始就说明了,记录是一种受限的类。那到底有什么限制呢?

  • 记录不能派生任何类,反过来,任何类也不能继承记录,即它是 final
  • 记录不能是 abstract 的,是隐式 final
  • 记录不能声明实例变量(其实例变量均只能是数据字段)以及实例初始化器
  • 记录的数据字段也都是隐式 final
1
2
3
4
5
6
7
8
9
10
record Rectangle(double length, double width) {

// 错误,只能声明为 static
BiFunction<Double, Double, Double> diagonal;

// 错误,实例初始化器是不允许的
{
diagonal = (x, y) -> Math.sqrt(x*x + y*y);
}
}

记录类的超类是 java.lang.Record

除去这些限制,记录与普通类很相似,它可以:

  • 内部可声明嵌套类和接口(包括嵌套记录类),不过它们是隐式 static
  • 可以创建泛型记录
  • 可以实现接口
  • 使用 new 关键字创建实例
  • 内部可声明:静态方法、静态变量、静态初始化器、构造器、实例方法以及嵌套类型
  • 可以注解记录和它的各个数据字段
1
2
3
4
5
6
// 泛型记录
record Triangle<C extends Coordinate> (C top, C left, C right) { }
// 注解
record Rectangle(
@GreaterThanZero double length,
@GreaterThanZero double width) { }

注意,注解是同时应用于:规范构造器参数、记录类组件、私有字段和访问器方法等,除非声明中有指定注解应用的目标(@Target)。

API更新

如果不查看一个类的声明,单从名称上并不能看出它是否是记录。因此,必然需要一种方式来识别。

Class 为此新增了两个方法,boolean isRecord() 用以判断一个类是否是记录,如果是记录,RecordComponent[] getRecordComponents() 会返回其数据字段信息,否则返回 null

参考

Oracle - Java Language Updates 14

Oracle - Java Language Updates 15