JDK9 新特性 (一)

Java8 在 2014 年 3 月份推出的,而历经曲折的 Java9 终于终于在 2017 年 9 月 21 日发布,中间历经 3 年多时间,Java9 提供了超过 150 项新功能特性,包括备受期待的模块化系统、可交互的 REPL 工具:jshell,JDK 编译工具,Java 公共 API 和私有代码,以及安全增强、扩展提升、性能管理改善等。可以说 Java 9 是一个庞大的系统工程,完全做了一个整体改变。Java8 中最核心的新特性就是 Lambda 表达式和 Stream API,那么对于 Java9 来说其中最核心莫过于模块化系统和 JShell 命令。

Java 更快的发布周期意味着开发者将不需要像以前一样为主要发布版本望眼欲穿。这也意味着开发者将可能跳过 Java 9 和它的不成熟的模块化功能,只需要再等待 6 个月就可以迎来新版本,这将可能解决开发者的纠结。反正 Java11 已经支持正式商用了,Java 11 将会获得 Oracle 提供的长期支持服务,直至 2026 年的 9 月。所以想用上稳定的、最新的 JDK 还是选择 Java11 吧,我将在后面的文章记述 Java11 的新特性以及 Java14 的部分新特性。

在这个网站上可以看到 JavaSE9 的新特性 《Overview of What’s New in JDK 9》

JEP 与 JSR

JEP (JDK Enhancement Proposals):jdk 改进提案,每当需要有新的设想时候,JEP 可以在 JCP (java community Process) 之前或者同时提出非正式的规范 (specification),被正式认可的 JEP 正式写进 JDK 的发展路线图并分配版本号。
JSR (Java Specification Requests): java 规范提案,新特性的规范出现在这一阶段,是指向 JCP (Java Community Process) 提出新增一个标准化技术规范的正式请求。请求可以来自于小组 / 项目、JEP、JCP 成员或者 Java 社区 (community) 成员的提案,每个 Java 版本都由相应的 JSR 支持。

  • 小组:对特定技术内容,比如安全、网络、HotSpot 等有共同兴趣的组织和个人
  • 项目: 编写代码、文档以及其他工作,至少由一个小组赞助支持,比如最近的 Lambda 计划,JigSaw 计划等

目录结构变化

mark

模块化系统

谈到 Java 9 大家往往第一个想到的就是 Jigsaw 项目。众所周知,Java 已经发展超过 20 年(95 年最初发布),Java 和相关生态在不断丰富的同时也越来越暴露出一些问题:

  • 问题一:Java 运行环境的膨胀和臃肿。每次 JVM 启动的至少会加载 30-60MB 内存,原因是 JVM 需要加载 rt.jar,不管其中的类是否被 classloader 加载,整个 jar 都会被 JVM 加载到内存当中去(而模块化可以根据模块的需要加载程序运行需要的 class)
  • 问题二:当代码库越来越大,创建复杂。不同版本的类库交叉依赖导致让人头疼的问题,这些都阻碍了 Java 开发和运行效率的提升。
  • 问题三:很难真正地对代码进行封装,而系统并没有对不同部分 (也就是 JAR 文件) 之间的依赖关系有个明确的概念。每一个公共类都可以被类路径之下任何其它的公共类所访问到,这样就会导致无意中使用了并不想被公开访问的 API。
  • 问题四:类路径本身也存在问题,你怎么知晓所有需要的 JAR 都已经有了,或者是不是会有重复的项呢?

模块化的概念,其实就是 package 外再裹一层,也就是说,用模块来管理各个 package,通过声明某个 package 暴露,不声明默认就是隐藏。因此,模块化使得代码组织上更安全,因为可以指定哪些部分暴露,哪些部分隐藏。

模块化实现目标:

  • 主要目的在于减少内存的开销
  • 只须必要模块,而非全部 jdk 模块,可简化各种类库和大型应用的开发和维护
  • 改进 JavaSE 平台,使其可以适应不同大小的计算设备
  • 改进其安全性,可维护性,提高性能

下面是模块化演示,我新建一个项目叫做 java9news,然后通过 IDEA 的 new module 功能,生成了两个模块,一个是 java9demo、一个是 java9test:

mark

java9demo 模块中有很简单的两个类 Person、User:

1
2
3
4
5
6
7
8
9
10
11
public class Person {
private String name;
private int age;
//Getter/ Setter /toString
}

public class User {
private String name;
private int age;
//Getter/ Setter /toString
}

java9test 模块是无法使用 java9demo 中的类的,必须引入一个 module-info.java 的文件,在 java9demo 模块中的 module-info.java:

1
2
3
4
5
module java9demo {

// 指出我们想导出的包
exports xpu.tim.bean;
}

在 java9test 模块中的 module-info.java:

1
2
3
4
5
6
7
module java9test {
// 指明我们想导入的模块
requires java9demo;

// 导入日志模块
requires java.logging;
}

接下来测试一下,之所以无法用 User,就是因为在 java9demo 模块中的 module-info.java 中并未指定把 xpu.tim.entity 这个包给导出,所以使用 User 类报 Error:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 测试 Java9 模块化特性
*/
public class ModuleTest {
private static final Logger LOGGER = Logger.getLogger ("Tim");

public static void main(String [] args) {
Person person = new Person("Tim", 20);
System.out.println (person);

//User user = new User (); // Error

LOGGER.info ("This one log");
}
}

Java 的 REPl 工具 jShell

像 Python 和 Scala 之类的语言早就有交互式编程环境 REPL (read - evaluate - print- loop) 了,以交互式的方式对语句和表达式进行求值。开发者只需要输入一些代码,就可以在编译前获得对程序的反馈。而之前的 Java 版本要想执行代码,必须创建文件、声明类、提供测试方法方可实现。

jShell 的实现目标 >

1、Java9 中终于拥有了 REPL 工具:jShell。利用 jShell 在没有创建类的情况下直接声明变量,计算表达式,执行语句。即开发时可以在命令行里直接运行 Java 的代码,而无需创建 Java 文件,无需跟人解释 public static void main (String [] args) 这句废话。

2、jShell 也可以从文件中加载语句或者将语句保存到文件中。

3、jShell 也可以是 tab 键进行自动补全和自动添加分号。

jShell 使用示例,jShell 可以使用 Tab 键补全:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
D:>jshell
| 欢迎使用 JShell -- 版本 9.0.1
| 要大致了解该版本,请键入: /help intro

jshell> System.out.println ("HelloWorld")
HelloWorld

jshell> int i = 10;
i ==> 10

jshell> int j = 20;
j ==> 20

jshell> int k = i + j;
k ==> 30

jshell> System.out.println (k)
30

jshell> public int add (int i, int j){
...> return i + j;
...> }
| 已创建 方法 add (int,int)

jshell> System.out.println (add (50, 100))
150

jshell> add (10, 20)
$8 ==> 30

mark

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
jshell> /help
| 键入 Java 语言表达式,语句或声明。
| 或者键入以下命令之一:
| /list [< 名称或 id>|-all|-start]
| 列出您键入的源
| /edit < 名称或 id>
| 编辑按名称或 id 引用的源条目
| /drop < 名称或 id>
| 删除按名称或 id 引用的源条目
| /save [-all|-history|-start] < 文件 & gt;
| 将片段源保存到文件。
| /open <file>
| 打开文件作为源输入
| /vars [< 名称或 id>|-all|-start]
| 列出已声明变量及其值
| /methods [< 名称或 id>|-all|-start]
| 列出已声明方法及其签名
| /types [< 名称或 id>|-all|-start]
| 列出已声明的类型
| /imports
| 列出导入的项
| /exit
| 退出 jshell
| /env [-class-path < 路径 & gt;] [-module-path < 路径 & gt;] [-add-modules < 模块 & gt;] ...
| 查看或更改评估上下文
| /reset [-class-path < 路径 & gt;] [-module-path < 路径 & gt;] [-add-modules < 模块 & gt;]...
| 重启 jshell
| /reload [-restore] [-quiet] [-class-path < 路径 & gt;] [-module-path < 路径 & gt;]...
| 重置和重放相关历史记录 -- 当前历史记录或上一个历史记录 (-restore)
| /history
| 您键入的内容的历史记录
| /help [<command>|<subject>]
| 获取 jshell 的相关信息
| /set editor|start|feedback|mode|prompt|truncation|format ...
| 设置 jshell 配置信息
| /? [<command>|<subject>]
| 获取 jshell 的相关信息
| /!
| 重新运行上一个片段
| /<id>
| 按 id 重新运行片段
| /-<n>
| 重新运行前面的第 n 个片段
|
| 有关详细信息,请键入 '/help', 后跟
| 命令或主题的名称。
| 例如 '/help/list' 或 '/help intro'。主题:
|
| intro
| jshell 工具的简介
| shortcuts
| 片段和命令输入提示,信息访问以及
| 自动代码生成的按键说明
| context
| /env/reload 和 /reset 的评估上下文选项

jshell>

mark

jshell 还可以从外部文件加载源代码,如下面是我在桌面上的一个 HelloWorld.java 文件:

1
2
3
4
5
6
// 测试从外部文件加载源代码 
void printHello() {
System.out.println (" 测试从外部文件加载源代码 & quot;);
}

printHello ();

mark

jShell 没有受检异常(编译时异常),本来应该强迫我们捕获一个 IOException,但却没有出现。因为 jShell 在后台为我们隐藏了。

mark

多版本兼容 jar 包

新版本的 Java 出现时,用户要花费数年时间才会切换到这个新的版本。这就意味着库得去向后兼容你想要支持的最老的 Java 版本(许多情况下就是 Java 6 或者 Java7)。这实际上意味着未来的很长一段时间,你都不能在库中运用 Java 9 所提供的新特性。幸运的是,多版本兼容 jar 功能能让你创建仅在特定版本的 Java 环境中运行库程序选择使用的 class 版本。

mark

如上图所示:root.jar 可以在 Java9 中使用,不过 A 或 B 类使用的不是顶层的 root.A 或 root.B 这两个 class, 而是处在 META-INF/versions/9 下面的这两个。这是特别为 Java 9 准备的 class 版本,可以运用 Java 9 所提供的特性和库。同时,在早期的 Java 诸版本中使用这个 JAR 也是能运行的,因为较老版本的 Java 只会看到顶层的 A 类或 B 类。

现有目录结构如下:

mark

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//java 中 Application.java
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;

public class Application {
public static void testMultiJar(){
Generator gen = new Generator();
System.out.println ("Generated strings: " + gen.createStrings ());
}

}


//java 中 Generator.java
import java.util.Set;
import java.util.HashSet;

public class Generator {
public Set<String> createStrings() {
Set<String> strings = new HashSet<String>();
strings.add ("Java");
strings.add ("8");
return strings;
}
}


//java9 中 Generator.java
import java.util.Set;

public class Generator {
public Set<String> createStrings() {
return Set.of ("Java", "9");
}
}

现在将其编译为 Jar 包:

1
2
3
javac -d build --release 8 src/main/java/*.java
javac -d build9 --release 9 src/main/java-9/*.java
jar --create --main-class=Application --file multijar.jar -C build . --release 9 -C build9 .

mark

接下来分别在 JDK8 和 JDK9 的环境中调用 Jar 包中的方法,结果如下图:

mark

接口的私有方法

Java 8 中规定接口中的方法除了抽象方法之外,还可以定义静态方法和默认的方法。一定程度上,扩展了接口的功能,此时的接口更像是一个抽象类。

在 Java 9 中,接口更加的灵活和强大,连方法的访问权限修饰符都可以声明为 private 的了,此时方法将不会成为你对外暴露的 API 的一部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
interface MyInterface {
//JDK7
void method1();

//JDK8: 静态方法
static void method2(){
System.out.println ("method2");
}

//JDK8:默认方法
default void method3(){
System.out.println ("method3");
method4 ();
}

//JDK9:私有方法
private void method4(){
System.out.println ("method");
}
}

class MyInterfaceImpl implements MyInterface{

@Override
public void method1() {

}
}

public class MyInterfaceTest{
public static void main(String [] args) {
MyInterface myInterface = new MyInterfaceImpl();
myInterface.method3 ();
//myInterface.method4 (); Error
}
}

钻石操作符使用升级

我们将能够与匿名实现类共同使用钻石操作符(diamond operator)在 java 8 中如下的操作是会报错的:

1
2
3
4
5
6
7
8
public class MyOperatorTest {
private List<String> flattenStrings(List<String>... lists) {
Set<String> set = new HashSet<>(){};
for(List<String> list : lists) {
set.addAll (list);
}
return new ArrayList<>(set);
}

那么在 JDK9 中呢?其实就是我们的匿名子类和泛型可以一起使用了:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyOperatorTest {
public static void main(String [] args) {
Set<String> set = new HashSet<>(){
@Override
public int size() {
return super.size () * 100;
}
};
set.addAll (Arrays.asList ("AAA", "BBB", "CCC"));
System.out.println (set.size ()); // 300
}
}

try 语句升级

JDK7 之前的版本如何进行资源关闭呢?无非就是 try-catch-finally 这种结构,在 finally 中保证资源关闭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyTryCatchTest {
public static void main(String [] args) {
InputStreamReader reader = null;
reader = new InputStreamReader(System.in);
try {
// 数据读取过程..
reader.read ();
} catch (IOException e) {
e.printStackTrace ();
}finally {
try {
reader.close ();
} catch (IOException e) {
e.printStackTrace ();
}
}
}
}

JDK7 出现了 try-with-resource,不用显式处理资源的关闭,但是要求执行后必须关闭的所有资源必须在 try 子句中初始化,否则编译不通过:

1
2
3
4
5
6
7
8
9
10
public class MyTryCatchTest {
public static void main(String [] args) {
try(InputStreamReader reader = new InputStreamReader(System.in)) {
// 数据读取过程..
reader.read ();
} catch (IOException e) {
e.printStackTrace ();
}
}
}

JDK9 中,用资源语句编写 try 将更容易,我们可以在 try 子句中使用已经初始化过的资源,此时的资源是 final 的:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyTryCatchTest {
public static void main(String [] args) {
InputStreamReader reader = new InputStreamReader(System.in);
OutputStreamWriter writer = new OutputStreamWriter(System.out);
try(reader; writer) {
// 数据读取过程..
reader.read ();
//reader = null; //Error 此时 reader 和 writer 是 final 的,不可再次赋值
} catch (IOException e) {
e.printStackTrace ();
}
}
}

UnderScore 使用限制

UnderScore 其实就是下划线,在 java 8 中,标识符可以独立使用 _ 来命名:

1
String _ = "HelloWorld";

但是在 java 9 中规定 _ 不再可以单独命名标识符了,如果使用则报错:

mark

String 存储结构变更

String 再也不用 char [] 来存储啦,改成了 byte [] 加上编码标记,节约了一些空间。在这里可以看到官方文档的说明: http://openjdk.java.net/jeps/254

1
2
3
4
5
6
7
8
9
10
11
Motivation (修改动机)
The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.

Description
We propose to change the internal representation of the String class from a UTF-16 char array to a byte array plus an encoding-flag field. The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.

String-related classes such as AbstractStringBuilder, StringBuilder, and StringBuffer will be updated to use the same representation, as will the HotSpot VM's intrinsic string operations.

This is purely an implementation change, with no changes to existing public interfaces. There are no plans to add any new public APIs or other interfaces.

The prototyping work done to date confirms the expected reduction in memory footprint, substantial reductions of GC activity, and minor performance regressions in some corner cases.

String 类的当前实现将字符存储在 char 数组中,每个字符使用两个字节(十六个比特位)。从许多不同应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且大多数 String 对象仅包含拉丁字符。 这样的字符只需要存储一个字节,因此此类 String 对象的内部 char 数组中的一半空间都没有使用。

我们建议将 String 类的内部表示形式从 UTF-16 字符数组更改为字节数组,再加上一个编码标志字段。 新的 String 类将基于字符串的内容存储编码为 ISO-8859-1 / Latin-1(每个字符一个字节)或 UTF-16(每个字符两个字节)的字符。 编码标志将指示使用哪种编码。

与字符串相关的类(例如 AbstractStringBuilder,StringBuilder 和 StringBuffer)将更新为使用相同的表示形式,HotSpot VM 的固有字符串操作也将使用相同的表示形式。这纯粹是实现更改,不更改现有的公共接口。 没有计划添加任何新的公共 API 或其他接口。

迄今为止完成的原型工作证实了在某些特殊情况下预期的内存占用减少,GC 活动大大减少以及性能下降的预期。

mark

那 StringBuffer 和 StringBuilder 是否仍无动于衷呢?其实由于 String 类的底层存储结构的更改会影响到 StringBuffer 和 StringBuier,我们看到 StringBuffer 和 StringBuilder 的源码,发现多了 @HotSpotIntrinsicCandidate 这个注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Constructs a string buffer with no characters in it and an
* initial capacity of 16 characters.
*/
@HotSpotIntrinsicCandidate
public StringBuffer() {
super(16);
}

/**
* Constructs a string builder with no characters in it and an
* initial capacity of 16 characters.
*/
@HotSpotIntrinsicCandidate
public StringBuilder() {
super(16);
}

JDK 的源码中,被 @HotSpotIntrinsicCandidate 标注的方法,在 HotSpot 中都有一套高效的实现,该高效实现基于 CPU 指令,运行时,HotSpot 维护的高效实现会替代 JDK 的源码实现,从而获得更高的效率。 所以可见 StringBuffer 和 StringBuilder 都是通过 HotSpot 的高效实现,其还也就是底层通过 byte [] 来实现的。

集合工厂方法:快速创建只读集合

要创建一个只读、不可改变的集合,必须构造和分配它,然后添加元素,最后包装成一个不可修改的集合。可以参考这些, http://openjdk.java.net/jeps/269

在 JDK1.8 中创建只读集合的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CollectionTest {
public static void main(String [] args) {
List<String> namesList = new ArrayList<>();
namesList.add ("Joe");
namesList.add ("Bob");
namesList.add ("Bill");
namesList = Collections.unmodifiableList (namesList);

List<String> namesList = new ArrayList<>();
namesList.addAll (Arrays.asList ("Joe", "Bob", "Bill"));
namesList = Collections.unmodifiableList (namesList);
}
}

但是在 JDK9 中可以直接这样操作(其实很多地方参考了这种设计,比如 JPA 中的分页参数就是典例):

1
2
3
4
5
6
7
8
9
10
11
Map<String, Integer> map = Collections.unmodifiableMap (new HashMap<>(){
{
put ("AAA", 1);
put ("BBB", 1);
put ("CCC", 1);
}
});

// 甚至如下的写法更简单
List<String> namesList = List.of ("Joe", "Bob", "Bill");
Map<String, Integer> map = Map.of ("AAA", 1, "BBB", 1, "CCC", 1);

在创建后,继续添加元素到这些集合会导致 UnsupportedOperationException。由于 Java 8 中接口方法的实现,可以直接在 List,Set 和 Map 的接口内定义这些方法,便于调用。

StreamAPI 增强

Java 的 Steam API 是 java 标准库最好的改进之一,让开发者能够快速运算,从而能够有效的利用数据并行计算。Java 8 提供的 Steam 能够利用多核架构实现声明式的数据处理。在 Java 9 中,Stream API 变得更好,Stream 接口中添加了 4 个新的方法:dropWhile, takeWhile, ofNullable,还有个 iterator 方法的新重载方法,可以让你提供一个 Predicate (判断条件) 来指定什么时候结束迭代。

除了对 Stream 本身的扩展,Optional 和 Stream 之间的结合也得到了改进。现在可以通过 Optional 的新方法 stream () 将一个 Optional 对象转换为一个 (可能是空的) Stream 对象。

takeWhile () 的使用:用于从 Stream 中获取一部分数据,接收一个 Predicate 来进行选择。在有序的 Stream 中,takeWhile 返回从开头开始的尽量多的元素。

1
2
3
4
5
6
7
8
9
10
11
public class StreamAPITest {
public static void main(String [] args) {
List<Integer> list = Arrays.asList (45, 43, 76, 87, 42, 77);
list.stream ().takeWhile (x -> x < 50)
.forEach (System.out::println);
System.out.println ();
list = Arrays.asList (1, 2, 3, 4, 5, 6, 7, 8);
list.stream ().takeWhile (x -> x < 5)
.forEach (System.out::println);
}
}

dropWhile () 的使用:dropWhile 的行为与 takeWhile 相反,返回剩余的元素。

1
2
3
4
5
6
7
8
9
10
11
public class StreamAPITest {
public static void main(String [] args) {
List<Integer> list = Arrays.asList (45, 43, 76, 87, 42, 77, 90, 73, 67, 88);
list.stream ().dropWhile (x -> x < 50)
.forEach (System.out::println);
System.out.println ();
list = Arrays.asList (1, 2, 3, 4, 5, 6, 7, 8);
list.stream ().dropWhile (x -> x < 5)
.forEach (System.out::println);
}
}

ofNullable () 的使用:Java 8 中 Stream 不能完全为 null,否则会报空指针异常。而 Java 9 中的 ofNullable 方法允许我们创建一个单元素 Stream,可以包含一个非空元素,也可以创建一个空 Stream。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StreamAPITest {
public static void main(String [] args) {
Stream<String> stringStream = Stream.of ("AA", "BB", null);
System.out.println (stringStream.count ()); //3

List<String> list = new ArrayList<>();
list.add ("AA");
list.add (null);
System.out.println (list.stream ().count ()); //2

Stream<Object> stream1 = Stream.ofNullable (null);
System.out.println (stream1.count ()); //0

Stream<String> stream = Stream.ofNullable ("hello world");
System.out.println (stream.count ()); //1
}
}

iterator () 重载的使用:

1
2
3
4
5
6
7
8
9
public class StreamAPITest {
public static void main(String [] args) {
// 原来的控制终止方式:
Stream.iterate (1,i -> i + 1).limit (10) .forEach (System.out::println);

// 现在的终止方式:
Stream.iterate (1,i -> i < 100,i -> i + 1) .forEach (System.out::println);
}
}

Optional 类中 stream () 的使用:

1
2
3
4
5
6
7
8
9
10
11
12
public class StreamAPITest {
public static void main(String [] args) {
List<String> list = new ArrayList<>();
list.add ("Tom");
list.add ("Jerry");
list.add ("Tim");
Optional<List<String>> optional = Optional.ofNullable (list);

Stream<List<String>> stream = optional.stream ();
stream.flatMap (x -> x.stream ()).forEach (System.out::println);
}
}