Java 8 到 Java 17 的特性

Posted on May 30, 2022

个人选择

公司中的遗留项目基于 Java 8,也有部署使用 Java 11 的开源软件;对于 Side Project,甚至是新项目,可以毫不犹豫使用 Java 11/17。个人嫌在 Oracle 官方下载 JDK 过于麻烦,也非 100% 开源,加上本地操作系统指令集架构从 x86 转到了 ARM,因此决定下载 Azul Zulu Builds of OpenJDK

市面上各家厂商构建的 JDK 不可能毫无差异,比如 OpenJDK 与 Oracle JDK – 比较表

向后兼容

javaversions-5

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 表达式的数据类型,例如:ConsumerPredicateSupplier

final or effectively final

Lambda 表达式或匿名类在封闭范围访问的外围本地变量必须是 finaleffectively 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 种类型的用例可供参考。

method_ref_type

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 的管道隐藏了内部结构,使过程更清晰和简单。

filter-sorted-map-collect

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 1Part 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 上的 exemsi、MacOS 上的 dmgpkg、Linux 上的 debrpm,然而每种格式必须在其运行的平台上构建。

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 的脆弱性危险性

遇到的坑

CGLIB 未支持 Java 17

至少有两种解决方案:

  • 使用 Byte Buddy 代替 cglib,但 API 大相径庭。
  • 使用 org.springframework.cglib 代替 net.sf.cglib,但依赖 org.springframework:spring-core:5.3.x。

本文首发于 https://h2cone.github.io/

参考资料