Java 11 主要引入了哪些新特性?

本文重点回顾 Java 11 引入的那些主要特性。

Java 11 主要新特性脑图

(Java 11 主要新特性脑图)

1 全新的 HTTP 客户端 API

Java 11 引入了全新的 HTTP 客户端 API(主要有三个类 HttpClientHttpRequestHttpResponse),目的是替换现有的 HttpURLConnection API。

现有的 HttpURLConnection API 存在许多问题:

  • 设计陈旧

    在设计时考虑了多种协议,而这些协议现在有好多都已失效(ftp、gopher 等)。

  • 不支持现代 Web 特性

    该 API 仅支持 HTTP/1.1,不支持 HTTP/2,不支持 WebSocket 等特性。

  • 不支持连接池

    该 API 不支持连接池。

  • 易用性差

    该 API 设计过于复杂,易用性差,如:必须由开发者手动处理输入和输出流、手动进行错误处理,且不支持请求失败后的自动重试。

  • 性能不佳

    该 API 工作模式为阻塞模式(即一个线程处理一个请求和响应),不支持异步模式。

  • 非线程安全

    HttpURLConnection 非不可变,也非线程安全。

基于上述几点,Java 11 引入了全新的 HTTP 客户端 API 来替代 HttpURLConnection。相比 HttpURLConnection,新的 API 具有如下优势:

  • 简洁的 API 设计

    新 API 提供了更简洁的编程接口(如:允许链式调用,使得构建请求和发送请求变得更简单),开发者可以用更少的代码实现更复杂的功能。

  • 支持 HTTP/2

    新 API 几乎支持 HTTP/2 协议的所有特性,这意味着可以利用 HTTP/2 的多路复用特性,使得单个连接可以同时处理多个请求和响应,提高了性能和效率。

  • 支持同步和异步通信

    新 API 同时支持同步通信和异步通信,意味着其既可以像传统的 HTTP 客户端一样使用阻塞方式发送请求并等待响应,也可以通过非阻塞方式发送请求并处理响应。

  • 支持 WebSocket

    新 API 支持 WebSocket,允许建立持久的连接,并进行全双工通信。

  • 更好的错误处理机制

    新 API 提供了更好的错误处理机制,当 HTTP 请求失败时,可以通过异常机制更清晰地了解到发生了什么。

下面看一个简单的示例:

// src/main/java/NewHTTPClientAPITest.java
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;

public class NewHTTPClientAPITest {

    public static void main(String[] args) throws IOException, InterruptedException {
        // 构建 HttpClient 对象
        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofMinutes(1))
                .build();

        // 构建 HttpRequest 对象
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://leileiluoluo.com"))
                .GET()
                .build();

        // 同步请求
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.body());

        // 异步请求
        CompletableFuture<HttpResponse<String>> futureResponse = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
        futureResponse.thenApply(HttpResponse::body) // 获取响应体
                .thenAccept(System.out::println) // 打印响应体
                .join(); // 等待所有操作完成
    }
}

如上示例,首先构建了一个 HttpClient 对象,指定超时时间为 1 分钟;然后构建了一个 HttpRequest 对象,指定了请求的 URI 与请求方法(GET);然后分别使用同步方式和异步方式发起了请求并打印了响应体。

2 String API 增强

Java 11 对 String API 进行了增强,主要新增了以下几个实例方法:

  • isBlank()

    isBlank() 方法用于判断字符串是否是空白字符串,如果是(仅由空格、制表符、换行符等字符组成),则返回 true,否则返回 false。其比 Java 1.6 引入的 isEmpty() 方法更加全面(isEmpty() 仅判断字符串长度是否为 0)。

  • lines()

    lines() 方法会将字符串使用行终止符进行分隔,并将结果以 Stream 返回。

  • strip()stripLeading()stripTrailing()

    strip() 方法会将字符串的首尾空白字符去除,并返回一个新的字符串。与 Java 1.0 引入的 trim() 方法仅可以处理 ASCII 空白字符不同的是,strip() 方法不仅可以处理 ASCII 空白字符还可以处理 Unicode 空白字符。

    stripLeading()stripTrailing()strip() 相似,不同的是此两者分别用于去除首空白字符和尾空白字符。

  • repeat(int)

    repeat(int) 方法会将字符串重复指定次数,并返回一个新的字符串。

下面看一个示例:

// src/main/java/StringAPIEnhancementsTest.java
import java.util.stream.Collectors;

public class StringAPIEnhancementsTest {

    public static void main(String[] args) {
        // isBlank() 使用:换行符、制表符、半角空格、全角空格等都会被认为是空字符
        System.out.println(" \n\t ".isBlank());

        // lines() 使用:会将字符串以 \n 或 \r\n 分割为一个 Stream
        System.out.println("Hello\nWorld!".lines()
                .collect(Collectors.joining(", "))); // Hello, World!

        // strip() 使用:首尾的换行符、制表符、半角空格、全角空格等都会被处理掉
        System.out.println(" \n\t\n\r   你好,世界! ".strip()); // "你好世界"

        // stripLeading() 使用:头部的换行符、制表符、半角空格、全角空格等都会被处理掉
        System.out.println(" \n\t\n\r   你好,世界! ".stripLeading()); // "你好,世界! "

        // stripTrailing() 使用:尾部的换行符、制表符、半角空格、全角空格等都会被处理掉
        System.out.println(" \n\t\n\r   你好,世界! ".stripTrailing()); // " \n\t\n\r   你好,世界!"

        // repeat() 使用:将一个字符串重复两次
        System.out.println("Hello World!".repeat(2)); // Hello World!Hello World!
    }
}

可以看到,如上示例分别演示了 String 类新实例方法 isBlank()lines()strip()stripLeading()stripTrailing()repeat() 的使用。

3 Files API 增强

Java 11 对 Files API 进行了增强,主要新增了如下几个静态方法:

  • readString()

    该方法用于读取文本文件的内容,并返回一个 String。该方法简化了读取文件内容的操作(以前需要使用 BufferedReader 类等方式进行读取,很繁琐),特别是在文件内容较小的情况下。它是 Files.readAllBytes() 的一种更高层次的抽象,适用于读取文本文件。

  • writeString()

    该方法允许直接将字符串写入文件。

下面看一个示例:

// src/main/java/FilesAPIEnhancementsTest.java
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class FilesAPIEnhancementsTest {

    public static void main(String[] args) throws IOException, URISyntaxException {
        // readString() 使用
        URL resource = FilesAPIEnhancementsTest.class.getResource("test.txt");
        Path path = Paths.get(resource.toURI());
        System.out.println(Files.readString(path, StandardCharsets.UTF_8)); // Hello, World!

        // writeString() 使用
        Files.writeString(path, "你好,世界!", StandardCharsets.UTF_8);
    }
}

如上示例,首先使用 Files.readString() 静态方法读取了位于 resources 文件夹下 test.txt 文件的内容。然后使用 Files.writeString() 静态方法将字符串写入了上述文件。

4 Lambda 参数的局部变量语法

局部变量类型推断是在 Java 10 引入的特性,使用关键字 var 来代替显式地指定变量类型,而变量的类型则由编译器自行推断。但 Java 10 是不支持在 Lambda 参数使用 var 的,这在 Java 11 中得到了改进。

关于局部变量类型推断这一特性的详细介绍,请参考本人之前的一篇文章「Java 10 新特性:局部变量类型推断」。

下面看一个示例:

// src/main/java/LocalVariableSyntax4LambdaParametersTest.java
import java.util.function.BiFunction;
import java.util.function.Function;

public class LocalVariableSyntax4LambdaParametersTest {

    public @interface NonNull {
    }

    public static void main(String[] args) {
        // Java 10:显式类型 Lambda 表达式
        Function<String, String> toUpperCase = (String s) -> s.toUpperCase();

        // Java 10:隐式类型 Lambda 表达式
        Function<String, String> toUpperCase1 = s -> s.toUpperCase();

        // Java 11:局部变量语法(var)在隐式类型 Lambda 表达式中的使用
        Function<String, String> toUpperCase2 = (var s) -> s.toUpperCase();

        // Java 11:局部变量语法与注解结合使用
        BiFunction<Integer, Integer, Integer> sum = (@NonNull var a, @NonNull var b) -> a + b;
    }
}

如上示例,首先演示了在 Java 11 Lambda 参数支持局部变量语法之前,分别使用显式参数类型和隐式参数类型编写 Lambda 表达式的写法;然后演示了 Java 11 Lambda 参数支持局部变量语法后,上述两种写法的等价写法;最后演示了 Lambda 表达式局部变量语法与注解结合使用的写法。

5 Collection 新增 toArray() 重载方法

Java 11 在 Collection 接口中新增了一个重载版本的 toArray() 方法(重载了 Collection 接口中既有的 Object[] toArray()T[] toArray(T[] a) 两个抽象方法),用于将集合转换为数组。其方法签名如下:

default <T> T[] toArray(IntFunction<T[]> generator)

其入参 IntFunction<T[]> generator 是一个函数接口,用于生成一个具有指定大小的数组。这个函数接受一个整数参数(数组的大小),并返回一个具有指定大小的数组实例。

其返回值 T[] 是一个包含集合中所有元素的数组。如果提供的生成器函数返回的数组的大小足够大,那么元素将被放入这个数组中。如果生成器函数返回的数组的大小不足,该方法将创建一个新数组并将元素放入其中。

下面看一个示例:

// src/main/java/CollectionEnhancementsTest.java
import java.util.Arrays;
import java.util.List;

public class CollectionEnhancementsTest {

    public static void main(String[] args) {
        // Java 1.6:调用 List 的 `Object[] toArray()` 方法
        Object[] names = Arrays.asList("Larry", "Jacky", "Alice").toArray();

        // Java 1.6:调用 List 的 `T[] toArray(T[] a)` 方法
        String[] names1 = Arrays.asList("Larry", "Jacky", "Alice").toArray(new String[3]);

        // Java 11:调用 Collection 的 `toArray(IntFunction<T[]> generator)` 方法
        String[] names2 = List.of("Larry", "Jacky", "Alice")
                .toArray(String[]::new);
    }
}

如上示例,首先使用 Java 1.6 语法,介绍了 Collection 既有抽象方法 Object[] toArray()T[] toArray(T[] a) 的使用;然后使用 Java 11 新语法,介绍了 Collection 引入的新方法 T[] toArray(IntFunction<T[]> generator) 的使用,其中传入的 IntFunction 参数是一个方法引用 String[]::new,等价于 Lambda 表达式 (int s) -> new String[s],其会生成一个与集合大小相同的 String 数组。

6 Optional 类增强

Optional 类是 Java 8 引入的、用于处理可能为 null 对象的包装类,它提供了一种优雅的方法来减少 NullPointerException 出现的可能性。关于 Optional 类的详细介绍,请参阅本人之前的一篇文章「Java 8 新特性:Optional 类」。

Java 11 对 Optional 类进行了增强,在其中新增了一个方法:isEmpty(),用于判断 Optional 中的对象是否为 null,其与 isPresent() 方法正好相反。

下面看一个示例:

// src/main/java/OptionalEnhancementsTest.java
import java.util.Optional;

public class OptionalEnhancementsTest {

    public static void main(String[] args) {
        Optional<String> optional = Optional.empty();
        if (optional.isEmpty()) {
            System.out.println("Optional is empty");
        }
    }
}

如上示例演示了 isEmpty() 方法的使用。

7 基于嵌套的访问控制

在 Java 中,类和接口可以相互嵌套,这种组合之间可以不受限制的访问彼此,包括访问彼此的构造器函数、字段和方法等(即使设置为 private 的,也可以访问)。

在 Java 11 之前,嵌套类会编译为不同的类文件,针对嵌套类中私有成员的访问是编译器通过一种特殊的技术实现的,该技术称为可访问性扩展桥方法。该种技术会在编译时为含有私有成员的目标类生成对应的方法(包私有),所以针对私有成员的访问会变成一个个方法调用。而该技术破坏了封装,也增加了 .class 文件的个数。所以 Java 11 正式对类和接口的嵌套访问控制进行了规范化,允许以更简单、更安全、更透明的方式直接实现访问控制,而无需借助编译器的「特殊操作」。

下面看一段示例代码:

// src/main/java/NestBasedAccessControlTest.java
public class NestBasedAccessControlTest {
    private int number = 10;

    private void printOuter() {
        new Inner().printInner();
    }

    private class Inner {
        private void printInner() {
            number += 10;
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new NestBasedAccessControlTest().printOuter();
    }
}

如上代码中,NestBasedAccessControlTest 类中的字段 number 是私有的,但是可以被 Inner 类的方法 printInner() 直接访问;Inner 类的私有方法 printInner() 同样可以被 NestBasedAccessControlTest 类的方法 printOuter() 直接访问。这种设计是为了更好的实现封装,因为从外部使用者的角度来看,这几个彼此嵌套的类是一体的,所以私有元素也应该是共有的。

下面首先基于 Java 8 对 NestBasedAccessControlTest.java 文件进行编译:

javac NestBasedAccessControlTest.java

ls -lht
NestBasedAccessControlTest$1.class
NestBasedAccessControlTest.class
NestBasedAccessControlTest$Inner.class

可以看到,基于 Java 8 使用 javacNestBasedAccessControlTest.java 源文件进行编译后会生成三个单独的 .class 文件。

接着,同样基于 Java 8 使用 javap 命令对上述步骤生成的 .class 文件进行反编译:

javap -c NestBasedAccessControlTest.class

Compiled from "NestBasedAccessControlTest.java"
public class NestBasedAccessControlTest {
  public NestBasedAccessControlTest();
    Code:
       0: aload_0
       1: invokespecial #2                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        10
       7: putfield      #1                  // Field number:I
      10: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #6                  // class NestBasedAccessControlTest
       3: dup
       4: invokespecial #7                  // Method "<init>":()V
       7: invokespecial #8                  // Method printOuter:()V
      10: return

  static int access$200(NestBasedAccessControlTest);
    Code:
       0: aload_0
       1: getfield      #1                  // Field number:I
       4: ireturn

  static int access$202(NestBasedAccessControlTest, int);
    Code:
       0: aload_0
       1: iload_1
       2: dup_x1
       3: putfield      #1                  // Field number:I
       6: ireturn
}
javap -c NestBasedAccessControlTest\$Inner.class

Compiled from "NestBasedAccessControlTest.java"
class NestBasedAccessControlTest$Inner {
  final NestBasedAccessControlTest this$0;

  NestBasedAccessControlTest$Inner(NestBasedAccessControlTest, NestBasedAccessControlTest$1);
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #2                  // Method "<init>":(LNestBasedAccessControlTest;)V
       5: return

  static void access$100(NestBasedAccessControlTest$Inner);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method printInner:()V
       4: return
}
javap -c NestBasedAccessControlTest\$1.class

Compiled from "NestBasedAccessControlTest.java"
class NestBasedAccessControlTest$1 {
}

可以看到,编译器为 NestBasedAccessControlTest 类私有成员 number 创建了一个包私有的「桥」方法 access$200()access$202() 来供内部类进行读写;为 NestBasedAccessControlTest$Inner 类私有方法 printInner() 创建了另一个「桥」方法 access$100() 来供外部包装类访问。

所以,编译器生成的代码类似于下面这样:

// NestBasedAccessControlTest.java
public class NestBasedAccessControlTest {
    private int number = 10;

    static int access$200(NestBasedAccessControlTest obj) {
        return obj.number;
    }

    static int access$202(NestBasedAccessControlTest obj, int number) {
        obj.number = number;
        return obj.number;
    }

    private void printOuter() {
        NestBasedAccessControlTest$Inner.access$100(new NestBasedAccessControlTest$Inner(this));
    }

    public static void main(String[] args) {
        new NestBasedAccessControlTest().printOuter();
    }
}

// NestBasedAccessControlTest$Inner.java
public class NestBasedAccessControlTest$Inner {
    private final NestBasedAccessControlTest obj;

    NestBasedAccessControlTest$Inner(NestBasedAccessControlTest obj) {
        this.obj = obj;
    }

    static void access$100(NestBasedAccessControlTest$Inner inner) {
        inner.printInner();
    }

    private void printInner() {
        NestBasedAccessControlTest.access$202(this.obj, NestBasedAccessControlTest.access$200(this.obj) + 10);
        System.out.println(NestBasedAccessControlTest.access$200(this.obj));
    }
}

而在 Java 11,开始原生支持嵌套类(或接口)的私有成员访问,所以这种特殊的编译器「桥」方法技术也就成为历史了。

此外,Java 11 从语言特性上直接支持嵌套类(或接口)的私有成员访问后,其语义特性会更加严谨。

比如对最开始的示例代码进行一点改造(将 printOuter() 方法对 Inner 类私有方法 printInner() 的直接调用改为反射调用):

// src/main/java/NestBasedAccessControlReflectionTest.java
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class NestBasedAccessControlReflectionTest {
    private int number = 10;

    private void printOuter() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Inner inner = new Inner();
        final Method method = Inner.class.getDeclaredMethod("printInner");
        method.invoke(inner);
    }

    private class Inner {
        private void printInner() {
            number += 10;
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
        new NestBasedAccessControlReflectionTest().printOuter();
    }
}

这段代码在 Java 11 之前的环境上跑是会抛异常的:

Exception in thread "main" java.lang.IllegalAccessException: Class NestBasedAccessControlReflectionTest can not access a member of class NestBasedAccessControlReflectionTest$Inner with modifiers "private"
...

而在 Java 11,语言层面和编译器层面都是支持嵌套类的私有成员访问的(不管是直接调用还是反射调用),这种奇怪的、语义不一致的行为就不会发生了。

此外,Java 11 还在 Class 类新增了几个方法(getNestHost()getNestMembers()isNestmateOf())来获取嵌套关系,对于其使用方式,这里就不再赘述了。

8 Epsilon:一个无操作垃圾收集器

Java 11 引入了一个试验性的无操作垃圾收集器 Epsilon。Epsilon 不执行实际的垃圾收集工作,所以它不会回收内存,直至可用 Java 堆耗尽时,即会终止 Java 虚拟机。这个特性使得它非常适用于性能测试、基准测试,以及垃圾收集不是主要关注点的应用场景(如实时应用)。

在启动 Java 应用程序时,添加 -XX:+UnlockExperimentalVMOptions-XX:+UseEpsilonGC 参数即可解锁实验性选项并启用 Epsilon 垃圾收集器:

java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -jar app.jar

9 ZGC:一个可扩展的低延迟垃圾收集器

除了上述 Epsilon 外,Java 11 还引入了另一个试验性的垃圾收集器,称为 ZGC(Z Garbage Collector),是一个只适用于 Linux/x64 平台的可扩展低延迟垃圾收集器。ZGC 特别适用于对延迟敏感的应用程序,如金融服务、高频交易系统、大规模实时数据处理平台等。

ZGC 主要拥有如下特点:

  • 低延迟

    ZGC 旨在将垃圾收集的暂停时间控制在 10 毫秒之内。它通过将垃圾收集操作分解为许多小的、可并行的任务来最小化对应用程序的影响,从而实现低延迟。

  • 可扩展

    ZGC 的设计考虑了大规模内存的场景。它能够高效地处理包含数百 GB 甚至 TB 级别内存的堆空间。因此,无论堆内存的大小如何,ZGC 都能够保持其性能优势。

  • 并行性

    ZGC 使用并行化的处理策略来加速垃圾收集过程。这意味着垃圾收集任务可以在多个处理器核心上并行执行,从而减少总体垃圾收集时间。

在启动 Java 应用程序时,解锁实验性选项并启用 ZGC 的命令如下:

java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar app.jar

10 飞行记录器

Java 11 引入了一个名为「飞行记录器(Java Flight Recorder,JFR)」的功能,它是一个事件记录和分析引擎,用于在运行时收集和分析 Java 应用程序的运行数据。飞行记录器可以捕获各种事件(如:方法调用、垃圾收集、线程活动、I/O 操作等),还可以收集各种度量指标(如:CPU 使用率、内存使用量、线程数量等)。这些事件和度量指标可以用于分析和优化应用程序的性能、诊断问题和进行故障排除等。

在启动 Java 应用程序时,启用飞行记录器功能的命令如下:

java -XX:+FlightRecorder -jar app.jar

11 低开销的堆分析

Java 11 引入了一项名为「低开销的堆分析」功能,它允许在应用程序运行时收集堆分析数据,以便更好地理解和调试内存使用情况。传统的堆分析工具通常会对应用程序的内存进行全面的快照,会对应用程序的性能产生较大的开销。而低开销的堆分析功能通过减少采样频率和记录粒度,以及使用一些技术手段来减少开销,从而提供了一种更轻量级的堆分析方法。

要使用低开销的堆分析功能,可以使用如下命令启动 Java 应用程序:

java -XX:+FlightRecorder -XX:StartFlightRecording=heap=low -jar app.jar

这样即会在运行时启用低开销的堆分析功能,并将分析数据记录到默认的 JFR 文件中。然后,可以使用 Java Mission Control(JMC)或其它 JFR 分析工具加载和分析生成的 JFR 文件,以获得有关应用程序内存使用的详细信息。

12 单文件源码程序的运行

Java 11 引入了一项新功能,称为「运行单文件源代码程序」,允许直接启动单个源代码文件而无需先将其编译为字节码文件。

一个最简单的 Java 源代码文件 SingleFileSourceCodeProgramsTest.java 的内容如下:

// src/main/java/SingleFileSourceCodeProgramsTest.java
public class SingleFileSourceCodeProgramsTest {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

针对该源码文件,在 Java 11 之前,我们需要先使用 javac 命令将其编译为 .class 文件才能接着使用 java 命令来运行:

javac SingleFileSourceCodeProgramsTest.java
java SingleFileSourceCodeProgramsTest

而在 Java 11,直接使用 java 命令来运行即可:

java SingleFileSourceCodeProgramsTest.java

13 动态类文件常量

Java 11 引入了一种新的动态类文件常量(Dynamic Class-File Constants)机制,用于在 Java 类文件中处理动态常量,这可以提高运行时的灵活性和性能。

我们知道,在 Java 虚拟机中,常量池(Constant Pool)是一个用来存储类中常量的表。这些常量包括数字字面量、字符串、类引用等。传统上,Java 的常量池只能包含编译时确定的常量(如:静态字段或常量字符串)。然而,某些情况下,常量的值可能只有在运行时才能确定。Java 11 引入的动态类文件常量机制即是用于处理这些动态常量的。

要使用动态常量,编译器和虚拟机需要支持 invokedynamic 指令及其相关机制。在类文件中,可以通过CONSTANT_Dynamic 条目将动态常量的信息存储到常量池中。

14 支持 TLS(传输层安全)1.3

Java 11 引入了对 TLS(Transport Layer Security,传输层安全)1.3 协议的支持。TLS 1.3 旨在取代之前的 TLS 1.2 协议,以提供更高的安全性、更快的连接速度和更简化的协议设计。

TLS 1.3 使用了更强的加密算法,握手过程比 TLS 1.2 更加简化,还拥有更强的隐私保护能力,为 Java 应用程序提供了更强的安全性和更好的性能。

综上,我们速览了 Java 11 引入的主要特性或增强点。此外,Java 11 还删除了一些陈旧的模块(如:Java EE 和 CORBA 等),弃用了一些过时的工具或 API(如:Nashorn JavaScript 引擎、Pack200 工具等)。本文涉及的所有示例代码已提交至 GitHub,欢迎关注或 Fork。

参考资料

[1] Oracle: JDK 11 Release Notes, Important Changes, and Information - https://www.oracle.com/java/technologies/javase/11-relnote-issues.html

[2] OpenJDK: Java SE 11 Final Release Specification - https://cr.openjdk.org/~iris/se/11/latestSpec/

[3] OpenJDK: JDK 11 - https://openjdk.org/projects/jdk/11/

[4] Mkyong.com: Java 11 Nest-Based Access Control - https://mkyong.com/java/java-11-nest-based-access-control/

[5] 脚本之家:Java 11 中基于嵌套关系的访问控制优化详解 - https://www.jb51.net/article/233900.htm

[6] 掘金:一口气读完 Java 8 ~ Java 21 所有新特性 - https://juejin.cn/post/7315730050577006592

[7] 掘金:JDK 8 - JDK 17 新特性总结 - https://juejin.cn/post/7250734439709048869

评论

正在加载评论......