223 lines
12 KiB
Markdown
223 lines
12 KiB
Markdown
---
|
||
title: Java 16的新特性
|
||
tags:
|
||
- JVM
|
||
- Java
|
||
- Java 16
|
||
- 新特性
|
||
- instanceOf
|
||
- 记录类
|
||
- jpackage
|
||
categories:
|
||
- - JVM
|
||
- Java
|
||
keywords: 'Java,Java 16,新特性,instanceOf,模式匹配,package,jpackage,记录类'
|
||
date: 2021-05-26 13:04:41
|
||
---
|
||
|
||
Java 16是下一个LTS版本之前的最后一个发行版,有不少在两个LTS版本之间引入的新特性,已经在Java 16中得到了稳固。<!-- more -->
|
||
|
||
本系列的文章有:
|
||
|
||
- {% post_link nf-java8 %}
|
||
- {% post_link nf-java9 %}
|
||
- {% post_link nf-java10 %}
|
||
- {% post_link nf-java11 %}
|
||
- {% post_link nf-java12 %}
|
||
- {% post_link nf-java13 %}
|
||
- {% post_link nf-java14 %}
|
||
- {% post_link nf-java15 %}
|
||
- {% post_link nf-java16 %}
|
||
- {% post_link nf-java17 %}
|
||
|
||
## 再述记录类
|
||
|
||
记录类在经历了Java 14和Java 15两个预览版以后,在Java 16中进行了实装。记录类用来声明一个特殊的类,效果与使用Lombok的`@Data`修饰的普通类几乎相同。记录类主要设计为Java程序提供以下功能。
|
||
|
||
- 用一个简单的面向对象的数据结构持有一套简单的数据集合。
|
||
- 使开发者主要集中在构建不可变数据,而不是可扩展的对象行为。
|
||
- 自动实现数据访问和`equals()`等方法。
|
||
- 保留长期存在的Java原则,如名义类型和迁移兼容性等。
|
||
|
||
其实这其中记录类最主要的目标就是简化Java中数据载体类的编写,使那些低值、重复、易出错的代码得到简化,例如构造函数、访问器、`equals()`、`hashCode()`等。
|
||
|
||
记录类是一种全新的类,有助于比普通类以更少的代价对普通的数据进行聚合和建模。记录类声明由名称、可选参数、头信息和正文组成。其中头信息中列出了构成记录类的组件,这些组件也是构成记录类状态的变量。
|
||
|
||
```java
|
||
public record User(int uid, String username, String password) { }
|
||
```
|
||
|
||
例如在上面这个示例中,变量`uid`、`username`、`passsword`就是记录类`User`中的状态组件。记录类在进行编译的时候,会自动生成一个与组件相同名称和返回类型的`public`的访问器方法,还会生成一个与组件有相同类型的`private`的`final`的字段。记录类默认的构造函数其签名与头信息相同,并且每个变量也都会分配给记录类实例化表达式中的相应参数。对于`hashCode()`和`equals()`,记录类会采用类型和值都相等的比较方法来确保它们一定相等。
|
||
|
||
如果没有提供记录类一个构造函数,那么记录类将会使用一个规范构造函数来将所有私有字段赋予实例化该记录的new表达式的相应参数。对于上面这个记录类的示例,如果将其中的默认实现都展开,将会是以下样子。
|
||
|
||
```java
|
||
public record User(int uid, String username, String password) {
|
||
private final int uid;
|
||
private final String username;
|
||
private final String password;
|
||
|
||
User(int uid, String username, String password) {
|
||
this.uid = uid;
|
||
this.username = username;
|
||
this.password = password;
|
||
}
|
||
}
|
||
```
|
||
|
||
构造函数还可以根据需要重新定义,但是需要注意的是,构造函数的参数列表是固定的,也可以省略不写,而脾气与记录组件对应的私有字段不能在构造函数主体中被赋值,这些私有字段的赋值都是自动的。例如我们可以这样定义一个记录类,可以在记录类实例化的时候做一些验证工作,但并不必做赋值操作。
|
||
|
||
```java
|
||
public record Range(int lo, int hi) {
|
||
Range {
|
||
if (lo > hi) {
|
||
throw new IllegalArgumentException();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
如果实在需要对给定的记录组件进行变换,可以直接对记录类的参数进行赋值,因为对记录类示例内部的私有字段的赋值是在构造函数末尾发生的。例如。
|
||
|
||
```java
|
||
public record Rational(int num, int denom) {
|
||
Rational {
|
||
int gcd = gcd(num, denom);
|
||
num /= gcd;
|
||
denom /= gcd;j
|
||
}
|
||
}
|
||
```
|
||
|
||
记录类在使用的时候需要遵循以下规则:
|
||
|
||
- 记录类可以作为顶级类,也可以作为嵌套类,并且支持泛型。
|
||
- 记录类可以拥有静态方法、静态字段和静态初始值。
|
||
- 记录类可以拥有实例方法。
|
||
- 记录类可以实现一个接口,因为接口描述的是行为,而不是状态。
|
||
- 记录类中可以声明嵌套类型,包括嵌套记录类,嵌套记录类默认是静态的。
|
||
- 记录类的头信息中的组件可以使用注解修饰,这些注解会自动被传播到自动派生的成员上。
|
||
- 记录类的示例可以被串行化和反串行化,但是只能通过构造函数进行反串行化,而不能通过`writeObject`、`readObject`、`readObjectNodeData`、`writeExternal`和`readExternal`方法来自定义反串行化过程。
|
||
- 记录类不支持继承,也就是说记录类不能被继承,也不能继承其他的类。
|
||
- 记录类是隐式`final`的,并且不能是`abstract`的。
|
||
- 由记录组件派生的所有字段都是`final`的,这保证了记录类的不可变性。
|
||
- 记录类不能显式声明字段,也不能包含任何字段初始值。
|
||
- 任何自定义的成员,必须与自动派生的成员类型完全匹配。
|
||
- 记录类不能声明原生方法,原生方法将可能改变记录类的内部状态。
|
||
|
||
## 再述instanceOf的模式匹配
|
||
|
||
对于一个强类型的语言来说,判断一个对象的类型,然后再将其转换为相应的类型是一件经常要做的事情。而这件事情常用的逻辑就如同以下示例代码所示。
|
||
|
||
```java
|
||
if (obj instanceof String) {
|
||
String str = (String) obj;
|
||
// 完成其他对于字符串变量str的操作
|
||
}
|
||
```
|
||
这种代码几乎已经形成了一种模板代码,虽然这种模式十分的简单,但是并不是那么理想。instanceOf的模式匹配语法就大大的简化了这种模板代码。模式匹配允许简洁的表示对象所需的模式,并允许语句和表达式根据其输入测试指定的模式。
|
||
|
||
模式匹配由两部分组成,第一部分是应用于目标的谓词或者测试,第二部分是一组局部变量的组合,这些局部变量仅会在前置谓词成功应用于目标以后才会从目标中提取。基于这个目的,`instanceOf`操作符被扩展为了使用类型模式,而不像以前一样仅仅是使用类型。类型模式由一个指定的类型和一个模式变量组成。所以上面的示例就可以被改写为以下这种形式。
|
||
|
||
```java
|
||
if (obj instanceOf String str) {
|
||
// 完成其他对于字符串变量str的操作。
|
||
}
|
||
```
|
||
|
||
在上面这个示例中,变量`str`的作用域仅存在于其后的语句块内,所以变量`str`在其他的`instanceOf`模式匹配中还是可以使用的,前提是不能在`str`的作用域内。这样一条规则就引申出来以下两个问题:在与`instanceOf`操作符联合进行判断的布尔表达式中,模式匹配得到的变量能不能使用。对于这个问题的回答可以参考以下示例。
|
||
|
||
```java
|
||
// 以下使用 && 的表达式是可以使用的,因为 s.length() 在求值的时候,instanceOf 表达式已经返回了 true 值
|
||
// 如果 instanceOf 表达式返回了 false,那么这个布尔表达式将会“短路”
|
||
if (obj instanceOf String s && s.length() > 5) { }
|
||
// 下面使用 || 的表达式将会报错,因为 instanceOF 表达式返回 false 不能阻止 s.length() 继续计算
|
||
// 但是变量 s 此时并没有被初始化
|
||
if (obj instanceOf String s || s.length() > 5) { }
|
||
```
|
||
|
||
但是这里需要关注一个比较复杂的示例,注意这个示例中变量`p`的作用域。
|
||
|
||
```java
|
||
class Example {
|
||
String p;
|
||
|
||
void test(Object o) {
|
||
if (o instanceOf String p) {
|
||
// 在这个语句块内,p 指代的是来自于模式匹配的参数 o
|
||
} else {
|
||
// 在这个语句块内,p 指代的是类 Example 的字段 p
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 构建自包含的Java应用
|
||
|
||
Java应用的发布向来都是使用Jar文件,这是一种Zip格式的文件,将所有已经完成编译的Java类文件和资源文件整合在一起,形成一个可供JRE识别和执行的文件。Jar文件的执行必须依靠系统中安装的JRE,如果系统中没有安装JRE,或者安装了低版本的JRE,那么Java应用可能就不能正常运行。
|
||
|
||
但是现在大多数语言都已经开始支持在操作系统中直接运行,有的是采用直接将程序编译为系统原生可执行文件的形式,例如Go和Rust;有的则是采用Launcher的形式。为了能够让Java程序也在系统内可以不依赖系统JRE直接运行,在Java 14中就引入了一项孵化功能:打包工具,而这个工具在Java 16中已经完全稳定了。
|
||
|
||
打包工具的命令为`jpackage`,可以构建在Windows、macOS和Linux上的可执行文件和安装包。`jpackage`采用的是构建Launcher的形式,通过在发布的应用中包含一个JRE来支持用户应用的运行。`jpakcage`不支持交叉编译,所以只能在一种操作系统中打包当前系统可用的发布包。对于不同种类的操作系统,`jpackage`会形成以下不同结构的应用。
|
||
|
||
{% oss_image nf-java16/jpackage.svg jpackage打包结构 750 %}
|
||
|
||
`jpackage`命令只能通过命令行执行,目前并不提供GUI。不过在Gradle和Maven等构建工具的支持下,`jpackage`的配置还是比较容易的。
|
||
|
||
由于自Java 9起引入了模块系统,所以`jpackage`对于使用了模块系统的应用和没有使用模块系统的应用其打包方式是不一样的。
|
||
|
||
### 打包未使用模块系统的应用
|
||
|
||
假设应用的所有Jar文件都位于`build/lib`目录中,应用的主要启动Jar文件名为`app.jar`。那么就可以使用以下命令来进行打包。
|
||
|
||
```bash
|
||
jpackage --name app --input build/lib --main-jar app.jar
|
||
```
|
||
|
||
如果`app.jar`中没有使用`MANIFEST.MF`通过`Main-Class`属性指定主类,那么就需要在打包命令中指定主类。
|
||
|
||
```bash
|
||
jpackage --name app --input build/lib --main-jar app.jar --main-class app.Main
|
||
```
|
||
|
||
### 打包使用了模块系统的应用
|
||
|
||
模块化的应用在`build/lib`目录中除了存在Jar文件以外,还有可能存在JMod文件。所以这时打包命令就会变成以下这样。
|
||
|
||
```bash
|
||
jpackage --name app --module-path build/lib -m app
|
||
```
|
||
|
||
选项`-m`指定的是包含有主类的模块名称,主类可以在创建模块化Jar和JMod文件时,使用`--main-class`选项指定。如果模块化的Jar和JMod中没有设定主类,那么就需要使用以下格式的命令来指定主类。
|
||
|
||
```bash
|
||
jpackage --name app --module-path build/lib -m app/app.Main
|
||
```
|
||
|
||
### 整合运行时
|
||
|
||
默认情况下,`jpackage`会使用`jlink`工具来产生应用所需要包含的运行时镜像。不同的应用,其产生的运行时镜像也不相同。非模块化的应用,其包含的运行时镜像就是整个JDK模块集;而模块化的应用,其运行时镜像将只包含应用的主模块和所有依赖项的可传递闭包。`jpackage`在调用`jlink`的时候,默认会使用以下选项组合:`--strip-native-command --strip-debug --no-man-pages --no-header-files`。如果需要修改或者使用其他的选项,可以直接使用`--jlink-options`重新指定。
|
||
|
||
## 全功能的Unix-Domain Socket
|
||
|
||
Unix-Domain Socket主要用于同一主机上的进程间通信(IPC)。Unix-Domain Socket在大多数方面与TCP/IP Socket是十分类似的,最大的区别只是Unix-Domain Socket是通过文件系统路径名来寻址的。
|
||
|
||
对于本地进程间通信来说,Unix-Domain Socket要比TCP/IP回环链接更加安全和高效。这是因为Unix-Domain Socket只是严格用于同一系统上进程之间的通信,而且Socket会受到操作系统基于文件系统访问控制的保护,相比TCP/IP来说还具有更快的启动时间和数据吞吐量。在目前容器化迅猛发展的当下,Unix-Domain Socket更加适合于位于同一系统之上的容器之间的通信。
|
||
|
||
为了支持Unix-Domain Socket,Java 16增加了以下API。
|
||
|
||
- `java.net.UnixDomainSocketAddress`,用于支持Unix-Domain Socket地址。
|
||
- 在`java.net.StandardProtocolFamily`中增加了用于代表Unix-Domain Socket的常量。
|
||
- 在`SocketChannel`和`ServerSocketChannel`上增加了新的工厂方法用于描述新的协议簇。
|
||
- 更新`SocketChannel`和`ServerSocketChannel`的定义,以兼容Unix-Domain Socket的行为。
|
||
|
||
## 预览版功能
|
||
|
||
- 密封类
|
||
|
||
## 孵化功能
|
||
|
||
- Vector
|
||
- 外部链接器
|
||
- 外部内存访问
|