随着 Java 16 的正式发布(以及长期支持版 17 的即将到来),还在用 Java 8 的小伙伴们可能已经觉得有太多东西要学了。本人将整理从 Java 9 到 Java 16 带来的主要新特性,按照分类陆续展示供大家参考。
本文针对的是语法元素的变化。它们不会影响代码逻辑,但了解它们有助于用更简洁的方式编写代码。本文的代码示例基本上都是来自 JEP 文档中的例子。
JEP 286: Local-Variable Type Inference(本地变量类型推断)
发布版本:Java 10
该特性能够简化本地变量的声明。可推断类型的变量声明语句不需以类型开头,而是用关键字 var
代替。
例子:
ArrayList list = new ArrayList<>(); // 不使用本地变量类型推断
var list = new ArrayList(); // 使用本地变量类型推断
不适用的情况:下面一些本地变量的声明不适用类型推断:
// 下面是错误使用 var 关键字的示例
var x; // 没有初始化表达式
var x = null; // 初始化为 null
var x = () -> {}; // 无法确定对应哪种函数式接口
var x = this::f; // 无法确定对应哪种函数式接口
var x = {1, 2}; // 初始化数组必须严格声明元素类型,如 var x = new int[]{1, 2};
JEP 361: Switch Expressions(Switch表达式)
发布版本:Java 14
该特性能够简化 switch
语句的编写,并避免因为忘记在分支上用 break;
语句结束而造成 BUG。
例子:
// 旧语法
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;
}
// 新语法
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
case TUESDAY -> System.out.println(7);
case THURSDAY, SATURDAY -> System.out.println(8);
case WEDNESDAY -> System.out.println(9);
}
此外,switch
本身也可以作为一个表达式来输出值。例如:
T result = switch (arg) {
case L1 -> e1;
case L2 -> e2;
default -> e3;
};
// 注意:如果 switch 表达式的一个分支是代码块,则需要用 yield 关键字返回结果。
// 这里不能用 return,因为 return 关键字已经被用于从当前方法返回。
int j = switch (day) {
case MONDAY -> 0;
case TUESDAY -> 1;
default -> {
int k = day.toString().length();
int result = f(k);
yield result;
}
};
// 下面是一个错误使用 switch 表达式的例子:
int i = switch (day) {
case MONDAY -> {
System.out.println("Monday");
// 错误!该分支需包含 yield 声明
}
default -> 1;
};
JEP 378: Text Blocks(文本块)
发布版本:Java 15
该特性使得多行字符串常量的表达方式更加直观。
例子:
// 旧语法
String html = "\n" +
" \n" +
" Hello, world
\n" +
" \n" +
"\n";
// 新语法
String html = """
Hello, world
""";
注意:
- 文本块的开始和结束都是
"""
,同一文本块的开始和结束符不能在同一行。 - 文本块中每行的行首空白字符被视为“缩进”,编译时取缩进大小最小的行作为整个文本块的左边界。
- 文本块中每行的行尾空白字符会被忽略掉。
- 不论源代码文件中的换行符是
\r
、\n
还是\r\n
,编译出来统一以\n
(LF)作为换行符。 如果文本块中包含
"""
,则需要转义为\"""
,"\""
或""\"
。例如:System.out.println(""" 1 " 2 "" 3 ""\" 4 ""\"" 5 ""\""" 6 ""\"""\" 7 ""\"""\"" 8 ""\"""\""" 9 ""\"""\"""\" 10 ""\"""\"""\"" 11 ""\"""\"""\""" 12 ""\"""\"""\"""\" """);
如果某行以
\
结尾,则它与下一行内容之间没有换行符。例如:// 下面两个表达式结果相同 String s = "abc"; String s = """ a\ b\ c""";
如果想要用空格填充使得每行长度一致,可以用
\s
填充:String s = """ red \s green\s blue \s """;
如果想在文本块内加入变量,可以直接用
+
:String type = getType(); String code = """ public void print(""" + type + """ o) { System.out.println(Objects.toString(o)); } """;
但是为了代码的可读性和可维护性,还是建议保持文本块自身的连贯。例如:
String source = """ public void print(%s object) { System.out.println(Objects.toString(object)); } """.formatted(type);
JEP 394: Pattern Matching for instanceof(instanceof 操作的匹配模板)
发布版本:Java 16
该特性简化了 “类型判断+强制类型转换” 逻辑的代码,特别是在编写 equals()
方法的时候。
例子:
// 旧语法
if (obj instanceof String) {
String s = (String) obj;
flag = s.contains("jdk");
}
// 新语法
if (obj instanceof String s) {
flag = s.contains("jdk");
}
// 当后面跟着 && 时,新的变量 s 可以在后面的判断表达式中立即使用
if (obj instanceof String s && s.length() > 5) {
flag = s.contains("jdk");
}
// 反之则不然
if (obj instanceof String s || s.length() > 5) { // 错误!
...
}
equals()
方法的代码可以得到极大简化。例如:
// 旧语法
public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
Point other = (Point) o;
return x == other.x
&& y == other.y;
}
// 新语法
public boolean equals(Object o) {
return (o instanceof Point other)
&& x == other.x
&& y == other.y;
}
注意:使用本特性所定义的模板变量也是一种本地变量。和其他本地变量一样,它可能会覆盖所在类的成员名。下面是一个例子:
class Example2 {
Point p;
void test2(Object o) {
if (o instanceof Point p) {
// p 指的是本地模板变量
...
} else {
// p 指的是所在类的成员
...
}
}
}
上面的例子中,不同 if 分支下的变量 p 指代的是完全不同的对象。因此请谨慎命名,以免造成理解上的混乱。
JEP 395: Records(记录类)
发布版本:Java 16
该特性简化了只读数据传输对象的定义。注意,它并不能代替现有的 java bean 概念,因为 java bean 的属性是可写的。
例子:
// 旧语法
class Point {
private final int x;
private final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
int x() { return x; }
int y() { return y; }
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
Point other = (Point) o;
return other.x == x && other.y == y;
}
public int hashCode() {
return Objects.hash(x, y);
}
public String toString() {
return String.format("Point[x=%d, y=%d]", x, y);
}
}
// 新语法
record Point(int x, int y) { }
// 如果要对参数进行校验和处理
record Point(int x, int y) {
Point {
if (x < 0) throw new IllegalArgumentException("x不能为负数");
y = Math.abs(y);
}
}
// 记录类的使用
Point p = new Point(1, 1);
System.out.println("x=" + p.x() + ", y=" + p.y());
使用限制:
- 记录类不可从其他类继承,因为所有的记录类都是
java.lang.Record
的子类。一个记录类甚至不可以是另一个记录类的子类。 - 记录类不可以是抽象的,也不可以有子类。
- 记录类的成员都是
final
的,即不可重新赋值。 - 记录类不可以再定义额外的成员,也不可以再定义额外的构造方法。
除此之外,记录类和普通 class 一样:
- 使用 new 关键字来创建实例;
- 可以实现其他接口;
- 可以是外部的,可以是内部的,也可以包含泛型;
- 可以定义静态方法、静态成员和非静态方法;
本地记录类:你可以在一个方法内声明本地记录类,以帮助实现业务逻辑。例如:
List findTopMerchants(List merchants, int month) {
// 声明本地记录类
record MerchantSales(Merchant merchant, double sales) {}
return merchants.stream()
.map(merchant -> new MerchantSales(merchant, computeSales(merchant, month)))
.sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
.map(MerchantSales::merchant)
.collect(toList());
}
注意,内部记录类(在类中声明)和本地记录类(在方法中声明)都是静态的。
代码兼容性问题:
因为默认包下新增了 java.lang.Record
类,所以任何其他包下的 Record 类的使用都会受到影响。例如你有一个类叫做 a.b.Record
,那么如果代码中使用 import a.b.*;
,这种情况下是无法使用你的 Record 类的。你必须改为 import a.b.Record;
才能编译通过。