本文重点回顾 Java 9 引入的那些新特性。
Java 9 引入的一个最主要的特性就是模块系统(全称为 Java 平台模块系统,Java Platform Module System)。根据官方的定义,模块是一个命名的、自描述的代码和数据的集合。模块系统会在编译时和运行时之间新加一个可选的链接时,在该阶段可以将一组模块组装为一个自定义的运行时镜像。
模块系统中的几个核心概念:
模块(Module)
模块是模块化系统的基本单元。它是一个逻辑上独立的代码单元,包括类、接口、资源和 module-info.java
文件。每个模块都有一个唯一的名称,如:java.base
、com.example.myapp
等。
模块路径(Module Path)
模块路径是一组包含模块的路径,用于在运行时指定应用程序所需的模块。类似于类路径,但只用于模块。
模块依赖(Module Dependencies)
在 module-info.java
文件中,可以使用 requires
关键字声明模块所需的依赖。
模块导出(Module Exporting)
在 module-info.java
文件中,可以使用 exports
关键字声明哪些包可以被其它模块访问,这有助于控制包的可见性。
引入模块系统有下面几个缘由:
显式依赖管理
模块化系统需要我们明确声明模块之间的依赖关系,减少了传统类路径(classpath)上的混乱和不稳定性。每个模块都需要显式声明自己需暴露的 package
,而自己所依赖的和自己内部使用的 package
,则不会暴露,也不会被外部依赖,这有助于保护内部实现,防止不应该公开的部分被外部模块访问。
更好地安全性
模块化系统可以提供更严格的可见性控制,防止私有实现被不应访问的模块访问,从而增强了应用程序的安全性。代码真正意义上可以按照作者的设计思路进行公开和隐藏,同时也限制了反射的滥用,更好的保护了那些不建议被外部直接使用或过时的实现。
标准化
模块化引入了标准化的方式来组织和管理代码。显式声明暴露的内容,可以让第三方库的开发者更好地管理自己的内部实现逻辑。
自定义最小运行时镜像
Java 因为其向后兼容的原则,不会轻易对其内容进行删除,包含的陈旧过时的技术也越来越多,导致 JDK 变得越来越臃肿。而 Java 9 的显示依赖管理使得加载最小所需模块成为了可能,我们可以选择只加载必须的 JDK 模块,抛弃如 java.awt
, javax.swing
, java.applet
等这些用不到的模块。这种机制,大大的减少了运行 Java 环境所需要的内存资源,对于嵌入式系统开发或其它硬件资源受限的开发场景非常有用。
更加适合大型应用程序管理与更好的性能
对于大型应用程序而言,模块化系统提供更好的组织结构,减少了复杂性,使开发者能够更轻松地管理和扩展应用程序。
通过减少不必要的类路径搜索和提供更紧凑的部署单元,模块化系统也有助于提高应用程序的性能。
下面使用一个示例来演示模块的使用:
假设我们有两个模块 module-1
与 module-2
,module-2
需要依赖 module-1
,两模块的目录结构分别为:
module-1
├─ src/main
│ └─ java
│ ├─ com.leileiluoluo.module1
│ │ ├─ model
│ │ │ └─ User.java
│ │ └─ util
│ │ └─ AgeUtil.java
│ └─ module-info.java
└─ pom.xml
module-2
├─ src/main
│ └─ java
│ ├─ com.leileiluoluo.module1
│ │ ├─ ModuleTest.java
│ └─ module-info.java
└─ pom.xml
我们在 module-1
中定义了两个 package
:com.leileiluoluo.module1.util
和 com.leileiluoluo.module1.model
。假如我们只想将 Model 类暴露出去供其它模块使用,Util 类仅作为内部使用,则其 module-info.java
文件可以使用如下方式定义:
// module-1: src/main/java/module-info.java
module leileiluoluo.module1 {
exports com.leileiluoluo.module1.model;
}
因 module-2
需要使用 module-1
的 Model 类,其 module-info.java
文件可以使用如下方式定义:
// module-2: src/main/java/module-info.java
module leileiluoluo.module2 {
requires leileiluoluo.module1;
}
这样,即可以在 module-2
中使用 module-1
暴露的 Model 类了,对于 module-1
未暴露的其它类(如 Util 类),则完全不可见。
// module-2: src/main/java/com/leileiluoluo/module2/ModuleTest.java
package com.leileiluoluo.module2;
import com.leileiluoluo.module1.model.User;
public class ModuleTest {
public static void main(String[] args) {
User user = new User("Larry", 28);
System.out.println("name: " + user.getName());
System.out.println("age: " + user.getAge());
System.out.println("group: " + user.getAgeGroup());
}
}
这里仅演示了一种比较简单的使用方式,在实际项目中的使用情况会比较复杂,需要我们在工作过程中不断地去探索。
两个模块的完整代码已提交至 GitHub。
Java 9 引入了 JShell,其是一个 REPL(Read-Eval-Print Loop)工具。REPL 是一个交互式编程环境,允许用户输入命令或表达式,然后进行评估并打印结果。这种环境已在解释型编程语言(如:Python、Ruby、JavaScript 等)中得到广泛应用,其即时反馈的特征对于编程语言的初学者、原型设计者或新特性探索人员非常有帮助,但其只适合简单代码片段的执行与测试,并不能取代 IDE。
下面即看一下 JShell 如何使用。
启动
在命令行输入 jshell
即可启动 JShell:
$ jshell
| 欢迎使用 JShell -- 版本 9
| 要大致了解该版本, 请键入: /help intro
jshell>
交互式操作
在提示符 jshell>
下输入 Java 代码并立即查看结果:
jshell> int a = 10;
a ==> 10
jshell> int b = 20;
b ==> 20
jshell> int c = a + b;
c ==> 30
上述示例,定义了两个整数 a
和 b
,然后计算了两者之和。
声明方法
在 JShell 中可以声明方法并立即调用:
jshell> void greet(String someone) {
...> System.out.println("Hello, " + someone + "!");
...> }
| 已创建 方法 greet(String)
jshell> greet("Larry");
Hello, Larry!
上述示例,声明了一个接收 String
参数的方法 greet()
,然后作了调用。
Tab 补全和历史记录查看
JShell 支持 Tab 补全和历史命令查看,提高了输入效率:
jshell> String message = "Hello World!";
message ==> "Hello World!"
jshell> System.
...
mapLibraryName( nanoTime() out
...
jshell> System.out.println(message);
Hello World!
上述示例中,键入 System.
后,敲 Tab 键会提示所有可用方法。按上下箭头符号也会列出历史输入过的命令。
Java 9 对 try-with-resources 特性作了增强。我们知道,try-with-resources 特性是在 Java 7 引入的,主要用于确保资源(实现了 AutoCloseable 接口)使用后的自动关闭。而在 Java 7 之前,资源的关闭是需要开发者在 finally 块中显式进行的。
基于 Java 7 使用 try-with-resources 特性时,资源的创建与变量的声明都需要放在 try 圆括号内进行;而基于 Java 9 使用 try-with-resources 特性时,资源的创建与变量的声明可以在 try-with-resources 语句之前进行,只需将已声明的资源变量放在 try 圆括号内即可(注意:这些变量必须是 final
变量或者等效于 final
的变量才可以)。
下面即以一个示例来对照两个版本在使用上的不同:
// src/main/java/TryWithResourcesTest.java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryWithResourcesTest {
public static void testJava7ReadFileWithMultipleResources() throws IOException {
String filePath = TryWithResourcesTest.class.getResource("test.txt").getPath();
try (FileReader fr = new FileReader(filePath);
BufferedReader br = new BufferedReader(fr)) {
System.out.println(br.readLine());
}
}
public static void testJava9ReadFileWithMultipleResources() throws IOException {
String filePath = TryWithResourcesTest.class.getResource("test.txt").getPath();
FileReader fr = new FileReader(filePath);
BufferedReader br = new BufferedReader(fr);
try (fr; br) {
System.out.println(br.readLine());
}
}
public static void main(String[] args) throws IOException {
// java 7
testJava7ReadFileWithMultipleResources();
// java 9
testJava9ReadFileWithMultipleResources();
}
}
上述示例尝试读取一个文本文件,并打印其内容。testJava7ReadFileWithMultipleResources()
方法使用了 Java 7 中的写法,资源的创建与变量的声明均须放在 try 圆括号内;而 testJava9ReadFileWithMultipleResources()
方法使用了 Java 9 中的增强型写法,资源的创建与声明放在了 try-with-resources 语句之前,try 圆括号内只需放置资源变量即可。
关于 try-with-resources 特性的前世今生,请参看本人之前写的一篇文章「Java try-with-resources 特性详解 」。
我们知道,在 Java 8 之前,接口中定义的方法必须是 public abstract
的。而在 Java 8 时,接口中可以定义非 abstract
的默认方法了,但默认方法间若有重复代码该怎么办?Java 9 支持定义私有方法即是用于解决该问题的。接口的私有方法(或静态私有方法)可以被接口的默认方法(或静态方法)调用,但不能被接口的实现类直接访问或被其它接口继承。
下面使用一个示例来演示接口私有方法的使用:
// src/main/java/PrivateInterfaceMethodsTest.java
public class PrivateInterfaceMethodsTest {
public interface MyInterface {
void abstractMethod();
default void defaultMethod() {
int result = privateMethod();
System.out.println("Result: " + result);
}
static void staticMethod() {
int result = privateStaticMethod();
System.out.println("Result: " + result);
}
private int privateMethod() {
return 28;
}
private static int privateStaticMethod() {
return 39;
}
}
public static void main(String[] args) {
MyInterface myInterface = new MyInterface() {
@Override
public void abstractMethod() {
System.out.println("Abstract Method Implemented!");
}
};
myInterface.abstractMethod(); // Abstract Method Implemented!
myInterface.defaultMethod(); // Result: 28
MyInterface.staticMethod(); // Result: 39
}
}
上述示例中,privateMethod()
是一个私有实例方法,被默认方法 defaultMethod()
调用;privateStaticMethod()
是一个私有静态方法,被静态方法 staticMethod()
调用;而 abstractMethod()
方法是一个抽象方法,需要被实现。
Java 9 在集合接口 List
、Set
和 Map
上增加了几个实用静态工厂方法,用于创建一个拥有少量元素的集合,且该集合是不可变的。
在 Java 9 之前,我们要想创建一个拥有少量元素的不可变 List 或 Set,需要先使用 Arrays.asList()
方法来创建一个 List,然后再将该 List 传入工具方法 Collections.unmodifiableXXX()
中才可以实现需求。
而在 Java 9 的话,只需要调用对应集合的 of()
工厂方法就可以实现同样的需求。
看一个具体示例:
// src/main/java/CollectionFactoryMethodsTest.java
import java.util.*;
public class CollectionFactoryMethodsTest {
public static void main(String[] args) {
// Java 8: creating an unmodifiable list
List<String> names = Arrays.asList("a", "b", "c");
names = Collections.unmodifiableList(names);
// Java 9: creating an unmodifiable list
List<String> names2 = List.of("Larry", "Lucy", "Jacky");
// Java 8: creating an unmodifiable set
Set<Integer> numbers = new HashSet<>(Arrays.asList(1, 2, 3));
numbers = Collections.unmodifiableSet(numbers);
// Java 9: creating an unmodifiable set
Set<Integer> numbers2 = Set.of(1, 2, 3);
// Java 8: creating an unmodifiable map
Map<String, Integer> nameAgeMap = new HashMap<>();
nameAgeMap.put("Larry", 18);
nameAgeMap.put("Lucy", 28);
nameAgeMap.put("Jacky", 29);
nameAgeMap = Collections.unmodifiableMap(nameAgeMap);
// Java 9: creating an unmodifiable map
Map<String, Integer> nameAgeMap2 = Map.of("Larry", 18, "Lucy", 28, "Jacky", 29);
}
}
如上示例中,分别使用 Java 8 和 Java 9 的写法演示了只读 List、Set 和 Map 的创建。对于这些只读集合,调用任何可以改变集合的方法(如:add()
、remove()
、replaceAll()
、clear()
等),都会抛出 UnsupportedOperationException
。
我们知道,Stream API 是在 Java 8 引入的,其提供了一种简洁而强大的集合数据处理方式。Java 9 对 Stream API 作了一些增强,主要新增了 ofNullable()
、takeWhile()
和 dropWhile()
这几个方法,并且提供了 iterate()
方法的重载版本。
ofNullable()
工厂方法用于创建一个其中只包含一个可空元素的 Stream。该方法的主要目的是简化处理可能包含 null
值的集合时的逻辑,以便避免显式地检查和过滤 null
值。
takeWhile()
和 dropWhile()
这两个方法允许根据谓词条件从流中选择或删除元素,直到遇到第一个不满足条件的元素时停止。这两个方法用于处理流中靠前的元素,而不必处理整个流,为处理 Stream 提供了更多的灵活性。
iterate()
方法在 Java 8 中创建的是一个无限流,其元素由给定的初始值和一个生成下一个元素的函数产生,为了可以将流终止,我们需要使用一些限制性的函数(如 limit()
)来操作。而在 Java 9 中,为了限制该无序流的长度,增加了一个谓词参数(Predicate<? super T> hasNext
)。
下面看一下这几个方法的使用样例:
// src/main/java/StreamEnhancementsTest.java
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamEnhancementsTest {
public static void main(String[] args) {
// ofNullable() 使用样例
String name = null;
List<String> names = Stream.ofNullable(name)
.collect(Collectors.toList());
System.out.println(names); // []
// takeWhile() 使用样例
List<Integer> numbers = Stream.of(1, 2, 3, 4, 3, 2, 1)
.takeWhile(num -> num < 4)
.collect(Collectors.toList());
System.out.println(numbers); // [1, 2, 3]
// dropWhile() 使用样例
List<Integer> numbers2 = Stream.of(1, 2, 3, 4, 3, 2, 1)
.dropWhile(num -> num < 4)
.collect(Collectors.toList());
System.out.println(numbers2); // [4, 3, 2, 1]
// iterate() 使用样例,Java 8 版本
List<Integer> oddNumbers = Stream.iterate(1, v -> v + 2)
.limit(5)
.collect(Collectors.toList());
System.out.println(oddNumbers); // [1, 3, 5, 7, 9]
// iterate() 使用样例,Java 9 版本
List<Integer> oddNumbers2 = Stream.iterate(1, v -> v < 10, v -> v + 2)
.collect(Collectors.toList());
System.out.println(oddNumbers2); // [1, 3, 5, 7, 9]
}
}
上述示例中,首先对 ofNullable()
的使用作了演示,然后对 takeWhile()
和 dropWhile()
的使用作了演示,最后对照 Java 8 与 Java 9 对 iterate()
的使用作了演示。
Optional 类是 Java 8 引入的一个用于安全处理潜在 null
值的封装类。Java 9 对 Optional 类作了一些改进,以提供更多的实用方法和增强功能。下面是 Java 9 对 Optional 类的一些改进点:
or()
方法的重载
or()
方法现在支持 Supplier
函数接口,用于提供 Optional
中对象为空情况下的备选值。
stream()
方法
新增的 stream()
方法用于将 Optional
对象转换为一个包含单个元素的 Stream。如果 Optional 对象有值,则返回一个包含该值的 Stream,否则返回一个空 Stream。
ifPresentOrElse()
方法
新增的 ifPresentOrElse()
方法用于在 Optional 对象有值时执行一个操作,否则执行一个备选操作。这样可以避免使用传统的 if-else
语句来处理 Optional 对象的值。
下面看一个示例:
// src/main/java/OptionalEnhancementsTest.java
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class OptionalEnhancementsTest {
public static void main(String[] args) {
// 使用 or() 方法设置备选值
Optional<String> optional = Optional.empty();
String result = optional.or(() -> Optional.of("Default value"))
.orElse("Other value");
System.out.println(result); // Default value
// 使用 stream() 方法将 Optional 转换为 Stream
List<String> names = Optional.of("Larry")
.stream()
.collect(Collectors.toList());
System.out.println(names); // [Larry]
// 使用 ifPresentOrElse() 方法执行操作
Optional.empty()
.ifPresentOrElse(
value -> System.out.println("Value: " + value),
() -> System.out.println("No value is present")
); // No value is present
}
}
上述示例中,首先演示了 or()
方法的使用;然后使用 stream()
方法将 Optional 对象转换为了一个包含单个元素的 Stream,又转换为了一个 List;最后演示了 ifPresentOrElse()
方法的使用。
Java 9 引入了一些重要的改进来增强 Process API,使其更易于管理外部进程。以下是几个主要的改进点:
新增 ProcessHandle 接口
引入了 ProcessHandle
接口,用于代表操作系统中的一个进程。通过 ProcessHandle
,可以轻松地获取进程的 PID(进程标识符)、父进程、子进程以及其它信息。
获取所有进程的流式 API
引入了 ProcessHandle.allProcesses()
方法,返回一个 Stream<ProcessHandle>
,可以用来遍历当前系统中所有的进程,进而对它们进行管理和监控。
方便的进程管理方法
ProcessHandle
接口提供了一系列方法来管理进程,如 destroy()
、destroyForcibly()
、isAlive()
等,使得与外部进程的交互更加直观和简便。
支持处理进程的异步操作
引入了异步方法,如 onExit()
和 onKill()
,允许在进程终止或被杀死时进行异步处理。这种方式避免了传统的轮询检查进程状态的需要,提升了效率。
下面看一个示例:
// src/main/java/ProcessAPITest.java
public class ProcessAPITest {
public static void main(String[] args) {
// 获取当前进程的信息
ProcessHandle currentProcess = ProcessHandle.current();
System.out.println("当前进程 PID: " + currentProcess.pid());
System.out.println("当前进程是否存活: " + currentProcess.isAlive());
System.out.println("当前进程信息: " + currentProcess.info());
// 遍历所有进程并打印信息
ProcessHandle.allProcesses().forEach(process -> {
System.out.println("PID: " + process.pid());
System.out.println("命令: " + process.info().command().orElse("未知"));
System.out.println("启动时间: " + process.info().startInstant().orElse(null));
System.out.println("------------------------");
});
// 异步处理进程退出事件
ProcessHandle handle = ProcessHandle.of(currentProcess.pid()).orElseThrow(() -> new IllegalArgumentException("无效的进程 ID"));
handle.onExit().thenAccept(process -> {
System.out.println("进程 " + process.pid() + " 已退出");
});
// 销毁指定进程
ProcessHandle processHandle = ProcessHandle.of(currentProcess.pid()).orElseThrow(() -> new IllegalArgumentException("无效的进程 ID"));
boolean destroyed = processHandle.destroy(); // processHandle.destroyForcibly();
if (destroyed) {
System.out.println("进程 " + currentProcess.pid() + " 已被销毁");
} else {
System.out.println("无法销毁进程 " + currentProcess.pid());
}
}
}
如上示例中,首先获取了当前进程的各种信息;然后使用 ProcessHandle.allProcesses()
获取了所有的进程并打印了相关信息;接着使用 onExit()
方法,添加了进程退出时的异步处理逻辑;最后使用 destroy()
方法尝试销毁进程。
Java 9 对钻石操作符的使用场景作了拓展,对钻石操作符的类型推断作了改进,特别是在嵌套泛型的情况下,使其更加灵活和智能。
下面看一段示例代码:
// src/main/java/DiamondOperatorTest.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DiamondOperatorTest {
public static void main(String[] args) {
// Java 8 以前,需要显式指定泛型参数
List<String> list1 = new ArrayList<String>();
// 在 Java 8 中,可以使用钻石操作符进行推断
List<String> list2 = new ArrayList<>();
// 在 Java 8 中,无法在匿名内部类中使用钻石操作符
// 而在 Java 9 中则可以
Runnable runnable = () -> {
List<String> list = new ArrayList<>(); // 自动推断为 ArrayList<String>
list.add("Java 9");
System.out.println("Inside Runnable: " + list);
};
// 在 Java 9 中更复杂的嵌套泛型也能正确推断
Map<String, List<Map<Integer, String>>> complexMap = new HashMap<>();
}
}
如上示例演示了 Java 9 改进的钻石操作符针对匿名内部类、复杂嵌套泛型结构时,依然能够对泛型类型作出准确推断,使得钻石操作符的使用场景得到扩展,也使得代码更加简洁和易读。
Java 9 对 @Deprecated
注解作了一些改进,主要集中在如何标记和使用过时元素的增强上:
forRemoval
参数
用于指示被标记为过时的元素是否计划在未来的版本中被移除,默认为 false
。
since
参数
用于指定从哪个版本开始该元素被标记为过时,默认值为空字符串。
这些改进使得 @Deprecated
注解更加丰富,有助于开发者更好地管理和维护代码库中的过时元素。
请看一个示例:
// src/main/java/DeprecatedAnnotationTest.java
public class DeprecatedAnnotationTest {
@Deprecated(since = "9", forRemoval = true)
public static void method1() {
// 即将废弃的方法实现
}
@Deprecated(since = "10") // forRemoval = false
public static void method2() {
// 过时的方法实现
}
public static void main(String[] args) {
DeprecatedAnnotationTest.method1();
DeprecatedAnnotationTest.method2();
}
}
如上示例演示了 @Deprecated
注解中,forRemoval
参数与 since
参数的使用。
综上,我们速览了 Java 9 引入的那些主要特性。用于演示模块系统特性所使用的完整代码已提交至 java-9-module-usage-demo,其它特性对应的完整示例代码已提交至 java-9-new-features-demo,欢迎关注或 Fork。
参考资料
[1] Oracle: What’s New in JDK 9? - https://docs.oracle.com/javase/9/whatsnew/
[2] 掘金:一口气读完 Java 8 ~ Java 21 所有新特性 - https://juejin.cn/post/7315730050577006592
[3] 掘金:JDK 8 - JDK 17 新特性总结 - https://juejin.cn/post/7250734439709048869