简介
Java 8 及之前,版本都是特性驱动的版本更新,就是有重大的特性产生,然后进行更新。
Java 9 开始,JDK 开始以时间为驱动进行更新,以半年为周期,到时即更新。三年出一个长期支持版,其他都是短暂的版本。
目前的长期支持版有 Java 8、Java 11 和 Java 17。这些版本大家注意在将来的工作中使用的概率更高,也就是说我们将来研发使用 Java 11,然后使用 Java 17 是必然的,只是一个时间的问题。
Java9新特性
概述
经过 4 次推迟,历经曲折的 Java 9 最终在 2017 年 9 月 21 日发布。因为里面加入的模块化系统,在最初设想的时候并没有想过那么复杂,花费的时间超出预估时间。距离 Java 8 大约三年时间。Java 9 提供了超过 150 项新功能特性,包括备受期待的模块化系统、可交互的 REPL 工具: jshell、JDK编译工具。语法层面的改变:Java 公共API 和私有代码,以及安全增强、扩展提升、性能管理改善等。可以说 Java 9 是一个庞大的系统工程,完全做了一个整体改变。但是这个巨大改变的功劳,都给了 Java 11 了,目前 Oracle 对 8、11 都长期支持。9、10不支持了,只能从历史版本(jdk.java.net/)中下载。
Java 11 将会获得 Oracle 提供的长期支持服务,直至 2026 年 9 月。从 Java 9 这个版本开始,Java 的计划发布周期是 6 个月,下一个 Java 的主版本将于 2018 年 3 月发布,命名为 Java 18.3(Java10),紧接着再过六个月将发布 Java18.9(Java 11)。这意味着 Java 的更新从传统的以特性驱动的发布周期,转变为以时间驱动的(6个月为周期)发布模式。(更快的时间周期,Oracle 的理念就是小步快跑,快速迭代,像 IBM(DB2 数据库,保守型内部测试才投入市场),并逐步的将 Oracle JDK 原商业特性进行开源。针对企业客户的需求,Oracle 将以三年为周期发布长期支持版本(long term support)。
脑图

目录的变化

目录作用介绍:
- bin:包含命令行开发和调试工具。如 javac、jar、javadoc。
- include:包含编译本地代码时使用的 C/C++ 头部文件。
- lib:包含 JDK 工具的几个 jar 和其他类型的文件,他有一个 tools.jar 文件,其中含 javac 编译器的 java 类。
- jre/bin:包含基本指令。如 java 指令。在 Windows 平台上,它包含系统的运行时动态链接。
- jre/lib:包含用户可编辑的配置文件,如 properties 和 .policy 文件,包含几个 jar 文件。rt.jar 文件包含运行时的 Java 类和资源文件。

目录介绍:
- bin:包含所有指令,在 Windows 平台上,他继续包含系统的运行时动态链接。
- conf:包含用户可编辑的配置文件,例如之前位于 jre/lib 目录中的 .properties 和 .policy 文件。
- includes:包含在以前编译本地代码时使用 C/C++头文件,他只存在于 JDK 中。
- jmods:包含 JMOD 格式的平台模块,创建自定义运行时映像需要他,它只存在于 JDK 中。
- legal:法律声明。
- lib:包含非 Windows 平台上的动态链接本地库,其子目录和文件不应由开发人员直接编译或使用。
从 9 开始以后的 JDK 目录结构都是如此。
语法层次变化
钻石操作符
Java 8 中,匿名内部类不能使用钻石操作符,如下代码在 Java 8 中是报错的,匿名内部类这里不支持泛型推断,重写的方法不明确泛型:

这里匿名内部类中的 <> 号里必须要和前面的声明保持一致,不能空着不写,这样重写的方法就根据匿名内部类的泛型。

但是这种写法在 Java 9 中就允许了。
try语法升级
普通的 try catch finally 语句,要释放的资源可以放到 finally 语句块中:
public static void main(String[] args) {
InputStreamReader reader = null;
try {
reader = new InputStreamReader(System.in);
int read = reader.read();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 这里可以释放资源
if (null != reader) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Java 8 中已经对 try 语法进行了升级,可以将要释放的资源放到 try 后面的小括号中,这样就不用通过finally 语句块释放资源了,但是要求执行后必须关闭的资源一定要放在 try 子句中进行初始化,否则编译不通过。
下面的案例中,reader 必须放在 try 后面的小括号中进行初始化。
public static void main(String[] args) {
try (InputStreamReader reader = new InputStreamReader(System.in)) {
int read = reader.read();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
Java 9 资源的关闭操作,我们可以在 try 子句中使用已经初始化的资源但是此时的资源必须是 final 修饰的,final 可以省略不写。
// Java 9 try 语法升级
public static void main(String[] args) throws FileNotFoundException {
InputStreamReader isr = new InputStreamReader(new FileInputStream("d:/UserMapper.xml"));
OutputStreamWriter isw = new OutputStreamWriter(new FileOutputStream("d:/UserMapper1.xml"));
try (isr; isw) {
isr.read();
} catch (Exception e) {
e.printStackTrace();
}
}
下划线命名标识符使用限制
通常,标识符命名组成:字母,数字,下划线,美元符。
Java 8 中,可以使用一个 _ 作为标识符的命名。
public static void main(String[] args) {
String _ = "Hello World!";
}
Java 9 中,就不可以使用一个 _ 作为标识符的命名了,无法通过编译,但是标识符中仍然可以使用 _,必须配合其他内容。
public static void main(String[] args) {
// As of Java 9, "_" is a keyword, and may not be used as an identifier.
String _ = "Hello World!";
String user_name = "Trump";
}
API层次变化
接口的私有方法
Java 7 中,接口只能有抽象方法。
Java 8 中,接口中 static 和 default 修饰的方法可以拥有方法体。
Java 9 中,接口中可以使用 private 修饰方法,并拥有方法体。但是接口中的成员变量仍然不能用 private修饰。private 方法只能在默认方法中调用。
public interface Demo {
int A = 100;
void m1();
static void hello() {
System.out.println("Hello");
}
default void m2() {
m3();
}
private void m3() {
System.out.println("It's a private method");
}
}
String底层数据结构
Java 8 中的 String 源码,String 类内部维护的是一个 final 修饰的私有 char 数组。说明 String 的底层是通过 char 数组存储字符串的。

Java 9 中 String 的源码,String 类内部维护的是一个 final 修饰的私有 byte 数组,说明 String的底层是通过 byte 数组存储字符串的。

大多数 String 对象只包含 latin - 1 字符。这样的字符只需要一个字节的存储空间,因此这样的 String 对象的内部字符数组中有一半的空间没有使用, 我们建议将 String 类的内部表示形式从 UTF-16 字符数组更改为一个字节数组加上一个结束编码标志字段。
Stream的4的方法
Java 9 中,Stream 接口添加了 4 个新方法:takeWhile、dropWhile、ofNullable,还有一个 iterate 方法的新重载方法,可以通过一个 Predicate 来指定什么时候结束迭代。
/**
* 测试 Stream 新增 takeWhile 方法
* takeWhile 从流中的头开始取元素,直到不满足条件为止。
*/
public static void testTakeWhile(){
List<Integer> list = Arrays.asList(1, 89, 63, 45, 72, 65, 41, 65, 82, 35, 95, 100);
// 从头开始取所有奇数,直到遇见一个偶数为止
list.stream().takeWhile(e -> e % 2 == 1).forEach(System.out::println);
}
/**
* 测试 Stream 新增 dropWhile 方法
* dropWhile 从头开始删除满足条件的数据,直到遇见第一个不满足的位置,并保留剩余元素。
*/
public static void testDropWhile() {
List<Integer> list = Arrays.asList(2, 86, 63, 45, 72, 65, 41, 65, 82, 35, 95, 100);
// 删除流开头所有的偶数,直到遇见奇数为止
list.stream().dropWhile(e -> e % 2 == 0).forEach(System.out::println);
}
/**
* 测试 Stream 新增 ofNullable 方法
* ofNullable 允许创建 Stream 流时,只放入一个 null。
*/
public static void testOfNullable() {
// of 方法获取流, 允许元素中有多个 null 值
Stream<Integer> stream1 = Stream.of(10, 20, 30, null);
// 如果元素中只有一个null,是不允许的
Stream<Integer> stream2 = Stream.of(null);
// Java 9 中,如果元素为 null,返回的是一个空 Stream,如果不为 null,返回一个只有一个元素的 Stream
Stream<Integer> stream3 = Stream.ofNullable(null);
}
/**
* 测试 Stream 新增 iterate 方法
* iterate 指定种子数,指定条件和迭代方式来获取流。
*/
public static void testNewIterate(){
// Java 8 通过 generate 方法获取一个 Stream
Stream.generate(Math::random).limit(10).forEach(System.out::println);
// Java 8 通过 iterate 获取一个 Stream
Stream.iterate(0, t-> t + 2).limit(10).forEach(System.out::println);
// Java 9 通过重载 iterate 获取 Stream
Stream.iterate(0, t -> t < 10, t-> t + 1).forEach(System.out::println);
}
除了 Stream 本身的扩展,Optional 和 Stream 之间的结合也得到了改进,现在可以通过 Optional 的新方法将一个 Optional 对象转换为一个 Stream对象(可能是空的)。
/**
* Optional类新增Stream方法,可以将一个Optional转换为Stream
*/
public static void testOptionalStream() {
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 10, 5, 45, 95, 36, 85, 47);
Optional<List<Integer>> optional = Optional.ofNullable(list);
// 通过 Optional 的 Stream 方法获取一个 Stream
Stream<List<Integer>> stream = optional.stream();
// 以为内部的每个元素也是一个 List,通过 flatMap 方法,将内部的 List 转换为 Stream 后再放入一个大Stream
stream.flatMap(x -> x.stream()).forEach(System.out::println);
}
InputStream的transferTo方法
InputStream 新增 transferTo 方法,可以用来将数据直接传输到 OutpuStream,这是在处理原始数据时非常常见的一种方法。
InputStream inputStream = new FileInputStream("d:/aaa.txt");
OutputStream outputStream = new FileOutputStream("d:/bbb.txt");
try (inputStream; outputStream) {
inputStream.transferTo(outputStream);
} catch (IOException e) {
e.printStackTrace();
}
只读集合
Java 8 要创建一个只读、不可改变的集合,必须构造和分配他,然后添加元素,然后再包装成一个不可修的集合。
List<String> list new ArrayList<>();
list.add("Tom");
list.add("Jerry");
list.add("Mark");
list.add("Jhon");
list = Collections.unmodifiableList(list);
System.out.println(list);
放入数据后,后要通过 unmodifiableList 才能让集合变为只读集合,不能表达为单个的表达式。Java 9 通过集合工厂方法,创建一个只读集合,只要通过新增的 of 方法即可完成创建。
public static void main(String[] args) {
List<String> list = List.of("张三", "李四", "王五");
System.out.println(list);
list.set(0, "aaa");
System.out.println(list);
}
其他变化
模块化
谈到 Java 9 大家往往第一个想到的就是 Jigsaw 项目(后改名为 Modularity)。众所周知,Java 已经发展超过 20年(95 年最初发布),Java 和相关生态在不断丰富的同时也越来越暴露出一些问题:
- Java 运行环境的膨胀和臃肿。每次JVM启动的时候,至少会有 30 ~ 60 MB 的内存加载,主要原因是 JVM 需要加载 rt.jar,不管其中的类是否被
Classloader加载,第一步整个 jar 都会被 JVM 加载到内存当中去(而模块化可以根据模块的需要加载程序运行需要的 class)。 - 当代码库越来越大,创建复杂,盘根错节的“意大利面条式代码”的几率呈指数级的增长。不同版本的类库交叉依赖导致让人头疼的问题,这些都阻碍了 Java 开发和运行效率的提升。
- 很难真正地对代码进行封装,而系统并没有对不同部分(也就是 JAR 文件)之间的依赖关系有个明确的概念。每一个公共类都可以被类路径之下任何其它的公共类所访问到,这样就会导致无意中使用了并不想被公开访问的 API。
本质上讲,模块化就是在 package 外面包裹一层。说白了项目下有众多模块进行项目管理,管理各个模块,比如一个电商项目下面有支付模块、购物模块。模块跟模块之间相互调用,这样代码就更安全,可以指定哪些暴露、哪些隐藏。
模块之间的可访问性是所使用的模块和使用模块之间的双向协议:模块明确地使其公共类型可供其他模块使用,而且使用这些公共类型的模块明确声明对第一个模块的依赖,模块中所有未导出的软件包都是模块的私有的,他们不能在模块之外使用。之前做不到,现在可以考虑这个事了。
创建一个模块,只需要在根目录下创建 module-info.java 文件即可:
module my-mod1 {
// 设置当前模块对外暴露的包
exports org.codeart.controller;
}
再创建一个模块,引入前一个模块:
module my-mod2 {
requires my-mod1;
}
这样就可以使用 my-mod1 模块中的类了。
REPL工具
像 Python 和 Scala 之类的语言早就有交互式编程环境 REPL (read - evaluate - print - loop) 了,以交互式的方式对语句和表达式进行求值。开发者只需要输入一些代码,就可以在编译前获得对程序的反馈。而之前的 Java 版本要想执行代码,必须创建文件、声明类、提供测试方法方可实现。

将环境变量配置为 Java 9,就可以在控制命令台使用 jshell 命令了:如果电脑上安装了其他版本的 JDK,环境变量也是其他版本,大家可以在 dos 上通过 cd 切换到指定版本的 bin 目录下,就可以使用该版本的 jshell 了。

笔者认为此功能非常鸡肋,Java 这类语言就不适合交互式命令行工具。了解即可,有兴趣可以 Google 一下 JShell 的命令。