Java 8 到 Java 17 的特性
个人选择
公司中的遗留项目基于 Java 8,也有部署使用 Java 11 的开源软件;对于 Side Project,甚至是新项目,可以毫不犹豫使用 Java 11/17。个人嫌在 Oracle 官方下载 JDK 过于麻烦,也非 100% 开源,加上本地操作系统指令集架构从 x86 转到了 ARM,因此决定下载 Azul Zulu Builds of OpenJDK。
市面上各家厂商构建的 JDK 不可能毫无差异,比如 OpenJDK 与 Oracle JDK – 比较表。
向后兼容
Java 8 有时被称为 Java 1.8,在这之后的版本号不再以 1. 开头,随着发布时间的增加而增加,上图中只有 8、11、17 是 LTS 版本。
% java -version
openjdk version "17.0.3" 2022-04-19 LTS
OpenJDK Runtime Environment Zulu17.34+19-CA (build 17.0.3+7-LTS)
OpenJDK 64-Bit Server VM Zulu17.34+19-CA (build 17.0.3+7-LTS, mixed mode, sharing)
Java 版本之间破坏性变更较小,JVM 高度向后兼容,新版通常兼容旧版,例如基于 Java 5 或 Java 8 的程序,一般情况下,不更改代码也可以在 Java 8 到 Java 17 的 JVM 上运行正常;反过来说,不能向前兼容,例如使用 Java 17 编译输出的 Class 或 Jar 文件无法在 17 之前的 JVM 上加载通过:UnsupportedClassVersionError
。
特性概览
下文只列出感兴趣的特性,一般情况下,后一版并不会移除上一版的特性。
Java 8
要么对数据进行抽象,要么对行为进行抽象。
Lambda expressions
尽管 Java 不是函数式语言,但是 Lambda 表达式允许我们将功能视为方法参数,或将代码视为数据。
// before java 8
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("hello world");
// --snip--
}
};
// after java 8
Runnable r2 = () -> {
System.out.println("hello world");
// --snip--
};
File dir = new File("/path/to/dir");
// anonymous class
File[] files1 = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getName().endsWith(".java");
}
});
// lambda
File[] files2 = dir.listFiles(pathname -> pathname.getName().endsWith(".java"));
如果匿名类是“旧酒”,那么 Lambda 表达式是“新瓶”,它更紧凑地表达了“单一抽象方法接口”(被称为 Functional Interface)的实例,Functional Interface 的抽象方法签名决定了函数的参数类型列表和返回值类型。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// --snip--
}
Java 提供了许多 Functional Interface 充当 lambda 表达式的数据类型,例如:Consumer、Predicate、Supplier。
final or effectively final
Lambda 表达式或匿名类在封闭范围访问的外围本地变量必须是 final 或 effectively final 变量。
int n = 0;
Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).forEach(item -> {
if (item % 2 == 0) {
// This code does not compile!
n++;
}
});
注意,上面和下面的代码编译都不会通过:Variable used in lambda expression should be final or effectively final.
Supplier<Integer> incrementer(int start) {
// This code does not compile!
return () -> start++;
}
在匿名类方法体内访问外围本地变量时,同样要求我们将它们声明为 final 或者不更改它们的值,后者对于编译器来说最终还是 final。
An anonymous class cannot access local variables in its enclosing scope that are not declared as final or effectively final.
JLS 当中的 §15.27.2 提到了原因:
The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems.
简而言之,该限制可以规避一些并发风险,不过访问放置在 Java 堆的实例字段、静态字段并不受此限制。
Method references
方法引用为已有名称的方法提供易于阅读的 Lambda 表达式。
Stream.of("a", "b", "c").map(item -> item.toUpperCase()).forEach(item -> System.out.println(item));
// can be replaced with a method reference
Stream.of("a", "b", "c").map(String::toUpperCase).forEach(System.out::println);
根据《Effective Java》中的说法,有 5 种类型的用例可供参考。
Streams
新的 java.util.stream 包提供了 Stream API 来支持对元素流(streams of elements)的函数式操作、链式操作、聚合操作。一个 Stream 是一个元素序列,不同于 Collection,它不是存储元素的数据结构,相反,Stream 通过管道携带来自 Source 的值。一个 Source 可以是一个 Collection、一个数组、一个生成器函数、一个 I/O Channel 等等。
Oracle 的 Java 指南把 Streams 安排到 Collections 之下,通常将两者的 API 结合起来使用,假设有一个名为 roster 且类型是 Collection 的变量,要求打印男性姓名:
// before java 8
for (Person p : roster) {
if (p.getGender() == Person.Sex.MALE) {
System.out.println(p.getName());
}
}
// after java 8
roster
.stream()
.filter(e -> e.getGender() == Person.Sex.MALE)
.forEach(e -> System.out.println(e.getName()));
一系列聚合操作如同 Unix 的管道隐藏了内部结构,使过程更清晰和简单。
List<Integer> transactionsIds =
transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.sorted(comparing(Transaction::getValue).reversed())
.map(Transaction::getId)
.collect(toList());
这么好的例子来自 Processing Data with Java SE 8 Streams, Part 1 和 Part 2: Processing Data with Java SE 8 Streams。
Default method
向某一接口新增方法,该接口的所有实现类均要更改,默认方法允许接口提供默认具体实现和兼容旧版本实现类。
interface One {
default void method() {
System.out.println("One");
}
}
interface Two {
default void method () {
System.out.println("One");
}
}
class Three implements One, Two {
public void method() {
One.super.method();
}
}
实现类非必须重写(override)默认方法,假如需要在重写的方法内引用默认方法,应该使用 interface 名称与关键词 super
。
Java 9
Collections 的便利工厂方法
{
// before java 9
// mutable
List<String> list1 = Arrays.asList("one", "two", "three");
// since java 9
// immutable
List<String> list2 = List.of("one", "two", "three");
// before java 9
// mutable
Set<String> set1 = new HashSet<>();
set1.add("one");
set1.add("two");
set1.add("three");
Set<String> set2 = new HashSet<String>() {
{
add("one");
add("two");
add("three");
}
};
// since java 9
// immutable
Set<String> set3 = Set.of("one", "two", "three");
// before java 9
// mutable
Map<String, Integer> map1 = new HashMap<>();
map1.put("foo", 1);
map1.put("bar", 2);
Map<String, Integer> map2 = new HashMap<String, Integer>() {
{
put("foo", 1);
put("bar", 2);
}
};
// since java 9
// immutable
Map<String, Integer> map3 = Map.of("foo", 1, "bar", 2);
}
Stream 生成器
for (int i = 0; i < 10; ++i) {
System.out.println(i);
}
// since java 9
Stream.iterate(0, i -> i < 10, i -> i + 1)
.forEach(System.out::println);
Stream takeWhile/dropWhile
// print: 0123443210
Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
.filter(i -> i < 5)
.forEach(System.out::print);
// print: 01234
Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
.takeWhile(i -> i < 5)
.forEach(System.out::print);
// print: 56789876543210
Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
.dropWhile(i -> i < 5)
.forEach(System.out::print);
遇到第一个不满足条件的元素时,方法 filter 将继续,而方法 takeWhile/dropWhile 将退出。
JShell
JShell 是一种 REPL,即“读-求值-打印”循环,它读取来自用户输入到终端的语句或表达式,进行求值,将结果输出到终端。
Java 10
本地变量类型推断
{
// Pre-Java 10
Person person = new Person();
// With Java 10
var person = new Person();
}
关键词 var 是 variable 的简写,编译器在编译时能够从初始化器(initializer)推断出声明为 var 的本地变量(local variable)的类型,尽管如此,Java 仍然是静态类型语言。
显式声明类型的本地变量未初始化不可使用,使用 var 修饰的本地变量同理,前者允许迟一点初始化或者干脆不初始化也不使用却能通过编译,后者要求声明后立即提供可推断的初始化器,例如下面的代码编译不通过:
{
// Cannot infer type: 'var' on variable without initializer
var title;
title = "barista";
// Cannot infer type: variable initializer is 'null'
var city = null;
}
Java 11
Strings 与 Files 的新方法
Strings
- 方法 isBlank()。没必要只为了 StringUtils.isBlank(java.lang.String) 引入 Apache Commons Lang3。
- 方法 lines()。返回从字符串提取的由行组成的流,行之间通过行终止符分隔。
- 方法 trim()。方法 trim() 仅删除字符 <= U+0020(空格);方法 strip() 删除所有 Unicode 空白字符(但不是所有控制字符,例如 \0)。
Files
更容易地从文件读取字符串和写入字符串。
Path path = Files.writeString(Files.createTempFile(tempDir, "demo", ".txt"), "Sample text");
String content = Files.readString(path);
assertThat(content).isEqualTo("Sample text");
运行源文件
可以直接运行源文件,而无需经过手动编译!
public class MyScript {
public static void main(String[] args) {
System.out.println("Hello, " + args[0] + "!");
}
}
命令行工具 java 帮你先编译后启动 JVM。
% java MyScript.java world
Hello, world!
Lambda 参数的类型推断
(var firstName, var lastName) -> firstName + lastName
Java 12
🥵 支持 Unicode 11 和 Switch 表达式的预览。
Java 13
Switch Expression (Preview)
Switch 表达式比 Switch 语句更紧凑、简洁、易于阅读。
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;
}
// since java 12/13
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 表达式与 Switch 语句一个重要区别在于前者有返回值。
int numLetters;
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;
default:
throw new IllegalStateException("Wat: " + day);
}
// since java 12/13
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
};
Text Blocks (Preview)
在 Java 源文件里写多行的 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`;
""";
Java 14
Switch Expression
Switch 表达式正式加入 Java。
int j = switch (day) {
case MONDAY -> 0;
case TUESDAY -> 1;
default -> {
int k = day.toString().length();
int result = f(k);
yield result;
}
};
在 Switch 表达式体内显式或隐式使用关键词 yield
产生返回值。
Records (Preview)
编写普通的类时常伴随着编写大量的样板代码:构造器、访问器(比如 Getters/Setters)、equals()、toString() 等等,通常有两种方案可以缓解编写样板代码的沮丧:
- 生成源代码。例如使用 IDE 生成样板代码。
- 编译时生成代码。例如使用 Project Lombok。
Records 是一种类型声明,它是 class
的一种受限形式,一条 record 具有名称和状态描述(state description),举例来说:
record Point(int x, int y) {
}
一条 record 自动获得了许多标准成员:public final
的实例字段、有参构造器、标准的 equals、hashCode、toString 方法,上文的 record Point 近乎等价于:
final class Point {
public final int x;
public final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// Implementation of equals, hashCode, toString
}
Pattern Matching for instanceof (Preview)
使用 instanceof
判断变量之后使用该变量需强制类型转换:
if (obj instanceof String) {
String s = (String) obj;
// use s
}
现在有了更简洁的写法:
if (obj instanceof String s) {
// can use s here
}
Packaging Tool (Incubator)
命令行工具 jpackage 将 Java 应用程序和 Java 运行时镜像作为输入,并产出包含所有必要依赖的 Java 应用程序镜像。它能够产生特定平台的原生包,例如 Windows 上的 exe
和 msi
、MacOS 上的 dmg
和 pkg
、Linux 上的 deb
和 rpm
,然而每种格式必须在其运行的平台上构建。
Remove CMS
移除并发标记清除 (CMS) 垃圾收集器。
Java 15
ZGC
可扩展的低延迟垃圾收集器 ZGC,生产就绪。
Text Blocks
Text Blocks 生产就绪。
Sealed Classes (Preview)
一个 sealed class
或者 sealed interface
要求指定允许哪些类继承或者实现。
package com.example.geometry;
public abstract sealed class Shape permits Circle, Rectangle, Square {
}
如上所示,类 Shape 只能被 Circle、Rectangle、Square 继承,且所有 sealed class 的子类必须是 final、sealed 或非 sealed 的。
Java 16
Pattern Matching for instanceof
Pattern Matching for instanceof 正式加入 Java。
Unix-Domain Socket Channels
Unix-domain sockets 用于同一主机上的进程间的通信,它们大多数方面类似于 TCP/IP Sockets,除了通过操作系统文件路径名而不是通过 IP 地址和端口来寻址。
Java 17
Pattern Matching for switch (Preview)
虽然 instanceof
已经有更简洁的模式匹配,但是结合过多的 if…else if…else 不比 switch
表达式更直观。
static String formatterPatternSwitch(Object o) {
return switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
}
现在我们可以把 Object
类型的变量传递给 switch
,且已匹配 case
的模式被自动赋值。
Sealed Classes
Sealed Classes 正式加入 Java。
Foreign Function & Memory API (Incubator)
Foreign Function & Memory API 是 JNI 的替代品,Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行交互。通过有效调用 Foreign Function(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存)使 Java 程序能够调用原生库(native libraries)并处理原生数据(process native data),且没有 JNI 的脆弱性和危险性。
遇到的坑
至少有两种解决方案:
- 使用 Byte Buddy 代替 cglib,但 API 大相径庭。
- 使用 org.springframework.cglib 代替 net.sf.cglib,但依赖 org.springframework:spring-core:5.3.x。
本文首发于 https://h2cone.github.io/