你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
概述
相较于 Java8,Java9 没有新增语法糖,但是其增加的特性也都是非常实用的,比如 Jigsaw 模块化、JShell、发布-订阅框架、GC 等。本文将快速、高层次的介绍一些新特性,完整的特性可以参加https://openjdk.java.net/projects/jdk9/。
这里需要说明一下,由于 Java9 并不是长期支持版,当前也是从现在看过去,所以笔者偷个懒,文章的示例代码都是在 Java11 下写的,可能会与 Java9 中的定义有些出入,不过,这也没啥,毕竟我们真正使用的时候还是优先考虑长期支持版。
Jigsaw 模块化
模块化是一个比较大的更新,这让以前 All-in-One 的 Java 包拆分成几个模块。这种模块化系统提供了类似 OSGi 框架系统的功能,比如多个模块可以独立开发,按需引用、按需集成,最终组装成一个完整功能。
模块具有依赖的概念,可以导出功能 API,可以隐藏实现细节。
还有一个好处是可以实现 JVM 的按需使用,能够减小 Java 运行包的体积,让 JVM 在内存更小的设备上运行。JVM 当时的初衷就是做硬件,也算是不忘初心了。
另外,JVM 中com.sun.*
的之类的内部 API,做了更强的封闭,不在允许调用,提升了内核安全。
在使用的时候,我们需要在 java 代码的顶层目录中定义一个module-info.java
文件,用于描述模块信息:
module cn.howardliu.java9.modules.car {
requires cn.howardliu.java9.modules.engines;
exports cn.howardliu.java9.modules.car.handling;
}
上面描述的信息是:模块cn.howardliu.java9.modules.car
需要依赖模块cn.howardliu.java9.modules.engines
,并导出模块cn.howardliu.java9.modules.car.handling
。
更多的信息可以查看 OpenJDK 的指引 https://openjdk.java.net/projects/jigsaw/quick-start,后续会单独介绍 Jigsaw 模块的使用,内容会贴到评论区。
全新的 HTTP 客户端
这是一个千呼万唤始出来的功能,终于有官方 API 可以替换老旧难用的HttpURLConnection
。只不过,在 Java9 中,新版 HTTP 客户端是放在孵化模块中(具体信息可以查看 https://openjdk.java.net/jeps/110)。
老版 HTTP 客户端存在很多问题,大家开发的时候基本上都是使用第三方 HTTP 库,比如 Apache HttpClient、Netty、Jetty 等。
新版 HTTP 客户端的目标很多,毕竟这么多珠玉在前,如果还是做成一坨,指定是要被笑死的。所以新版 HTTP 客户端列出了 16 个目标,包括简单易用、打印关键信息、WebSocket、HTTP/2、HTTPS/TLS、良好的性能、非阻塞 API 等等。
我们先简单的瞅瞅:
final String url = "https://postman-echo.com/get";
final HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(url))
.GET()
.build();
final HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
final HttpHeaders headers = response.headers();
headers.map().forEach((k, v) -> System.out.println(k + ":" + v));
System.out.println(response.statusCode());
System.out.println(response.body());
新版 HTTP 客户端可以在 Java11 中正常使用了,上面的代码也是在 Java11 中写的,API 是在
java.net.http
包中。
改进的进程 API
在 Java9 中提供的进程 API,可以控制和管理操作系统进程。也就是说,可以在代码中管理当前进程,甚至可以销毁当前进程。
进程信息
这个功能是由java.lang.ProcessHandle
提供的,我们来瞅瞅怎么用:
final ProcessHandle self = ProcessHandle.current();
final long pid = self.pid();
System.out.println("PID: " + pid);
final ProcessHandle.Info procInfo = self.info();
procInfo.arguments().ifPresent(x -> {
for (String s : x) {
System.out.println(s);
}
});
procInfo.commandLine().ifPresent(System.out::println);
procInfo.startInstant().ifPresent(System.out::println);
procInfo.totalCpuDuration().ifPresent(System.out::println);
java.lang.ProcessHandle.Info
中提供了丰富的进程信息
销毁进程
我们还可以使用java.lang.ProcessHandle#destroy
方法销毁进程,我们演示一下销毁子进程:
ProcessHandle.current().children()
.forEach(procHandle -> {
System.out.println(procHandle.pid());
System.out.println(procHandle.destroy());
});
从 Java8 之后,我们会发现 Java 提供的 API 使用了
Optional
、Stream
等功能,*Eating your own dog food *也是比较值得学习的。
其他小改动
Java9 中还对做了对已有功能做了点改动,我们来瞅瞅都有哪些。
改进 try-with-resources
从 Java7 开始,我们可以使用try-with-resources
语法自动关闭资源,所有实现了java.lang.AutoCloseable
接口,可以作为资源。但是这里会有一个限制,就是每个资源需要声明一个新变量。
也就是这样:
public static void tryWithResources() throws IOException {
try (FileInputStream in2 = new FileInputStream("./")) {
// do something
}
}
对于这种直接使用的还算方便,但如果是需要经过一些列方法定义的呢?就得写成下面这个样子:
final Reader inputString = new StringReader("www.howardliu.cn 看山");
final BufferedReader br = new BufferedReader(inputString);
// 其他一些逻辑
try (BufferedReader br1 = br) {
System.out.println(br1.lines());
}
在 Java9 中,如果资源是final
定义的或者等同于final
变量,就不用声明新的变量名,可以直接在try-with-resources
中使用:
final Reader inputString = new StringReader("www.howardliu.cn 看山");
final BufferedReader br = new BufferedReader(inputString);
// 其他一些逻辑
try (br) {
System.out.println(br.lines());
}
改进钻石操作符 (Diamond Operator)
钻石操作符(也就是<>
)是 Java7 引入的,可以简化泛型的书写,比如:
Map<String, List<String>> strsMap = new TreeMap<String, List<String>>();
右侧的TreeMap
类型可以根据左侧的泛型定义推断出来,借助钻石操作符可以简化为:
Map<String, List<String>> strsMap = new TreeMap<>();
看山会简洁很多,<>
的写法就是钻石操作符 (Diamond Operator)。
但是这种写法不适用于匿名内部类。比如有个抽象类:
abstract static class Consumer<T> {
private T content;
public Consumer(T content) {
this.content = content;
}
abstract void accept();
public T getContent() {
return content;
}
}
在 Java9 之前,想要实现匿名内部类,就需要写成:
final Consumer<Integer> intConsumer = new Consumer<Integer>(1) {
@Override
void accept() {
System.out.println(getContent());
}
};
intConsumer.accept();
final Consumer<? extends Number> numConsumer = new Consumer<Number>(BigDecimal.TEN) {
@Override
void accept() {
System.out.println(getContent());
}
};
numConsumer.accept();
final Consumer<?> objConsumer = new Consumer<Object>("看山") {
@Override
void accept() {
System.out.println(getContent());
}
};
objConsumer.accept();
在 Java9 之后就可以使用钻石操作符了:
final Consumer<Integer> intConsumer = new Consumer<>(1) {
@Override
void accept() {
System.out.println(getContent());
}
};
intConsumer.accept();
final Consumer<? extends Number> numConsumer = new Consumer<>(BigDecimal.TEN) {
@Override
void accept() {
System.out.println(getContent());
}
};
numConsumer.accept();
final Consumer<?> objConsumer = new Consumer<>("看山") {
@Override
void accept() {
System.out.println(getContent());
}
};
objConsumer.accept();
私有接口方法
如果说钻石操作符是代码的简洁可读,那接口的私有方法就是比较实用的一个扩展了。
在 Java8 之前,接口只能有常量和抽象方法,想要有具体的实现,就只能借助抽象类,但是 Java 是单继承,有很多场景会受到限制。
在 Java8 之后,接口中可以定义默认方法和静态方法,提供了很多扩展。但这些方法都是public
方法,是完全对外暴露的。如果有一个方法,只想在接口中使用,不想将其暴露出来,就没有办法了。这个问题在 Java9 中得到了解决。我们可以使用private
修饰,限制其作用域。
比如:
public interface Metric {
// 常量
String NAME = "METRIC";
// 抽象方法
void info();
// 私有方法
private void append(String tag, String info) {
buildMetricInfo();
System.out.println(NAME + "[" + tag + "]:" + info);
clearMetricInfo();
}
// 默认方法
default void appendGlobal(String message) {
append("GLOBAL", message);
}
// 默认方法
default void appendDetail(String message) {
append("DETAIL", message);
}
// 私有静态方法
private static void buildMetricInfo() {
System.out.println("build base metric");
}
// 私有静态方法
private static void clearMetricInfo() {
System.out.println("clear base metric");
}
}
JShell
JShell 就是 Java 语言提供的 REPL(Read Eval Print Loop,交互式的编程环境)环境。在 Python、Node 之类的语言,很早就带有这种环境,可以很方便的执行 Java 语句,快速验证一些语法、功能等。
$ jshell
| 欢迎使用 JShell -- 版本 13.0.9
| 要大致了解该版本,请键入:/help intro
我们可以直接使用/help
查看命令
jshell> /help
| 键入 Java 语言表达式,语句或声明。
| 或者键入以下命令之一:
| /list [<名称或 id>|-all|-start]
| 列出您键入的源
| /edit <名称或 id>
。很多的内容,鉴于篇幅,先隐藏
我们看下一些简单的操作:
jshell> "This is a test.".substring(5, 10);
$2 ==> "is a "
jshell> 3+1
$3 ==> 4
也可以创建方法:
jshell> int mulitiTen(int i) { return i*10;}
| 已创建 方法 mulitiTen(int)
jshell> mulitiTen(3)
$6 ==> 30
想要退出 JShell 直接输入:
jshell> /exit
| 再见
JCMD 新增子命令
jcmd
是用于向本地 jvm 进程发送诊断命令,这个命令是从 JDK7 提供的命令行工具,常用于快速定位线上环境故障。
在 JDK9 之后,提供了一些新的子命令,查看 JVM 中加载的所有类及其继承结构的列表。比如:
$ jcmd 22922 VM.class_hierarchy -i -s java.net.Socket
22922:
java.lang.Object/null
|--java.net.Socket/null
| implements java.io.Closeable/null (declared intf)
| implements java.lang.AutoCloseable/null (inherited intf)
| |--sun.nio.ch.SocketAdaptor/null
| | implements java.lang.AutoCloseable/null (inherited intf)
| | implements java.io.Closeable/null (inherited intf)
第一个参数是进程 ID,都是针对这个进程执行诊断。我们还可以使用set_vmflag
参数在线修改 JVM 参数,这种操作无需重启 JVM 进程。
有时候还需要查看当前进程的虚拟机参数选项和当前值:jcmd 22922 VM.flags -all
。
多分辨率图像 API
在 Java9 中定义了多分辨率图像 API,我们可以很容易的操作和展示不同分辨率的图像了。java.awt.image.MultiResolutionImage
将一组具有不同分辨率的图像封装到单个对象中。java.awt.Graphics
类根据当前显示 DPI 度量和任何应用的转换从多分辨率图像中获取变量。
以下是多分辨率图像的主要操作方法:
Image getResolutionVariant(double destImageWidth, double destImageHeight)
:获取特定分辨率的图像变体-表示一张已知分辨率单位为 DPI 的特定尺寸大小的逻辑图像,并且这张图像是最佳的变体。List<Image> getResolutionVariants()
:返回可读的分辨率的图像变体列表。
我们来看下应用:
final List<Image> images = List.of(
ImageIO.read(new URL("https://static.howardliu.cn/about/kanshanshuo_2.png")),
ImageIO.read(new URL("https://static.howardliu.cn/about/hellokanshan.png")),
ImageIO.read(new URL("https://static.howardliu.cn/about/evil%20coder.jpg"))
);
// 读取所有图片
final MultiResolutionImage multiResolutionImage = new BaseMultiResolutionImage(images.toArray(new Image[0]));
// 获取图片的所有分辨率
final List<Image> variants = multiResolutionImage.getResolutionVariants();
System.out.println("Total number of images: " + variants.size());
for (Image img : variants) {
System.out.println(img);
}
// 根据不同尺寸获取对应的图像分辨率
Image variant1 = multiResolutionImage.getResolutionVariant(100, 100);
System.out.printf("\nImage for destination[%d,%d]: [%d,%d]",
100, 100, variant1.getWidth(null), variant1.getHeight(null));
Image variant2 = multiResolutionImage.getResolutionVariant(200, 200);
System.out.printf("\nImage for destination[%d,%d]: [%d,%d]",
200, 200, variant2.getWidth(null), variant2.getHeight(null));
Image variant3 = multiResolutionImage.getResolutionVariant(300, 300);
System.out.printf("\nImage for destination[%d,%d]: [%d,%d]",
300, 300, variant3.getWidth(null), variant3.getHeight(null));
Image variant4 = multiResolutionImage.getResolutionVariant(400, 400);
System.out.printf("\nImage for destination[%d,%d]: [%d,%d]",
400, 400, variant4.getWidth(null), variant4.getHeight(null));
Image variant5 = multiResolutionImage.getResolutionVariant(500, 500);
System.out.printf("\nImage for destination[%d,%d]: [%d,%d]",
500, 500, variant5.getWidth(null), variant5.getHeight(null));
变量句柄(Variable Handles)
变量句柄(Variable Handles)的 API 主要是用来替代java.util.concurrent.atomic
包和sun.misc.Unsafe
类的部分功能,并且提供了一系列标准的内存屏障操作,用来更加细粒度的控制内存排序。一个变量句柄是一个变量(任何字段、数组元素、静态表里等)的类型引用,支持在不同访问模型下对这些类型变量的访问,包括简单的 read/write 访问,volatile 类型的 read/write 访问,和 CAS(compare-and-swap) 等。
这部分内容涉及反射、内联、并发等内容,后续会单独介绍,文章最终会发布在 从小工到专家的 Java 进阶之旅 中,敬请关注。
发布-订阅框架
在 Java9 中增加的java.util.concurrent.Flow
支持响应式 API 的发布-订阅框架,他们提供在 JVM 上运行的许多异步系统之间的互操作性。我们可以借助SubmissionPublisher
定制组件。
关于响应式 API 的内容可以先查看 http://www.reactive-streams.org/的内容,后续单独介绍,文章最终会发布在 从小工到专家的 Java 进阶之旅 中,敬请关注。怎么感觉给自己刨了这么多坑,得抓紧时间填坑了。
统一 JVM 日志记录
在这个版本中,为 JVM 的所有组件引入了一个通用的日志系统。它提供了日志记录的基础。这个功能是通过-Xlog
启动参数指定,并且定义很多标签用来定义不同类型日志,比如:gc(垃圾收集)、compiler(编译)、threads(线程)等等。比如,我们定义debug
等级的 gc 日志,日志存储在gc.log
文件中:
java -Xlog:gc=debug:file=gc.log:none
因为参数比较多,我们可以通过java -Xlog:help
查看具体定义参数。而且日志配置可以通过jcmd
命令动态修改,比如,我们将日志输出文件修改为gc_other.log
:
jcmd ${PID} VM.log output=gc_other.log what=gc
新的 API
不可变集合
在 Java9 中增加的java.util.List.of()
、java.util.Set.of()
、java.util.Map.of()
系列方法,可以一行代码创建不可变集合。在 Java9 之前,我们想要初始化一个有指定值的集合,需要执行一堆add
或put
方法,或者依赖guava
框架。
而且,这些集合对象是可变的,假设我们将值传入某个方法,我们就没有办法控制这些集合的值不会被修改。在 Java9 之后,我们可以借助ImmutableCollections
中的定义实现初始化一个不可变的、有初始值的集合了。如果对这些对象进行修改(新增元素、删除元素),就会抛出UnsupportedOperationException
异常。
这里不得不提的是,Java 开发者们也是考虑了性能,针对不同数量的集合,提供了不同的实现类:
List12
、Set12
、Map1
专门用于少量(List 和 Set 是 2 个,对于 Map 是 1 对)元素数量的场景ListN
、SetN
、MapN
用于数据量多(List 和 Set 是超过 2 个,对于 Map 是多余 1 对)的场景
改进的 Optional 类
Java9 中为Optional
添加了三个实用方法:stream
、ifPresentOrElse
、or
。
stream
是将Optional
转为一个Stream
,如果该Optional
中包含值,那么就返回包含这个值的Stream
,否则返回Stream.empty()
。比如,我们有一个集合,需要过滤非空数据,在 Java9 之前,写法如下:
final List<Optional<String>> list = Arrays.asList(
Optional.empty(),
Optional.of("看山"),
Optional.empty(),
Optional.of("看山的小屋"));
final List<String> filteredList = list.stream()
.flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty())
.collect(Collectors.toList());
在 Java9 之后,我们可以借助stream
方法:
final List<String> filteredListJava9 = list.stream()
.flatMap(Optional::stream)
.collect(Collectors.toList());
ifPresentOrElse
:如果一个Optional
包含值,则对其包含的值调用函数action
,即action.accept(value)
,这与ifPresent
方法一致;如果Optional
不包含值,那会调用emptyAction
,即emptyAction.run()
。效果如下:
Optional<Integer> optional = Optional.of(1);
optional.ifPresentOrElse(x -> System.out.println("Value: " + x), () -> System.out.println("Not Present."));
optional = Optional.empty();
optional.ifPresentOrElse(x -> System.out.println("Value: " + x), () -> System.out.println("Not Present."));
// 输出结果为:
// 作者:看山
// 佚名
or
:如果值存在,返回Optional
指定的值,否则返回一个预设的值。效果如下:
Optional<String> optional1 = Optional.of("看山");
Supplier<Optional<String>> supplierString = () -> Optional.of("佚名");
optional1 = optional1.or(supplierString);
optional1.ifPresent(x -> System.out.println("作者:" + x));
optional1 = Optional.empty();
optional1 = optional1.or(supplierString);
optional1.ifPresent(x -> System.out.println("作者:" + x));
// 输出结果为:
// 作者:看山
// 作者:佚名
文末总结
本文介绍了 Java9 新增的特性,完整的特性清单可以从https://openjdk.java.net/projects/jdk9/查看。文中也给自己刨了几个坑,碍于篇幅,没有办法展开,所有这些需要展开的功能细述,都会在 Java8 到 Java17 的新特性系列完成后补充,博文会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
推荐阅读
- 从小工到专家的 Java 进阶之旅
- 一文掌握 Java8 Stream 中 Collectors 的 24 个操作
- 一文掌握 Java8 的 Optional 的 6 种操作
- 使用 Lambda 表达式实现超强的排序功能
- Java8 的时间库(1):介绍 Java8 中的时间类及常用 API
- Java8 的时间库(2):Date 与 LocalDate 或 LocalDateTime 互相转换
- Java8 的时间库(3):开始使用 Java8 中的时间类
- Java8 的时间库(4):检查日期字符串是否合法
- Java8 的新特性
- Java9 的新特性
- Java10 的新特性
- Java11 中基于嵌套关系的访问控制优化
- Java11 的新特性
- Java12 的新特性
- Java13 的新特性
- Java14 的新特性
- Java15 的新特性
- Java16 的新特性
- Java17 的新特性
- Java18 的新特性
- Java19 的新特性
- Java20 的新特性
- Java21 的新特性
- Java22 的新特性
- Java23 的新特性
- Java24 的新特性
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java9 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java9 的新特性