跳至主要內容

Java 9 新特性总结

大约 48 分钟

Java 9 新特性总结

Java 9 新特性—概述

Java 9 发布于 2017 年 9 月 22 日。

img

JEP 261: 模块系统

在传统的 Java 应用中,类路径的机制限制了封装性。即使类被标记为 package-private,也可以被同一个类路径中的其他任意代码访问。而且随着应用程序规模的增长,类路径依赖变得越来越复杂。

Java 为了能够改进大型应用程序和库的封装性、可维护性和性能,在 Java 9 版本引入模块系统。模块系统是 Java 平台架构的一次重大变革,它旨在解决长期以来 Java 应用所面临的一些结构性问题,特别是在大型系统和微服务架构中。

其主要内容是:

  • 模块化 JDK:JDK 被划分为一系列的模块,使得可以只需要引入必需的 JDK 模块,减少了应用程序的体积。
  • 模块路径:取代了传统的类路径,用于指定模块的位置。
  • 模块声明:使用 module-info.java 文件来声明模块,指定模块所需的其他模块和模块导出的包。

使用方法是:

新建模块后,我们就需要在 module-info.java 中定义模块信息了,信息主要包括如下几个部分:

  1. 使用module关键字定义模块,并指定模块的名称,例如:module java.module01 { }
  2. 使用requires关键字声明模块之间的依赖关系,例如:requires java.sql; 表示模块依赖于java.sql模块。
  3. 使用exports关键字声明模块中哪些包可以被其他模块访问,例如:exports com.skjava.module01.entity; 表示导出com.skjava.module01.entity包。

模块化系统带来三大好处:

  1. 更好的封装性:允许我们封装内部实现,只暴露必要的 API,从而减少了意外的依赖。
  2. 性能提升:模块化可以帮助 JVM 更高效地加载代码,提高启动速度和降低内存占用。
  3. 更好的安全性:可以限制哪些模块可以访问 JDK 的特定部分,从而提高安全性。

更多阅读:Java 9 新特性—模块化open in new window

JEP 269: 集合工厂方法

在 Java 9 之前创建一个不可变集合通常涉及多个步骤,包括创建、填充和包装。Java 9 引入该特性旨在提供一种简洁、安全且不可变的方式来创建集合(List、Set、Map)。

其内容包括:

  • List.of():创建一个不可变的 List,可以传递任意数量的元素。
  • Set.of():创建一个不可变的 Set,元素不可重复。
  • Map.of()Map.ofEntries():用于创建一个不可变的 Map。Map.of() 可以直接传递键值对,而 Map.ofEntries() 可以通过 Map.entry(k, v) 创建条目。

更多阅读:Java 9 新特性—新增只读集合和工厂方法open in new window

JEP 222:Jshell

在传统的 Java 开发中,即使是为了运行一个简单的程序,也需要编写完整的类和方法,然后编译和运行,这种方式对初学者很不友好,而且也不利于想要快速验证算法或逻辑的需求。

所以,Java 9 引入 Java Shell,其目的是提供一个官方的 Java REPL,支持快速测试、探索和实验 Java 代码片段。

Java Shell,是一个交互式的 Java REPL(Read-Eval-Print Loop),即一个用于交互式地执行 Java 代码的命令行工具。它的主要特点是:

  • 交互式执行:可以直接在命令行中输入并执行 Java 代码,无需创建完整的类文件。
  • 自动补全:支持代码自动补全,提高编码效率。
  • 临时变量和历史记录:自动为表达式的结果分配临时变量,并保存命令历史,方便回顾和修改。
  • 错误诊断:即使代码片段出错,JShell 也可以继续运行,提供错误信息帮助诊断问题。

能带来三大好处:

  • 提高效率:允许我们快速测试小片段的代码,而无需编写完整的应用程序。
  • 便于学习:为初学者提供了一个友好的环境,帮助他们理解 Java 语言的基础概念。
  • 快速原型验证:可以迅速验证想法和算法,加速开发过程。

更多阅读:Java 9 新特性—REPL 工具:JSheel 命令open in new window

JEP 213:接口支持私有方法

我们知道 Java 8 支持默认方法和静态方法,虽然提高了 Java 接口的灵活性和扩展性,但他们限制了接口的封装性和复用性。

为了提高接口的封装性和代码复用性,Java 9 支持在接口中定义私有方法和私有静态方法。

  • 私有方法:可以在接口内部定义私有方法,以帮助实现默认方法或其他私有方法。
  • 私有静态方法:类似地,可以定义私有静态方法,用于辅助接口内部的静态方法。

引入接口私有方法,除了增强接口的封装性和代码复用外,还能够使接口的设计者可以更清晰地区分公共 API 和内部实现细节,从而提供更干净、更简洁的公共 API。

更多阅读:Java 9 新特性—接口支持私有方法open in new window

Stream API 增强

Java 9 对 Stream API 新增了几个方法,这些方法使得 Stream 在处理数据流变得更加方便和强大了。

  1. takeWhile()** **:允许从流的开始处理元素,直到给定的谓词返回 false。这在处理有序流时特别有用。
  2. dropWhile()** **:与 takeWhile() 相反,它从流的开始丢弃元素,直到谓词返回 false,然后处理剩余的元素。
  3. ofNullable():用于创建单元素流,如果元素是 null,则返回一个空流,避免了 NullPointerException
  4. iterate() 的新重载:在 Java 8 中,iterate() 方法是无限的。Java 9 添加了一个重载,允许你提供一个谓词作为终止条件,这样就可以创建有限的流。

更多阅读:Java 9 新特性—Stream API的增强open in new window

Optional 的增强

Optional 是在 Java 8 中引入的,主要是用来解决 NullPointerException 的。Java 9 引入 3 个方法,进一步增强 Optional 的使用场景。

  1. stream() :允许将 Optional 对象转换为一个(最多只有一个元素的)流。这在将多个 Optional 对象组合到一个流中时特别有用。
  2. ifPresentOrElse():这个方法允许执行一个操作,如果 Optional 包含值,则执行一个操作,否则执行另一个操作。这提供了类似于“if-else”语句的功能。
  3. or():允许在当前的 Optional 为空时,提供一个替代的 Optional。这类似于 orElse()orElseGet(),但返回的是 Optional 对象而不是值。

更多阅读:Java 9 新特性—Optional 的增强open in new window

改进 try-with-resources

try-with-resources 是在 Java 7 中引入的,它能够完成资源的自动管理,例如文件和套接字的关闭,但是它需要在 try 语句内部声明和初始化,尽管它已经在外部声明了,这导致了代码的重复和冗余。

Java 9 对其进行了一个小的改动:允许使用在 try 语句块外部声明的资源。这意味着如果资源已经是 final 或者 effectively final(即实际上没有被后续代码修改),就可以在 try-with-resources 语句中直接使用,而无需在 try 语句内再声明一个新的局部变量。例如:

  • Java 7
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
try (BufferedReader r = reader) {
    // 使用 reader
}
  • Java 9
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
try (reader) {
    // 使用 reader
}

更多阅读:Java 9 新特性—try-with-resources的升级open in new window

JEP 102:Process API

在 Java 9 之前,Java 提供的进程控制功能相对有限,很难直接获取关于系统进程的详细信息,也难以管理这些进程。这导致开发者需要依赖于特定平台的代码来完成这些任务,降低了代码的可移植性和易用性。

为了使 Java 应用能够更方便、更有效地管理和控制操作系统级别的进程,Java 9 引入 Process API,其目的是为了提供更好的控制和管理操作系统进程的能力,并使其在不同操作系统上的行为更加一致。其主要内容包括:

  • 增强的 Process 类:Java 9 增强了 Process 类,提供了更多方法来管理和控制进程。
  • ProcessHandle 接口:引入了 ProcessHandle 接口,它提供了获取进程的 PID(进程标识符)、父进程、子进程、进程状态等信息的能力。
  • 流式 API:利用流式 API,可以更方便地处理进程的信息和状态。

下面是获取本地所有进程的相关信息:

    @Test
    public void processHandleTest() {
        ProcessHandle.allProcesses() // 获取所有进程
                .forEach(processHandle -> {
                    System.out.printf("Process ID: %s, Command: %s, Start Time: %s, User: %s%n",
                            processHandle.pid(),                                                  // 获取进程ID
                            processHandle.info().command().orElse("Unknown"),               // 获取进程的命令信息
                            processHandle.info().startInstant()
                                    .map(i -> i.toString()).orElse("Unknown"),              // 获取进程的启动时间
                            processHandle.info().user().orElse("Unknown"));                 // 获取运行进程的用户
                });

    }

执行结果(部分):

img

JEP 264:平台日志 API 和 服务

在 Java 9 之前,JDK 内部使用了多种日志记录机制,比如 System.out.printlnjava.util.logging,这种方法缺乏统一性,使得维护和控制日志变得复杂。因此,需要一个统一的日志系统,使 JDK 自身的日志更加一致和可管理。

Java 9 引入该特性其主要目的是为 JDK 提供一个统一的日志系统,它能够通过不同的日志框架来捕获 JDK 内部的日志信息。这不仅简化了 JDK 自身的日志处理,也为开发者提供了更大的灵活性和控制力,使得他们能够更好地管理和监控 JDK 产生的日志信息。

主要内容:

  1. 新的日志API:引入了一组新的日志API,称为 System.Logger API,用于 JDK 内部日志记录。
  2. 日志级别支持:支持不同的日志级别,例如 ERROR, WARNING, INFO, DEBUG, 和 TRACE。
  3. 日志服务接口:定义了一个服务接口,允许替换JDK的日志记录系统,或者将其桥接到其他日志框架。

下面代码是该日志 API 的示例:

    @Test
    public void loggerTest() {
        System.Logger logger = System.getLogger("Java9Test");
        logger.log(System.Logger.Level.INFO, "这是 INFO 级别");
        logger.log(System.Logger.Level.WARNING, "这是 WARNING 级别");
        logger.log(System.Logger.Level.ERROR, "这是 ERROR 级别");
    }

它带来如下几个好处:

  1. 更好的日志管理:统一的API使日志管理变得更加简单和一致。
  2. 灵活性和可扩展性:开发者可以根据需要选择不同的日志实现,提高了灵活性和可扩展性。
  3. 更好的集成:使得JDK与现代日志框架(如 SLF4J, Log4j)之间的集成更加容易和无缝。

JEP 266: 反应式流(Reactive Streams)

在传统的阻塞 IO 和同步处理模式中,处理大量数据流或异步操作时常常面临效率和响应性问题。反应式编程是一种更有效地处理异步数据流的编程范式。Java 9 之前,并没有标准的方式在 Java 中实现反应式编程。因此 Java 9 引入反应式流,其目的是提供一种在 Java 中处理异步数据流的标准方式,同时保证高效率、低延迟,并支持背压(back-pressure),即允许接收者控制数据流的速度,防止被快速生产的数据淹没。

主要内容为:

  • 引入了

    java.util.concurrent.Flow
    

    类,它包含了几个嵌套的静态接口:

    Publisher
    

    Subscriber
    

    Subscription
    

    Processor
    

    • Publisher:一个数据流的生产者。
    • Subscriber:订阅 Publisher 并处理数据的消费者。
    • Subscription:连接 PublisherSubscriber,允许 Subscriber 控制数据流。
    • Processor:充当生产者和消费者的中间人,即 PublisherSubscriber 的组合。

JEP 224: HTML5 Javadoc

在 Java 9 之前,Javadoc 主要使用较老的 HTML 4 格式。随着 Web 标准的发展,特别是 HTML5 的普及,有必要更新 Javadoc 以支持更现代的 Web 技术和标准。

Java 9 引入 HTML5 Javadoc,主要目的是将 Javadoc 文档更新为使用 HTML5,从而提高文档的兼容性、可用性和可访问性。这包括支持更多现代浏览器的特性,提供更好的布局和样式,以及改善搜索功能。

JEP 238: 多版本兼容 JAR 文件

在 Java 9 之前,一个 JAR 文件只能包含针对一个特定 Java 版本编译的类文件。随着 Java 平台的不断发展和版本的迭代,这限制了库和应用程序在不同 Java 版本间的兼容性。

Java 9 引入该特性的主要目的是提供一种机制,使得库开发者可以在单个 JAR 文件中包含针对不同 Java 版本编译的类文件。这样,应用程序可以在不同的 Java 运行时环境中运行,而无需更改或重新打包。

JEP 277:改进的弃用注解 @Deprecated

@Deprecated 注解用于标记过时的 API,但它并没有提供关于 API 为何过时、何时过时以及替代方案等信息。这导致开发者在使用或维护这些 API 时缺乏足够的信息。Java 9 对其进行了改进,增加了两个的属性:

  • since 属性用于指明从哪个版本开始 API 被弃用。
  • forRemoval:指出这个 API 是否计划在未来的版本中被移除。

该项特性可以让开发者能够更清晰地了解 API 的状态和未来规划,比如是否继续使用该 API、寻找替代方案。

JEP213:改进钻石操作符(Diamond Operator)

在 Java 7 中引入的钻石操作符简化了泛型实例的创建,但它不能用于匿名内部类。由于这个限制,开发者不得不在使用匿名内部类时指定泛型参数,这增加了代码的冗余和复杂性。

在 Java 9 中改进了钻石操作符,它可以与匿名内部类一起使用。这意味着当我们在创建一个匿名内部类的实例,并且该类具有泛型参数时,我们可以省略这些参数,Java 编译器会根据上下文推断出正确的类型。

增强 CompletableFuture

CompletableFuture 是在 Java 8 中引入的,它是一个非常强大的用于异步编程的工具,但是在实际使用过程中,发现它还有一些改进空间。故而,Java 9 对其进行了一些增强,内容如下:

  • 新增方法
    • completeAsync():允许异步地完成 CompletableFuture。它受一个 Supplier 函数和可选的 Executor,用于异步生成结果。
    • orTimeout():为 CompletableFuture 添加超时功能。如果在指定的时间内未完成,CompletableFuture 将会被异常地完成。
    • completeOnTimeout():类似于 orTimeout(),但在超时发生时,它会使用提供的值来完成 CompletableFuture,而不是抛出异常。
  • 改进异常处理
    • exceptionallyCompose():它允许在 CompletableFuture 遇到异常时,构建并返回一个新的 CompletionStage,这为异常处理提供了更多的灵活性。
  • 增强的组合操作
    • delayedExecutor(): 这是一个工具方法,用于创建一个延迟执行任务的 Executor。它可以和其他 CompletableFuture 方法结合使用,实现延迟执行的效果。
    • minimalCompletionStage()** 和 **completeMinimalFuture(): 这两个方法分别用于创建一个具有最小完成状态的 CompletionStage,以及从 CompletionStage 创建一个 CompletableFuture。这些方法有助于在不需要 CompletableFuture 完整功能的场景中减少资源消耗。

Java 9 新特性—模块化

Java 9 中最大的特性毫无疑问就是模块化,其实模块化的概念在 Java 7 的时候就已经提出来了,由于它的复杂性,不断跳票,从 Java 7 到 Java 8 ,最后 Java 9终于姗姗来迟,它的出现犹如壮士断腕。

那模块化到底是什么呢?在实际开发中又有什么用呢?这篇文章大明哥带你彻底了解 Java 9 的模块化。

img

什么是模块化

模块是 Java 9 中新增的一个组件,官方是这么定义它的:**一个被命名的,代码和数据的自描述集合。( the module, which is a named, self-describing collection of code and data)。**怎么理解呢?我们可以简单地将它理解为 package 的上一级单位,是多个 package 的集合。

我们知道在 Java 中, Java 文件是最小的可执行文件,为了更好地管理这些 Java 文件,我们需要用 package 将同一类的 Java 文件统一管理起来,多个 package 文件、Java 文件可以打包成一个 jar 文件,现在 Java 9 在 package 上面增加 module,一个 module 可以包含多个 package,所以从代码结构上来看层级关系是这样的:jar > module > package > java 文件

所以,从本质上来说,模块就是用来管理各个 package 的组件,它的概念,其实就可以理解为在 package 上面再包一层,包这一层的主要目的是让我们能够更好地组织和管理 Java 应用程序的代码,以及更好地控制代码的可见性和依赖关系。

要掌握模块化,就需要理解它的几个核心概念:

  1. 模块(Module):模块是模块化系统的基本单元。它是一个逻辑上独立的代码单元,包括类、接口、资源和module-info.java文件。每个模块都有一个唯一的名称,例如:"java.base"、"com.example.myapp"等。
  2. 模块路径(Module Path):模块路径是一组包含模块的路径,用于在运行时指定应用程序所需的模块。类似于类路径,但它是用于模块。
  3. module-info.java 文件:每个模块都包含一个特殊的文件,名为module-info.java。这个文件描述了模块的信息,包括模块名称、依赖关系、导出的包以及其他模块信息。
  4. 模块依赖性(Module Dependencies):在module-info.java文件中,可以使用requires关键字声明模块之间的依赖关系。
  5. 模块导出(Module Exporting):在module-info.java文件中,可以使用 exports 关键字声明哪些包可以被其他模块访问,这有助于控制包的可见性。

模块化怎么体现的呢?下图是 Java 8 与Java 9 的目录结构:

img

img

从上图中你会发现 Java 9 中没有jre,没有rt.jar,没有tools.jar,而是多了一个 jmods,该文件夹下都是一个一个的模块:

img

对于 Java 9 之前的工程,他们都是单体模式,一个简单的 hello world,都需要引入 rt.jar,导致这个简单的 hello world 的 jar 变得很大,而 Java 9 引入模块后,它只需要引入它所依赖的即可。

为什么需要模块化

在 Java 9 之前我们没有使用模块化之前用起来还是很顺手的,现在突然在 package 上面增加一层 module,势必会增加我们编码的复杂度,既然增加了复杂度为什么还要引入呢?其实引入模块化有着几个非常好的优势。

1、显式管理依赖

模块化系统需要我们明确申请模块之间的依赖关系,它减少了传统类路径(classpath)上的混乱和不稳定性。每个模块都需要显示声明自己需暴露的 package,而自己所依赖的和自己内部使用的 package,则不会暴露,也不会被外部依赖,这有助于保护内部实现,防止不应该公开的部分被外部模块访问。依赖的模块也需要显示引入需要依赖的 package

2、更好地安全性

模块化系统可以提供更严格的可见性控制,防止私有实现被不应访问的模块访问,从而增强了应用程序的安全性。代码真正意义上可以按照作者的设计思路进行公开和隐藏,同时也限制了反射的滥用,更好的保护了那些不建议被外部直接使用或过时的内部类。

3、标准化

模块化引入了标准化的方式来组织和管理代码。显示的声明暴露的内容,可以让第三方库的开发者更好地管理自己的内部实现逻辑和内部类。

4、自定义最小运行时映像

Java因为其向后兼容的原则,不会轻易对其内容进行删除,包含的陈旧过时的技术也越来越多,导致JDK变得越来越臃肿。而Java 9的显示依赖管理使得加载最小所需模块成为了可能,我们可以选择只加载必须的JDK模块,抛弃如java.awt, javax.swing, java.applet等这些用不到的模块。这种机制,大大的减少了运行Java环境所需要的内存资源,在对于嵌入式系统开发或其他硬件资源受限的场景下的开发非常有用。

5、更加适合大型应用程序管理

于大型应用程序,模块化系统提供更好的组织结构,减少了复杂性,使开发者能够更轻松地管理和扩展应用程序。

6、更好的性能

通过减少不必要的类路径搜索和提供更紧凑的部署单元,模块化系统有助于提高应用程序的性能。

怎么用模块化

使用 Java 9 的模块化主要分为以下几个步骤。

1、创建模块

创建一个 modulemodule 下面包含该模块的代码和 module-info.java文件。module-info.java文件是每个模块的关键组成部分,它描述了模块的信息,包括名称、依赖关系和导出的包。

这里我们新建两个 modulejava-module-01java-module-02,同时 java-module-01 依赖 java-module-02,如下:

img

这里可能有小伙伴不知道怎么新建 module-info.java,其实只需要在 java 下面右键 new 就可以了:

img

如果这里没有,则表示你 modulejdk 没有配置好,配置下就可以了:

img

2、定义模块信息

新建模块后,我们就需要在 module-info.java 中定义模块信息了,信息主要包括如下几个部分:

  1. 使用module关键字定义模块,并指定模块的名称,例如:module java.module01 { }
  2. 使用requires关键字声明模块之间的依赖关系,例如:requires java.sql; 表示模块依赖于java.sql模块。
  3. 使用exports关键字声明模块中哪些包可以被其他模块访问,例如:exports com.skjava.module01.entity; 表示导出com.skjava.module01.entity包。

我们在 java-module-02 定义了 com.skjava.module02.entitycom.skjava.module02.service 两个包,同时将 com.skjava.module02.entity 暴露出去:

module java.module02 {
    exports com.skjava.module02.entity;
}

entity package 新建 UserEntity 类,service package 中新建 UserService 接口:

public class UserEntity {
    private String userName;

    private Integer age;

    public UserEntity(String userName,Integer age) {
        this.userName = userName;
        this.age = age;
    }

}

如果我们在 java-module-01 中不申请引入模块 java.module02,我们是无法使用 UserEntity 这个类的。所以我们在 java-module-01 中的 module-info.java 引入 java.module02

module java.module01 {
    requires java.module02;
}

这个时候我们就可以放心地在 java-module-01 中使用 com.skjava.module02.entity 的内容了:

import com.skjava.module02.entity.UserEntity;

public class UserService {
    public static void main(String[] args) {
        UserEntity user = new UserEntity("大明哥",18);
        System.out.println(user);
    }
}

看 UserEntity 导入的包是不是 com.skjava.module02.entity。那可以使用 com.skjava.module02.service 中的 UserService 呢?不可以,因为 java-module-02 并没有将 com.skjava.module02.service 暴露出去。

这里只是阐述一种比较简单的使用方式,在实际项目中使用情况会更加复杂,这些都需要我们在工作过程中不断地去探索。

Java 9 新特性—REPL 工具:JSheel 命令

熟悉 PythonScala 之类的语言的小伙伴应该知道,他们在很早就已经有了交互式编程环境 REPLRead-Eval-Print Loop),REPL 以交互式的方式对语句和表达式进行求值。REPL 提供了一个交互式的方式,允许开发人员输入代码,立即执行它,无需编译,然后查看执行结果。

Java 在 Java 9 引入 Java 版的 REPL 工具:Java Shell

什么是 JShell

Java Shell 是 Java 在 Java 9 中引入的一个交互式编程工具,它可以让开发人员能够在一个命令行界面即时编写、编辑和执行Java代码片段,而无需创建和编译传统的Java源代码文件。

JShell的目的是提供一个更快速、更便捷的方式来学习和测试Java代码,以及进行原型设计和实验性编程。它主要有如下几个特点:

  1. 即时反馈:可以立即查看代码的输出,不需要等待编译和运行整个Java应用程序。
  2. **代码片段:**您可以编写单个Java表达式、语句或方法,并在JShell中立即执行它们。这对于测试和验证代码的特定部分非常有用。
  3. 自动导入JShell自动导入常见的Java类和包,因此您无需手动导入它们,使代码编写更加简洁。
  4. 脚本化:我们可以将多个命令保存在脚本文件中,并使用JShell执行这些脚本。

JShell 通常用于教学、快速原型设计和测试,以及在 Java 开发中进行快速的探索性编程,但是它不适用于开发大型的Java应用程序。

为什么需要使用 JShell

我们先看我们以前刚刚学习 Java 时写的第一个 hello world 的程序步骤:

  1. 打开 idea,并新建一个 java-study 的工程
  2. 配置该工程的 Java 环境
  3. 新建一个 hello world 的 Java 类
  4. 编译 Java 文件,并修复各种问题
  5. 运行程序

该步骤比较繁琐,而使用 JShell 工具,我们可以一次输入一个程序元素,立即看到执行结果,并根据需要进行调整。

JShell对于初学者非常有用,因为它允许开发者快速编写和测试 Java 代码片段,无需担心完整项目结构和编译过程。这有助于新手更容易地理解 Java 语言的基础概念。同时由于不需要创建完整的应用程序,这可以加速原型设计过程,帮助我们更快地验证概念。

但是 JShell 不能代替 IDE,它只能作为辅助和便捷的工具而已。

使用 JShell

要使用 JShell 必须安装 JDK 9 或更高版本。

启动 JShell

使用 jshell 命令即可启动 JShell,如果启动不了,可能是环境变量配置配置好,可以进入“ bin ”目录,通过该命令启动 JShell

 chenssy@chenssydeMacBook-Pro  ~  jshell
|  欢迎使用 JShell -- 版本 17.0.8
|  要大致了解该版本, 请键入: /help intro

启动 JShell 我们就可以在该窗口测试代码了。

在 JShell 中我们可以直接声明变量创建方法调用方法

声明变量

jshell> int a = 1;
a ==> 1

jshell> int b = 2;
b ==> 2

这里声明了两个变量 a 和 b。

创建方法

jshell> int add(int x,int y) {
   ...>     return x + y;
   ...> }
|  已创建 方法 add(int,int)

新建了一个 add() 方法;

调用方法

我们可以直接利用上面的变量a 、b 来调用 add():

jshell> int c = add(a,b)
c ==> 3

运行表达式

JShell 还可以直接输入 Java 表达式,计算出表达式的结果,然后返回。

jshell> 1 + 2
$6 ==> 3

jshell> 1 > 2 ? "死磕 Java 并发" : "死磕 Java 新特性"
$7 ==> "死磕 Java 新特性"

jshell>

创建 Java 类

JShell 可以新建 Java 类,然后调用它。

jshell> public class SKTest{
   ...>     public void test(){
   ...>         System.out.println("死磕 Java 就是牛");
   ...>     }
   ...> }
|  已创建 类 SKTest

jshell> SKTest skTest = new SKTest();
skTest ==> SKTest@17a7cec2

jshell> skTest.test();
死磕 Java 就是牛

定义接口

我们还可以定义接口,并实现它。

jshell> public interface UserService{
   ...>     void test();
   ...> }
|  已创建 接口 UserService

jshell> public class UserServiceImpl implements UserService{
   ...>     public void test(){
   ...>         System.out.println("死磕 Java 新特性就是牛...");
   ...>     }
   ...> }
|  已创建 类 UserServiceImpl

jshell> UserService userService = new UserServiceImpl();
userService ==> UserServiceImpl@4783da3f

jshell> userService.test();

其他的情况大明哥就不一一介绍了,个人感觉这个工具还是显得有点儿鸡肋,比如你新建一个类,语法错了需要重新写,这就很鸡肋了,不过 Mac book 的 iTerm 倒是可以回退整块语句,也算方便了不少。

JShell 的主要命令

JShell 提供了一系列主要命令,用于在交互式环境中执行各种任务和操作。以下表格是一些常用的命令:

命令描述
/help (或 /?)查看可用命令的帮助信息,或获取特定命令的详细信息。
/list (或 /l)列出当前已经输入的代码片段。
/edit (或 /e)编辑以前输入的代码片段。
/drop (或 /d)删除以前输入的代码片段。
/save (或 /s)将当前会话中的代码保存到文件中。
/open (或/o)从文件中加载代码以继续会话。
/reset重置JShell环境,清除所有已输入的代码片段。
/vars (或 /v)查看当前定义的所有变量。
/methods (或 /m)查看当前定义的所有方法。
/types (或 /t)查看当前定义的所有类型。
/imports (或/i)查看当前导入的包和类。
/set (或 /s)设置JShell的各种选项,如类路径、编辑器、输出格式等。
/classpath (或 /cp)查看或设置类路径,以便加载外部类和库。
/history (或 /h)查看和搜索JShell历史记录。
/save (或 /s)JShell历史记录保存到文件中,以便将其用于以后的会话。

Java 9 新特性—接口支持私有方法

从我们一接触接口开始我们就知道 Java Interface 接口中是不能定义 private 私有方法的,但是这个在 Java 9 中被打破了。

为了减少代码重复,提高代码的可维护性,Java 9 接口支持私有方法,这样做有几个好处:

  1. 接口更好地演化:在不破坏现有实现的前提下,我们可以向接口中添加默认方法,这是接口演化的一种方式。然后,如果默认方法之间有重复代码,就会导致代码容易重复。在接口中定义私有方法,可以将这些重复的代码抽离出来,使接口更容易维护和扩展。
  2. 代码复用:将重复的,通用的功能封装在私有方法中,私有方法可以被接口中的其他方法调用,这有助于减少代码重复和提高代码的可维护性。
  3. 防止子类滥用:由于私有方法只能在接口内部使用,无法在接口的实现类中被滥用,这有助于保持接口的一致性和约定。

在接口中使用私有方法有如下几个限制:

  1. 接口私有方法不能是抽象的。
  2. 私有方法只能在接口内部使用,无法被接口的实现类或外部类访问。
  3. 私有方法不会继承给接口的子接口,每个接口都必须自己定义自己的私有方法。
  4. 私有静态方法可以在其他静态和非静态接口方法中使用。
  5. 私有非静态方法不能在私有静态方法内部使用。

下面演示下怎么在接口中使用私有方法。

定义接口 MyInterface

public interface MyInterface {

    /**
     * 抽象方法
     */
    void publicMethod();

    /**
     * 默认方法
     */
    default void defaultMethod() {
        System.out.println("defaultMethod");

        // 调用静态方法
        privateMethod();

        // 调用私有静态方法
        privateStaticMethod();
    }

    /**
     * 静态方法
     */
    private void privateMethod() {
        System.out.println("privateMethod");
    }

    /**
     * 静态方法
     */
    static void staticMethod() {
        System.out.println("staticMethod");

        // 调用私有静态方法
        privateStaticMethod();
    }

    /**
     * 私有静态方法
     */
    private static void privateStaticMethod() {
        System.out.println("privateStaticMethod");
    }
}

实现类:MyInterfaceImpl

public class MyInterfaceImpl implements MyInterface {
    @Override
    public void publicMethod() {
        System.out.println("publicMethod");
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        // 调用实例方法
        MyInterface myInterface = new MyInterfaceImpl();
        myInterface.publicMethod();
        
        // 调用默认方法
        System.out.println("");
        myInterface.defaultMethod();

        // 调用静态方法
        System.out.println("");
        MyInterface.staticMethod();
    }
}

// 结果......
publicMethod

defaultMethod
privateMethod
privateStaticMethod

staticMethod
privateStaticMethod

通过引入私有方法,Java 9 增强了接口的功能,使其更加灵活,能够更好地满足不同的编程需求,特别是在接口的演化和维护方面提供了更多的选项。这有助于减少代码重复,提高代码的可维护性,并使接口的设计更加清晰和一致。

Java 9 新特性—String 底层存储结构变更

大明哥相信绝大数小伙伴一定看过 Java 8 的 String 源码,对于它的底层存储结构一定不陌生,在 Java 9 之前,String 的底层存储结构都是 char[]

public final class String
     implements java.io.Serializable, Comparable<String>, CharSequence {

  //The value is used for character storage.
  private final char value[];

}

每个 char 都以 2 个字节存储在内存中。然而 Oracle 的 JDK 开发人员调研了成千上万个应用程序的 heap dump 信息,他们注意到大多数字符串都是以 Latin-1 字符编码表示的,它只需要一个字节存储就够了,两个字节完全是浪费,这比 char 数据类型存储少 50%(1 个字节)。

所以,在 Java 9 中将 String 的底层存储结构调整为 byte[]:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {

    @Stable
    private final byte[] value;

    private final byte coder;
}

Java 9 这样调整的目的是减小字符串的内存占用,这样带来了两个好处:

  1. 节省内存:对于包含大量ASCII字符的字符串,内存占用大幅减少,因为每个字符只占用一个字节而不是两个字节。
  2. 提高性能:由于字符串的存储结构与编码方式更加紧凑,字符串操作的性能也有所提高。

需要注意的是,这仅仅只是底层数据结构的变化,对于我们上层调用者完全是透明的,不会有任何影响,String 的方法以前怎么使用,现在还是怎么使用,例如:

public class StringTest {
    public static void main(String[] args) {
        String skString1 = "skjava";
        String skString2 = "死磕Java";
        System.out.println(skString1.charAt(0));
        System.out.println(skString2.charAt(0));
    }
}
// 结果......
s
死

charAt() 源码如下:

    public char charAt(int index) {
        if (isLatin1()) {
            return StringLatin1.charAt(value, index);
        } else {
            return StringUTF16.charAt(value, index); 
        }
    }

isLatin1() 用于判断编码格式是否为 Latin-1 字符编码,如果是则调用 StringLatin1.charAt(),否则调用 StringUTF16.charAt()。这里为什么要判断字符编码呢?Latin-1 字符编码也称 ISO 8859-1,它包括了拉丁字母(包括西欧、北欧和南欧语言的字母)以及一些常见的符号和特殊字符,但是它并不支持其他非拉丁字母的语言,例如希腊语、俄语或中文,对于这些我们只能使用其他字符编码了。

在 Java 9 中,String 支持的字符编码格式有两种:

  1. Latin-1Latin-1 编码用于存储只包含拉丁字符的字符串。它采用了一字节编码,每个字符占用一个字节(8位)。
  2. UTF-16UTF-16 编码用于存储包含非拉丁字符的字符串,以及当字符串包含不适合 Latin-1 编码的字符时。

在 Java 9 中,String 多了一个成员变量 coder,它代表编码的格式,0 表示 Latin-1 ,1 表示 UTF-16,我们在看 skString1skString2

img

从这张图可以清晰地看到 “skjava” 的字符编码是 Latin-1,而 “死磕Java” 的字符编码则是 UTF-16。不同的字符编码选择不同的方法来获取。其实你看下 String 里面的方法都是这种模式。

所以,Java 9 中的 String 使用 ***Latin-1*** 和 ***UTF-16*** 两种字符编码方式,根据字符串的内容来选择合适的编码格式,以便在内部存储时提高效率。

但是,有小伙伴就喜欢硬扛,我就不喜欢 Latin-1,可以完全用UTF-16 么 ?可以。Java 满足你的一切不合理的要求。

-XX:-CompactStrings:禁用精简字符串特性

  1. 如果启用 Compact Strings(默认情况),JVM 会根据字符串的内容来选择 Latin-1 还是 UTF-16,以在内存中有效地存储字符串,减小内存占用。
  2. 如果禁用 Compact Strings(使用 -XX:-CompactStrings),JVM 将始终使用 UTF-16 编码来存储字符串。

一般来说,我们是不需要显式设置 -XX:-CompactStrings,开启 Compact Strings 能够帮组我们节约内存和提高性能。

Java 9 新特性—Optional 的增强

在文章 Optional 解决NullPointerExceptionopen in new window 大明哥详细的介绍如何利用 Optional 来解决 NullPointerException 。然后 Java 9 对它又进行了一些增强,以提高其实用性和易用性。主要是增加三个方法:

  • or()
  • ifPresentOrElse()
  • stream()

下面就一一介绍这三个方法。

or()

or() 方法定义如下:

public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier);

方法接受一个 Supplier,当 Optional 为空时,将执行该 Supplier,以获取另一个 Optional 对象。这使得在处理 Optional 时更加灵活,可以轻松地提供备用值或计算。

    @Test
    public void orTest() {
        Optional<String> optional1 = Optional.of("死磕 Java 新特性");
        Optional<String> optional2 = Optional.empty();
        System.out.println("optional1 value:" + optional1.or(()->Optional.of("死磕 Java")).get());
        System.out.println("optional2 value:" + optional2.or(()->Optional.of("死磕 Java")).get());
    }
// 结果......
optional1 value:死磕 Java 新特性
optional2 value:死磕 Java

ifPresentOrElse()

Java 8 有一个 ifPresent(),它用于在 Optional 包含非空值时执行指定的操作,Java 9 对其的改进就是增加了一个 else

ifPresentOrElse() 方法定义如下:

public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction);

它接受两个参数:

  1. action:是一个 Consumer 函数式接口,用于在 Optional 包含非空值时执行的操作。
  2. emptyAction:是一个 Runnable 函数式接口,用于在 Optional 为空时执行的操作,通常是一些默认操作或错误处理。

使用 ifPresentOrElse() 方法,你可以避免手动编写空值检查代码,从而更容易地处理可能为空的情况。例如:

    @Test
    public void ifPresentOrElseTest() {
        Optional<String> optional = Optional.empty();
        optional.ifPresentOrElse(value -> System.out.println("optional value:" + optional.get()),
                () -> System.out.println("optional value is null"));
    }
// 结果......
optional value is null

该方法可以让我们不需要显式地编写 if...else 来检查 Optional 是否为空,从而提高了代码的可读性和可维护性,是比较实用的一个方法。

stream()

stream() 允许我们将 Optional 对象转化为一个流 (Stream),以便能够更灵活地处理包含或不包含值的情况。方法定义如下:

Stream<T> stream()

该方法允许我们执行以下操作:

  • 如果 Optional 包含非空值**:** stream() 会创建一个包含这个值的流。
  • 如果 Optional 为空**:** stream() 会创建一个空流。

有了 Stream 后,我们就可以利用 Stream 的各种功能来处理 Optional 中的值,例如映射、过滤、转换等等。例如:

    @Test
    public void streamTest() {
        Optional<String> optional = Optional.of("死磕 Java");
        optional.stream()
                .filter(o -> o.startsWith("死磕"))
                .map(val -> val + " —— https://skjava.com/")
                .forEach(System.out::println);
    }
// 结果......
死磕 Java-https://skjava.com/

再入:

    @Test
    public void streamTest1() {
        List<Optional<String>> list = List.of(
                Optional.of("死磕 java 并发"),
                Optional.of("死磕 java 新特性"),
                Optional.empty(),
                Optional.of("死磕 Netty"),
                Optional.empty()
        );
        list.stream()
                .flatMap(Optional::stream)
                .filter(o -> o.startsWith("死磕"))
                .map(val -> val + " —— https://skjava.com/")
                .forEach(System.out::println);
    }
// 结果......
死磕 java 并发 —— https://skjava.com/
死磕 java 新特性 —— https://skjava.com/
死磕 Netty —— https://skjava.com/

通过 stream() 可以让我们更加流畅地处理 Optional 中的值,而不需要额外的空值检查代码。

Java 9 新特性—try-with-resources的升级

只要学过 Java 的小伙伴都需要知道,资源用了是要关闭的,否则会发生大麻烦,轻则被骂,重则滚蛋,不要以为是玩笑话,很严重的!!

img

Java 7 之前,资源需要手动关闭

我相信有5 年以上工作经验的小伙伴一定写过这样的代码:

FileInputStream fileInputStream = null;
try {
    fileInputStream = new FileInputStream("file.txt");
    // 读取文件内容
} catch (IOException e) {
    // 处理异常
} finally {
    if (fileInputStream != null) {
        try {
            fileInputStream.close();
        } catch (IOException e) {
            // 处理关闭异常
        }
    }
}

这是因为,在 Java 7 之前,需要我们手动关闭各种资源,包括文件、数据库连接、网络套接字等等其他可能需要显式关闭的资源。但是这种关闭资源的方式存在几个问题:

  • 容易忽略关闭资源的步骤,导致资源泄漏。
  • 需要编写冗长的 try-catch-finally 代码,降低了代码的可读性。
  • 异常处理变得复杂,需要额外的嵌套 try-catch 块来处理关闭资源时可能出现的异常。

Java 7 中的 try-with-resources

为了解决上面的问题,Java 7 引入了 try-with-resources 语法。try-with-resources 是一个用于简化资源管理的语法糖,它可以自动关闭实现了 AutoCloseableCloseable 接口的资源,无需显式编写资源关闭代码。这种特性有如下几个优点:

  • 资源自动化管理,减少了资源泄漏的风险。
  • 减少了编写冗长的 try-catch-finally ,简化了代码,提高了代码的可读性和可维护性。
  • 自动处理资源关闭时的异常。

使用 try-with-resources 的步骤如下:

1、资源必须实现 ***AutoCloseable*** 或 ***Closeable*** 接口

要使用 try-with-resources,资源必须是实现了 AutoCloseable(Java 7 引入)或 Closeable( Java 5 引入)接口的类的实例。这些接口要求资源类提供一个 close 方法,用于在不再需要资源时执行清理操作。例如:

public class Resource implements AutoCloseable{
    
    public void doSome() {
        System.out.println("do something....");
    }
    
    @Override
    public void close() throws Exception {
        System.out.println("关闭了 Resource 资源");
    }
}

2、语法

try-with-resources 使用 try 块来声明和初始化资源。资源的声明必须在圆括号内,资源会在 try 块退出时自动关闭。此语法还可以包括一个 catch 块来处理可能抛出的异常。例如:

public class ResourceTest {
    public static void main(String[] args) {
        try(Resource resourceTest = new Resource()) {
            resourceTest.doSome();
        } catch (Exception e) {
            // 处理异常
        }
    }
}

3、自动关闭资源

try 块退出时,资源会自动关闭,即使在 try 块内发生异常也会如此。资源的 close() 会被调用以释放资源,并且资源的关闭异常会被抛出或被抑制(附加到原始异常中)。例如上诉代码执行结果如下:

do something....
关闭了 Resource 资源

不管是正常退出还是因为异常退出,都会自动调用资源的 close() 来释放资源。

4、多个资源

我们可以在同一个 try-with-resources 语句中管理多个资源,它会按照声明的顺序被初始化和关闭。多个资源的声明使用分号分隔。如下:

try (Resource1 resource1 = new Resource1();
     Resource2 resource2 = new Resource2();
     Resource3 resource2 = new Resource3()) {
     // 使用 resource1、resource2、resource3
} catch (Exception e) {
    // 处理异常
}
// 在这里,resource1、resource2、resource3 都会自动关闭

但是资源的关闭顺序与它们的声明顺序相反,即最后声明的资源最先关闭。

public class ResourceTest {
    public static void main(String[] args) {
        try(Resource1 resource1 = new Resource1();
            Resource2 resource2 = new Resource2();
            Resource3 resource3 = new Resource3()) {

        } catch (Exception e) {
            // 处理异常
        }
    }
}
// 结果......
关闭了 Resource3 资源
关闭了 Resource2 资源
关闭了 Resource1 资源

Java 9 对 try-with-resources 的改进

Java 7 虽然利用 try-with-resources 来优化了资源的管理,无须我们手动关闭资源,但是它依然是不够完美,它要求管理的资源必须在 try 子句中初始化,否则编译不通过,如下:

img

为了解决这个问题,Java 9 对其改进了:**如果你已经有一个资源是 final 或等效于 final 变量,您可以在 try-with-resources 语句中使用该变量,而无需在 try-with-resources 语句中声明一个新变量。**例如我们将编译环境调整到 Java 9 及以上去:

img

执行结果:

关闭了 Resource3 资源
关闭了 Resource2 资源
关闭了 Resource1 资源

通过这个例子我们就可以看到 Java 在不断地进步,对 Java 的未来充满信心!!!

Java 9 新特性—Stream API的增强

我们知道 Java 8 强势推出 Stream API,让我们能够以一种更加简洁、易读、高效的方式来处理集合数据,大大地提高了我们的生产力,在文章 Stream API 对元素流进行函数式操作open in new window,大明哥对 Stream API 做了非常详细的说明,各位小伙伴可以去阅读阅读。

为了提高 Stream API 的灵活性和性能,Java 9 对 Stream API 做了一些增强,主要体现在以下几个方法:

  1. 新增 ofNullable()
  2. 重载 iterate()
  3. 新增 dropWhile()takeWhile()

img

新增 ofNullable()

ofNullable() 用于创建一个 Stream,其中包含一个非空元素或者为空。该方法的主要目的是简化处理可能包含 null 值的集合时的代码,以便避免显式地检查和过滤 null 值。其定义如下:

    public static<T> Stream<T> ofNullable(T t) {
        return t == null ? Stream.empty()
                         : StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
    }
  • 如果参数 t 不为 null,则返回一个包含该非空元素的单元素 Stream。
  • 如果参数 t 为 null,则返回一个空的 Stream。

这就意味着我们可以使用 Stream.ofNullable() 方法来快速创建一个只包含非空元素的 Stream,且无需担心处理 null 值的边界情况。比如,我们有一个 List 包含一些可能为 null 的字符串,我们可以使用 Stream.ofNullable() 来创建一个只包含非空字符串的 Stream:

    @Test
    public void ofNullableTest() {
        List<String> list = Arrays.asList("死磕 Java",null,"死磕 Java 新特性","死磕 Netty",null,null);
        list.stream()
            .flatMap(Stream::ofNullable)
            .forEach(System.out::println);
    }

重载 iterate()

我们知道 Java 8 中的 iterate() 用于创建一个无限流,其元素由给定的初始值和一个生成下一个元素的函数产生,为了终止流我们需要使用一些限制性的函数来操作,例如 limit()

    @Test
    public void iterate() {
        Stream.iterate(1,v -> v + 2)
                .limit(10)
                .forEach(System.out::println);
    }

在 Java 9 中为了限制该无序流的长度,增加了一个谓词,方法定义如下:

static <T> Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)
  • seed:初始元素,作为序列的第一个元素。
  • hasNext:用于在生成元素时限制序列的长度。如果 hasNext 返回 true,则next 函数就会继续生成下一个元素;一旦 hasNext 返回 false,序列生成将停止。

例如:

    @Test
    public void iterateTest() {
        Stream.iterate(1,n -> n < 20,v -> v + 2)
                .forEach(System.out::println);
    }

当生成的值大于等于 20 时,序列生成就停止。

该重载方法允许我们更加方便地生成元素序列,并在需要时限制序列的长度,这在处理无限序列的情况下非常有用。

新增 dropWhile() 和 takeWhile()

Java 9 引入 dropWhile()takeWhile(),这两个方法允许我们根据谓词条件从流中选择或删除元素,直到遇到第一个不满足条件的元素。

dropWhile()takeWhile(),用于处理流中的前缀元素,而不必处理整个流,为处理 Stream 流提供了更多的灵活性。

dropWhile()

dropWhile() 方法定义如下:

dropWhile(Predicate<? super T> predicate)
  • 方法接受一个 Predicate(谓词)作为参数。
  • 方法会从流的开头开始,跳过满足谓词条件的所有元素,直到找到第一个不满足条件的元素。一旦找到第一个不满足条件的元素,dropWhile() 将停止跳过元素,并返回一个新的流,其中包含了剩余的元素。
  • 如果流的所有元素都满足谓词条件,那么返回一个空流。

示例:

    @Test
    public void dropWhileTest() {
        Stream<Integer> stream = Stream.of(1,2,3,4,5,3,2,6,7,8);
        stream.dropWhile(x -> x < 5)
                .forEach(System.out::println);
    }
    }
// 结果.....
5
3
2
6
7
8

谓词条件 x < 5,即 dropWhile() 会跳过小于 5 的元素, 一旦找到第一个不小于 5 的元素(5),它将停止跳过并返回包含剩余元素的新流(5,3,2,6,7,8)。

takeWhile()

takeWhile() 则与 dropWhile() 相反,它是从流的开头开始,选择满足谓词条件的所有元素,直到找到第一个不满足条件的元素,一旦找到该元素,将会停止选择元素,并返回一个新的流,其中包含了前缀元素。注意两个方法的用词区别,一个是跳过(dropWhile()),一个是选择(takeWhile())。

如果流的所有元素都满足谓词条件,它将返回原始流的一个副本。

示例:

    @Test
    public void takeWhileTest() {
        Stream<Integer> stream = Stream.of(1,2,3,4,5,3,2,6,7,8);
        stream.takeWhile(x -> x < 5)
                .forEach(System.out::println);
    }
// 结果......
1
2
3
4

看到没,同样的示例,只是方法不一样,效果就完全相反。

Java 9 新特性—新增只读集合和工厂方法

对于大多数小伙伴而言,其实是很少使用只读集合的,甚至有蛮大一部分小伙伴可能都没有听过这个,大部分使用集合无非就是如下两种方式:

List<String> list = new ArrayList();
or
List<String> list = getXxxList();

其实只读集合在编程中还是蛮有用处的,首先只读就意味着不可变,即线程安全,在使用它的过程中我们无需使用同步机制来保护其内部状态。同时只读集合通常比可变集合更加高效。因为它们是不可变的,不需要支持修改操作,因此在内部数据结构上可以进行优化,这可以提高数据访问的速度和降低内存开销。

这篇文章我们来看看 Java 引入的一项重要特性—只读集合和工厂方法。

Java 8 创建不可变集合

在 Java 9 之前要创建一个只读集合,通常需要通过将可变集合转换为不可变集合的方式来实现,即使用Collections.unmodifiableXXX(),其中XXX可以是ListSetMap,例如要创建一个只读 List:

    @Test
    public void test() {
        List<String> list = new ArrayList<>();
        list.add("死磕 Java 新特性");
        list.add("死磕 Netty");

        // 转换为只读集合
        list = Collections.unmodifiableList(list);
    }

使用 Collections.unmodifiableList() 将 List 转换为不可变集合,如果我们使用 add() 来添加元素,会报 UnsupportedOperationException 异常信息

这种方式虽然有效,但是比较麻烦,它需要额外的步骤来处理,而且容易出错。

Java 9 创建不可变集合

为了解决 Java 8 的问题,Java 9 引入不可变集合和对应的工厂方法,目的就在于提供更安全、更高效的方式来创建不可变集合,同时确保原始集合无法被修改。

ListSetMap 都提供了对应的工厂方法来创建不可变集合,我们这里列出 List 的:

static <E> List<E>  of()
static <E> List<E>  of(E e1)
static <E> List<E>  of(E e1, E e2)
static <E> List<E>  of(E e1, E e2, E e3)
static <E> List<E>  of(E e1, E e2, E e3, E e4)
static <E> List<E>  of(E e1, E e2, E e3, E e4, E e5)
static <E> List<E>  of(E e1, E e2, E e3, E e4, E e5, E e6)
static <E> List<E>  of(E e1, E e2, E e3, E e4, E e5, E e6, E e7)
static <E> List<E>  of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8)
static <E> List<E>  of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9)
static <E> List<E>  of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10)
//varargs
static <E> List<E>  of(E... elements)

看到这么多重载方法是不是有点儿懵逼,感觉是不是只需要有 of()of(E... elements) 这两个方法就可以了,从实现的效果上来说,确实是可以。of(E... elements) 确实是一个非常灵活的方法,可以适用于多种情况,但是Java 9 引入多个重载的 List.of() 方法并不是为了提供不同数量的参数选择的灵活性,而是为了性能和可读性的考虑。每个 List.of()方法的重载版本都是针对特定的参数数量进行了优化,以提高性能和代码清晰度。当使用 of(E... elements) 时,Java运行时需要创建一个数组以容纳传递的元素(Java 的可变参数,会被编译器转型为一个数组),这可能会引入一些额外的开销,尤其是在创建小型 List 时。而多个重载的 List.of() 方法避免了这种开销,因为它们直接接受参数,而无需创建数组。所以,看着懵逼,但是性能杠杠的。

不可变的 List 具有如下几个特征:

  1. 这些列表是不可变的。调用任何改变 List 的方法(如add()remove()replaceAll()clear()),都会抛出 UnsupportedOperationException
  2. 它们不允许 null 元素。 尝试添加 null 元素将导致 NullPointerException
  3. 列表中元素的顺序与提供的参数或提供的数组中的元素的顺序相同。

对于 Set 和 Map 大明哥就不介绍了,小伙们自己去探索下!

Java 9 新特性—改进CompletableFuture

CompletableFuture 是 Java 8 中引入用于处理异步编程的核心类,它引入了一种基于 Future 的编程模型,允许我们以更加直观的方式执行异步操作,并处理它们的结果或异常。关于 CompletableFuture 的核心原理请阅读:Java 8 新特性—深入理解 CompletableFutureopen in new window

但是在实际使用过程中,发现 CompletableFuture 还有一些改进空间,所以 Java 9 对它做了一些增强,主要内容包括:

  • 新的工厂方法
  • 支持超时和延迟执行
  • 支持子类化

img

新的工厂方法

在 Java 9 中 CompletableFuture 新增了三个工厂方法。

completedFuture()

此方法用于创建一个已经完成的 CompletableFuture 实例,方法定义如下:

public static <U> CompletableFuture<U> completedFuture(U value)

该方法允许我们快速创建一个已经有结果的 CompletableFuture,这对于单元测试或需要立即返回结果的场景非常有用。

failedFuture()

此方法创建一个异常完成(异常终止)的 CompletableFuture 实例,方法定义如下:

public static <U> CompletableFuture<U> failedFuture(Throwable ex) 

方法接受的参数为 Throwable,表明为异常终止,它为异步编程提供了一种简洁的方式来表示已知的失败情况,这对于错误处理和异常测试场景非常重要。

除上面两个工厂方法外,Java 9 引入了下面这对 stage-oriented 工厂方法,返回完成的或异常完成的 completion stages:

  • CompletionStage completedStage(U value): 返回一个新的以指定 value 完成的CompletionStage ,并且只支持 CompletionStage 里的接口。
  • CompletionStage failedStage(Throwable ex): 返回一个新的以指定异常完成的CompletionStage ,并且只支持 CompletionStage 里的接口。

支持超时和延迟执行

在 Java 9 中,CompletableFuture 引入了支持超时和延迟执行的改进,这两个功能对于控制异步操作的时间行为至关重要。

支持超时

orTimeout()

允许为 CompletableFuture 设置一个超时时间。如果在指定的超时时间内未完成,CompletableFuture 将以 TimeoutException 完成,方法定义如下:

public CompletableFuture<T> orTimeout(long timeout, TimeUnit unit)

该方法为 CompletableFuture 提供了超时机制,这对于避免永久挂起的异步操作和保证响应性至关重要。

  • 示例:
    @Test
    public void orTimeTest() {
       try {
           CompletableFuture completableFuture = CompletableFuture.runAsync(()->{
               System.out.println("异步任务开始执行....");
               try {
                   TimeUnit.SECONDS.sleep(5);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }).orTimeout(2,TimeUnit.SECONDS);

           completableFuture.join();
       } catch (Exception e) {
           System.out.println(e);
       }
    }
  • 执行结果
异步任务开始执行....
java.util.concurrent.CompletionException: java.util.concurrent.TimeoutException
completeOnTimeout()

该允许在指定的超时时间内如果未完成,则用一个默认值来完成 CompletableFuture,方法定义如下:

public CompletableFuture<T> completeOnTimeout(T value, long timeout, TimeUnit unit)

value 为默认默认值。该方法提供了一种优雅的回退机制,确保即使在超时的情况下也能保持异步流的连续性和完整性。

  • 示例
    @Test
    public void completeOnTimeoutTest() {
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(()->{
            System.out.println("异步任务开始执行....");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return "死磕 Java 新特性";
        }).completeOnTimeout("死磕 Java",2,TimeUnit.SECONDS);

        System.out.println("执行结果为:" + completableFuture.join());
    }
  • 执行结果
异步任务开始执行....
执行结果为:死磕 Java

支持延迟执行

CompletableFuture 提供了delayedExecutor() 来支持延迟执行,该方法创建一个延迟执行的 Executor,可以将任务的执行推迟到未来某个时间点。方法定义如下:

public static Executor delayedExecutor(long delay, TimeUnit unit, Executor executor)

它能够让我们更加精确地控制异步任务的执行时机,特别是在需要根据时间安排任务执行的场景中。

  • 示例
    @Test
    public void completeOnTimeoutTest() {
        // 创建一个延迟执行的Executor
        Executor delayedExecutor = CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS);

        // 使用延迟的Executor执行一个简单任务
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            System.out.println("任务延迟后执行...");
        }, delayedExecutor);

        // 等待异步任务完成
        future.join();
    }

支持子类化

在 Java 9 中,CompletableFuture 做了一些改进,使得 CompletableFuture 可以被更简单的继承。

newIncompleteFuture()

创建一个新的、不完整的 CompletableFuture 实例, 子类可以重写这个方法来返回 CompletableFuture 的子类实例,允许在整个 CompletableFuture API 中自定义实例的行为。

defaultExecutor()

该方法提供 CompletableFuture 操作的默认执行器,子类可以重写此方法以提供不同的默认执行器,这对于定制任务执行策略特别有用。

copy()

创建一个与当前 CompletableFuture 状态相同的新实例。子类可以重写此方法以确保复制的实例是特定子类的实例,而不仅仅是 CompletableFuture

上次编辑于:
贡献者: 诗人都藏在水底