Java9 - 新特性
笔记
经过 4 次跳票,历经曲折的 Java 9 终于终于在 2017 年 9 月 21 日发布。Java 9 提供了超过 150 项新功能特性,让我们学习其中一些重要的内容。
2022-02-08 @Young Kbt
# Java 9概述
# Java 9不得不说的新特性
java 9 提供了超过 150 项新功能特性,包括备受期待的模块化系统、可交互的 REPL 工具:JShell,JDK 编译工具,Java 公共 API 和私有代码,以及安全增强、扩展提升、性能管理改善等。可以说 Java 9 是一个庞大的系统工程,完全做了一个整体改变。
具体来讲:
- 模块化系统(核心)
- JShell 命令(核心)
- 多版本兼容 jar 包
- 接口的私有方法
- 钻石操作符的使用升级
- 语法改进:try 语句
- 下划线使用限制
- String 存储结构变更
- 便利的集合特性:of()
- 增强的 Stream API
- 多分辨率图像 API
- 全新的 HTTP 客户端 API
- Deprecated 的相关 API
- 智能 Java 编译工具
- 统一的 JVM 日志系统
- javadoc 的 HTML 5 支持
- Javascript 引擎升级:Nashorn
- java 的动态编译器
# java语言后续版本的更迭
从Java 9 这个版本开始,Java 的计划发布周期是 6 个月,下一个 Java 的主版本将于 2018 年 3 月发布,命名为 Java 18.3,紧接着再过六个月将发布 Java 18.9。
这意味着 Java 的更新从传统的以特性驱动的发布周期,转变为以时间驱动的(6 个月为周期)发布模式,并逐步的将 Oracle JDK 原商业特性进行开源。
针对企业客户的需求,Oracle 将以三年为周期发布长期支持版本(long term support)。
# 如何看待Java 9的更新
Java 更快的发布周期意味着开发者将不需要像以前一样为主要发布版本望眼欲穿。这也意味着开发者将可能跳过 Java 9 和它的不成熟的模块化功能,只需要再等待 6 个月就可以迎来新版本,这将可能解决开发者的纠结。
oracle 理念:小步快跑,快速迭代
# Java 9资源
jdk 9 的下载:http://www.oracle.com/technetwork/java/javase/downloads/index-jsp-138363.html
java 9 帮助文档的下载:http://www.oracle.com/technetwork/java/javase/documentation/jdk9-doc-downloads-3850606.html
在线 Oracle JDK 9 文档:https://docs.oracle.com/javase/9/
Java 9 的新特性文档:http://openjdk.java.net/projects/jdk9/
下面表格是 Java 9 的新特性,左侧是英文,右侧是中文,左侧点击即可跳转到官网对该新特性的介绍文档。
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 计划等
# JDK和JRE的改变
# JDK与JRE的关系
JDK:Java Development Kit(Java 开发工具包)
JRE:Java Runtime Environment(Java 运行环境)
说明:
- JDK = JRE + 开发工具集(例如 Javac 编译工具等)
- JRE = JVM + Java SE 标准类库
# JDK8的目录结构
说明:
目录 | 作用 |
---|---|
bin 目录 | 包含命令行开发和调试工具,如 javac,jar 和 javadoc |
include 目录 | 包含在编译本地代码时使用的 C/C++ 头文件 |
lib 目录 | 包含 JDK 工具的几个 JAR 和其他类型的文件。 它有一个 tools.jar 文件,其中包含 javac 编译器的 Java 类 |
jre/bin 目录 | 包含基本命令,如 Java 命令。 在 Windows 平台上,它包含系统的运行时动态链接库(DLL) |
jre/lib 目录 | 包含用户可编辑的配置文件,如 .properties 和 .policy 文件。包含几个 JAR。rt.jar 文件包含运行时的 Java 类和资源文件 |
# JDK9的目录结构
说明:
目录 | 作用 |
---|---|
bin 目录 | 包含所有命令。 在 Windows 平台上,它继续包含系统的运行时动态链接库 |
conf 目录 | 包含用户可编辑的配置文件,例如以前位于 jre\lib 目录中的 .properties 和 .policy 文件 |
include 目录 | 包含要在以前编译本地代码时使用的 C/C++ 头文件。它只存在于 JDK 中 |
jmods 目录 | 包含 JMOD 格式的平台模块。创建自定义运行时映像时需要它。它只存在于 JDK 中 |
legal 目录 | 包含法律声明 |
lib 目录 | 包含非 Windows 平台上的动态链接本地库。其子目录和文件不应由开发人员直接编辑或使用 |
# 模块化系统
参考 Jigsaw,现在改名为 Modularity。
官方 Feature:
- 200:The Modular JDK (opens new window)
- 201:Modular Source Code (opens new window)
- 220:Modular Run-Time Images (opens new window)
- 260:Encapsulate Most Internal APIs (opens new window)
- 261:Module System (opens new window)
- 282:jlink: The Java Linker (opens new window)
# 产生背景及意义
谈到 Java 9 大家往往第一个想到的就是 Jigsaw 项目。众所周知,Java 已经发展超过 20 年(95 年最初发布),Java 和相关生态在不断丰富的同时也越来越暴露出一些问题:
- Java 运行环境的膨胀和臃肿。每次 JVM 启动的时候,至少会有 30~60MB 的内存加载,主要原因是 JVM 需要加载 rt.jar,不管其中的类是否被classloader 加载,第一步 整个 jar 都会被 JVM 加载到内存当中去(而模块化可以根据模块的需要加载程序运行需要的 Class)
- 当代码库越来越大,创建复杂,盘根错节的「意大利面条式代码」的几率呈指数级的增长。不同版本的 类库交叉依赖 导致让人头疼的问题,这些都 阻碍了 Java 开发和运行效率的提升
- 很难真正地对代码进行封装,而系统并没有对不同部分(也就是 JAR 文件)之间的依赖关系有个明确的概念。每一个公共类都可以被类路径之下任何其它的公共类所访问到,这样就会导致无意中使用了并不想被公开访问的 API
- 类路径本身也存在问题: 你怎么知晓所有需要的 JAR 都已经有了,或者是不是会有重复的项呢
同时,由于 兼容性等各方面的掣肘,对 Java 进行大刀阔斧的革新越来越困难,Jigsaw 从 Java 7 阶段就开始筹备,Java 8 阶段进行了大量工作,终于在 Java 9 里落地,一种千呼万唤始出来的意味。
Jigsaw 项目(后期更名为 Modularity)的工作量和难度大大超出了初始规划。JSR 376 Java 平台模块化系统(JPMS,Java Platform Module System)作为 Jigsaw 项目的核心,其主体部分被分解成 6 个 JEP(JDK Enhancement Proposals)。
作为 Java 9 平台 最大的一个特性,随着 Java 平台模块化系统的落地,开发人员无需再为不断膨胀的 Java 平台苦恼,例如,您可以 使用 jlink 工具,根据需要定制运行时环境。这对于拥有大量镜像的容器应用场景或复杂依赖关系的大型应用等,都具有非常重要的意义。
本质上讲,模块(module)的概念,其实就是 package 外再裹一层,也就是说,用模块来管理各个 package,通过声明某个 package 暴露,不声明默认就是隐藏。因此,模块化使得代码组织上 更安全,因为它可以指定哪些部分可以暴露,哪些部分隐藏。
# 设计理念
模块独立、化繁为简。
模块化(以 Java 平台模块系统的形式)将 JDK 分成一组模块,可以在编译时,运行时或者构建时进行组合。
# 实现目标
- 主要目的在于减少内存的开销
- 只须必要模块,而非全部 JDK 模块,可 简化各种类库和大型应用的开发和维护
- 改进 Java SE 平台,使其可以 适应不同大小的计算设备
- 改进其安全性,可维护性,提高性能
# 代码举例
模块将由通常的类和新的模块声明文件(module-info.java)组成。该文件是位于 Java 代码结构的顶层,该模块描述符明确地定义了 我们的模块需要什么依赖关系,以及哪些模块被外部使用。在 exports 子句中未提及的所有包默认情况下将封装在模块中,不能在外部使用。
首先结构如下:
根目录 (java9test)
├── java9demo
│ ├── src
│ │ ├── cn.youngkbt
│ │ │ ├── bean
│ │ │ │ └── Person.java
│ │ │ ├── entity
│ │ │ │ └── User.java
│ │ │ └── module-info.java
├── javatest
│ ├── src
│ │ ├── cn.youngkbt
│ │ │ ├── java
│ │ │ │ └── ModuleTest.java
2
3
4
5
6
7
8
9
10
11
12
13
14
如图:
创建 java 9demo 模块 src 的 module-info.java 文件
这个文件导出包名给其他模块使用
module java9demo { exports cn.youngkbt.bean; exports cn.youngkbt.entry; }
1
2
3
4exports:控制着哪些包可以被其它模块访问到。所有不被导出的包默认都被封装在模块里面。
然后创建 javatest 模块 src 的 module-info.java 文件
这个文件则是引用 java9demo 导出的包和其他需要的包
module javatest { requires java9demo; requires java.logging; }
1
2
3
4requires:指明对其它模块的依赖。
java 9demo 模块中的 ModuleTest 类使用如下(如果不 requires 引入,则获取不到包下的类):
public class ModuleTest { private static final Logger LOGGER = Logger.getLogger("youngkbt"); public static void main(String[] args) { Person p = new Person("Tom",12); System.out.println(p); User user = new User(); LOGGER.info("aaaaaa"); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 总结
每个模块创建 module-info.java
文件,导出其他模块需要的包,引入其他模块已经导出的包。
其实 Maven 通过 pom.xml 已经实现了模块化,这里演示的是没有使用 Maven 项目的情况下,如何在不同模块下引入其他模块的包。
关于更多 Java 9 模块编程的内容请参考一本书:《Java 9 Modularity (opens new window)》,里面讲的比较详细,介绍了当前 Java 对 jar 之间以来的管理是多么的混乱,引入 modularity 之后的改变会是很明显的差别。
# JShell
官方 Feature:222:
# 产生背景
像 Python 和 Scala 之类的语言早就有交互式编程环境 REPL(read - evaluate - print - loop)了,以交互式的方式对语句和表达式进行求值。开发者只需要输入一些代码,就可以在编译前获得对程序的反馈。而 之前的 Java 版本要想执行代码,必须创建文件、声明类、提供测试方法方可实现。
# 理念设计
即写即得、快速运行。
# 目标实现
Java 9 中终于拥有了 REPL 工具:JShell。利用 JShell 在没有创建类的情况下直接声明变量,计算表达式,执行语句。即开发时可以在命令行里直接运行 Java 的代码,而无需创建 Java 文件,无需跟人解释 public static void main(String[] args)
这句废话。
JShell 也可以从 文件中加载语句 或者将语句保存到文件中。
JShell 也可以是 Tab 键进行自动补全 和 自动添加分号。
# 举例使用
调出 JShell
如果已经配置了 Java 9 的环境变量,则直接在命令行输入 jshell
即可,如果没有配置环境遍历,则去 Java 根目录下的 bin 目录找到 jshell.exe
。
获取帮助
任何命令行工具都有 help
帮助提示,JShell 也不例外,输入 /help intro
即可看到帮助。
基本使用
和在 Java 文件写代码一样,JShell 会自动执行输入的代码,并且保存起来,当退出后,再清空代码。
System.out.println("你好!world");
int i = 10;
int j = 20;
int k = i + k;
System.out.println(k); // 30
public int add(int m, int n){
return m + n;
}
int k = add(1,2);
System.out.println(k); // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意:在 JShell 环境下,语句末尾的 ;
是可选的。但推荐还是最好加上。提高代码可读性。
导入指定的包
指定包路径,可导入包。
默认已经导入如下的所有包:(包含
java.lang
包)
通过 /import
指令可以查看默认导入的包:
只需按下 Tab 键,就能自动补全代码
列出当前 session 里所有有效的代码片段
通过 /list
指令列出曾经输入的代码片段。
查看当前 session 下所有创建过的变量
通过 /var
指令列出所有创建过的变量。
查看当前 session 下所有创建过的方法
通过 /methods
指令列出所有创建过的方法。
注意:我们还可以重新定义相同方法名和参数列表的方法,即为对现有方法的修改(或覆盖)。
使用外部代码编辑器来编写 Java 代码
通过 /methods
指令弹出文本框,在文本框进行代码的编写。
从外部文件加载源代码
通过 /open
命令打开指定路径下的 Java 文件,并自动执行,在命令行输出结果。
/open D:/HelloWorld.java
没有受检异常(编译时异常)
下面代码本来应该强迫我们捕获一个 IOException,但却没有出现。因为 JShell 在后台为我们隐藏了。
URL url = new URL("http://notes.youngkbt.cn");
退出 JShell
/exit
# 多版本兼容jar包
官方 Feature:238:
# 使用说明
当一个新版本的 Java 出现的时候,你的库用户要花费数年时间才会切换到这个新的版本。这就意味着 库得去向后兼容你想要支持的最老的 Java 版本(许多情况下就是 Java 6 或者 Java7)。这实际上意味着未来的很长一段时间,你都不能在库中运用 Java 9 所提供的新特性。幸运的是,多版本兼容 jar 功能能让你创建仅在特定版本的 Java 环境中运行库程序选择使用的 Class 版本。
举例 1:
jar root
- A.class // 旧版本
- B.class // 旧版本
- C.class
- D.class
- META-INF
- versions
- 9
- A.class // Java 9 版本
- B.class // Java 9 版本
2
3
4
5
6
7
8
9
10
说明:在上述场景中,root.jar 可以在 Java 9 中使用,不过 A 或 B 类使用的不是顶层的 root.A 或 root.B 这两个 Class,而是处在 META-INF/versions/9
下面的这两个。这是 特别为 Java 9 准备的 Class 版本,可以运用 Java 9 所提供的特性和库。同时,在早期的 Java 诸版本中使用这个 JAR 也是能运行的,因为 较老版本的 Java 只会看到顶层的 A 类或 B 类。
举例 2:
jar root
- A.class // 旧版本
- B.class // 旧版本
- C.class
- D.class
- META-INF
- versions
- 9
- A.class // Java 9 版本
- B.class // Java 9 版本
- 10
- A.class // Java 10 版本
2
3
4
5
6
7
8
9
10
11
12
这样一来,旧版本使用上方的 ABCD Class 文件,而 Java 9 使用自己的 AB Class 文件,Java 10 则使用自己的 A Class。
官方说明:
By this scheme, it is possible for versions of a class designed for a later Java platform release to override the version of that same class designed for an earlier Java platform release.
# 使用举例
看视频更清楚:https://www.bilibili.com/video/BV1v4411U7Sk?p=32
步骤一:提供必要的类
Java 8 环境
在指定目录(E:\teach\01_Java9\multijar\src\main\java\cn\youngkbt)下提供如下的类:
Generator.java
public class Generator {
public Set<String> createStrings() {
Set<String> strings = new HashSet<String>();
strings.add("Java");
strings.add("8");
return strings;
}
}
2
3
4
5
6
7
8
Application.java
public class Application {
public static void testMultiJar() {
com.youngkbt.Generator gen = new Generator();
System.out.println("Generated strings: " +
gen.createStrings());
}
}
2
3
4
5
6
7
Java 9 环境
在如下目录(E:\teach\01_Java9\multijar\src\main\java-9\cn\youngkbt)下提供同名的类:
Generator.java
public class Generator {
public Set<String> createStrings() {
return Set.of("Java", "9");
}
}
2
3
4
5
步骤二:打包
打包 Java 8 环境的代码,指令如下:
javac -d build --release 8 src/main/java/com/youngkbt/*.java
打包 Java 9 环境的代码,指令如下:
javac -d build9 --release 9 src/main/java-9/com/youngkbt/*.java
最后打包整个多版本 jar 包,指令如下:
jar --create --main-class=Application --file multijar.jar -C build . --release 9 -C build9 .
步骤三:进行测试
将生成的多版本 jar 包分别放到 Java 8 和 9 环境下运行,就会发现得到的结果不一样。
Generated strings: Java 8
Generated strings: Java 9
2
# 语法改进 - 接口的私有方法
官方 Feature:213:
# 使用说明
Java 8 中规定接口中的方法除了抽象方法之外,还可以定义静态方法和默认的方法。一定程度上,扩展了接口的功能,此时的接口更像是一个抽象类。
在 Java 9 中,接口更加的灵活和强大,连方法的访问权限修饰符都可以声明为 private 的了,此时方法将不会成为你对外暴露的 API 的一部分。
# 使用举例
/**
*
* 面试题:抽象类 和 接口 的异同?
* ① 二者的定义: a. 声明的方式 b. 内部的结构(jdk 7 ;jdk 8 ; jdk 9)
* ② 共同点:不能实例化;以多态的方式使用
* ③ 不同点:单继承;多实现
*/
interface MyInterface {
//jdk 7 : 只能声明全局常量(public static final)和抽象方法(public abstract)
void normalInterfaceMethod();
// jdk 8 : 声明静态方法 和 默认方法
public static void method1(){
System.out.println("method1");
}
default void methodDefault1() {
init();
}
// 私有方法
private void init() {
System.out.println("默认方法中的通用操作");
}
}
class MyInterfaceImpl implements MyInterface{
@Override
public void normalInterfaceMethod() {
System.out.println("实现接口的方法");
}
}
public class MyInterfaceTest{
public static void main(String[] args) {
MyInterfaceImpl impl = new MyInterfaceImpl();
impl.methodDefault1();
// impl.init();// 不能调用
}
}
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
# 语法改进 - 钻石操作符使用升级
# 使用说明
我们将能够与匿名实现类共同使用钻石操作符(diamond operator) 在 Java 8 中如下的操作是会报错的(第 2 行结尾有 {}
):
private List<String> flattenStrings(List<String>... lists) {
Set<String> set = new HashSet<>(){};
for(List<String> list : lists) {
set.addAll(list);
}
return new ArrayList<>(set);
}
2
3
4
5
6
7
编译报错信息:
'<>' cannot be used with anonymous classes
# 使用举例
public class DiamondOperatorTest {
@Test
public void testDiamondOperator(){
diamondOperator();
}
public void diamondOperator(){
// 创建一个继承于 HashSet 的匿名子类的对象
Set<String> set = new HashSet<>(){};// 编译通过
set.add("MM");
set.add("JJ");
set.add("GG");
set.add("DD");
for(String s : set){
System.out.println(s);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 语法改进 - try语句
在 Java 8 之前,我们习惯于这样处理资源的关闭:
public void testTry1(){
InputStreamReader reader = null;
try{
reader = new InputStreamReader(System.in);
//流的操作
reader.read();
}catch (IOException e){
e.printStackTrace();
}finally{
if(reader != null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java 8 中,可以实现资源的自动关闭,但是要求执行后必须关闭的所有资源必须在 try 子句中初始化,否则编译不通过。如下例所示:
public void testTry2(){
try(InputStreamReader reader = new InputStreamReader(System.in)){
}catch (IOException e){
e.printStackTrace();
}
}
2
3
4
5
6
Java 9 中,用资源语句编写 try 将更容易,我们可以在 try 子句中使用已经初始化过的资源,此时的资源是 final 的:
public void testTry3(){
InputStreamReader reader = new InputStreamReader(System.in);
OutputStreamWriter writer = new OutputStreamWriter(System.out);
try(reader;writer){
// reader 是 final 的,不可再被赋值
// reader = null;
}catch (IOException e){
e.printStackTrace();
}
}
2
3
4
5
6
7
8
9
10
# 语法改进 - 下划线使用的限制
UnderScore(下划线)。
在 Java 8 中,标识符可以独立使用 _
来命名:
String _ = "hello";
System.out.println(_);
2
但是,在 Java 9 中规定 _
不再可以单独命名标识符了,如果使用, 会报错:
As of Java 9, '_' is a keyword, and may not be used as an identifier
# String存储结构变更
官方 Feature:JEP 254:
# 产生背景
String 类的当前实现将字符存储在字符数组,每个字符使用两个字节(16 位)。数据从许多不同的应用程序收集的数据表明字符串是堆使用的主要组成部分,而且最重要的是对象仅包含拉丁字母 1。这样的角色只需要一个字节的存储空间,因此占内部字符数组空间的一半许多这样的字符串对象将被闲置。
# 使用说明
我们建议更改 String 类的内部表示形式从 UTF-16 字符数组到字节数组再加上编码标志领域新的字符串类将存储编码为 ISO-8859-1 / 拉丁语-1(每个字符一个字节),或作为 UTF-16(两个字节每个字符),基于字符串的内容。编码标志将指示使用哪种编码。
结论:String 再也不用 char[] 来存储啦,改成了 byte[] 加上编码标记,节约了一些空间。
public final class String implements java.io.Serializable, Comparable<String>,
CharSequence {
@Stable
private final byte[] value;
}
2
3
4
5
# StringBuffer与StringBuilder
那 StringBuffer 和 StringBuilder 是否仍无动于衷呢?
与字符串相关的类,如 AbstractStringBuilder、StringBuilder、StringBuffer 将更新为使用相同的表示形式。
结论:AbstractStringBuilder、StringBuilder、StringBuffer 底层也变成了 byte[]
。
/**
* String:jdk 8 及之前:底层使用char[]存储;jdk 9 : 底层使用byte[] (encoding flag)
* StringBuffer:jdk 8 及之前:底层使用char[]存储;jdk 9 : 底层使用byte[]
* StringBuilder:jdk 8 及之前:底层使用char[]存储;jdk 9 : 底层使用byte[]
*
* String:不可变的字符序列;
* StringBuffer:可变的字符序列;线程安全的,效率低;
* StringBuilder:可变的字符序列;线程不安全的,效率高(jdk 5.0)
*
*/
2
3
4
5
6
7
8
9
10
# 集合工厂方法 - 快速创建只读集合
官方 Feature:269:
# 产生背景
要创建一个 只读、不可改变 的集合,必须构造和分配它,然后添加元素,最后包装成一个不可修改的集合。
Java 8 环境
比如使用 Collections
提供的 unmodifiableList
方法可以创建 只读、不可改变 的 List 集合:
public void test() {
List<String> namesList = new ArrayList <>();
namesList.add("Joe");
namesList.add("Bob");
namesList.add("Bill");
namesList = Collections.unmodifiableList(namesList);
System.out.println(namesList);
}
2
3
4
5
6
7
8
缺点:我们一下写了五行。即:它不能表达为单个表达式。
当然,我们也可以稍微简单点处理:
public void test() {
// List
List<String> list = Collections.unmodifiableList(Arrays.asList("a", "b", "c"));
// Set
Set<String> set = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("a", "b", "c")));
// Map:如下操作不适用于 jdk 8 及之前版本,适用于 jdk 9
Map<String,Integer> map = Collections.unmodifiableMap(new HashMap<>(){
{
put("a", 1);
put("b", 2);
put("c", 3);
}
});
map.forEach((k,v) -> System.out.println(k + ":" + v));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java 9 因此 引入了方便的方法,这使得类似的事情更容易表达。
# 使用说明
Java 9 给各个集合引入了 of()
静态方法,该方法代替 Collections.unmodifiableXXX
创建出 只读、不可改变 的集合。
List firsnamesList = List.of("Joe", "Bob", "Bill");
调用集合中静态方法 of()
,可以将不同数量的参数传输到此工厂方法中。此功能可用于 Set 和 List,也可用于 Map 的类似形式。此时得到的集合,是不可变的:在创建后,继续添加元素到这些集合会导致 UnsupportedOperationException
。
java.lang.UnsupportedOperationException
由于 Java 8 中接口方法的实现,可以直接在 List,Set 和 Map 的接口内定义这些方法,便于调用。
注意:Set.of()
里不能传重复的元素,虽然正常情况下 Set 类会自动把重复的元素去掉,但是 of()
方法遇到重复元素是会报错的,如:
Set<Integer> set = Set.of(100, 200, 300, 100);
# 使用举例
public void test(){
// List
List<Integer> list = List.of(1, 2, 3);
list.forEach(System.out::println);
// Set
Set<Integer> set = Set.of(2, 3, 4);
// Map
// 创建只读集合的方式一:
Map<String, Integer> map = Map.of("Tom", 23, "Jerry", 22, "Lilei", 12, "HanMeimei", 18);
// 创建只读集合的方式二:
Map<String, Integer> map1 = Map.ofEntries(Map.entry("Tom", 23), Map.entry("Jerry", 21));
System.out.println(map1);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 增强的Stream API
# 使用说明
Java 的 Steam API 是 Java 标准库最好的改进之一,让开发者能够快速运算,从而能够有效的利用数据并行计算。Java 8 提供的 Steam 能够利用多核架构实现声明式的数据处理。
在 Java 9 中,Stream API 变得更好,Stream 接口中添加了 4 个新的方法:dropWhile
、takeWhile
、ofNullable
,还有个 iterate
方法的新重载方法,可以让你提供一个 Predicate(判断条件)来指定什么时候结束迭代。
除了对 Stream 本身的扩展,Optional 和 Stream 之间的结合也得到了改进。现在可以通过 Optional 的新方法 stream()
将一个 Optional 对象转换为一个(可能是空的)Stream 对象。
# 使用举例
takeWhile()
的使用
用于从 Stream 中获取一部分数据,接收一个 Predicate 来进行选择。在有序的 Stream 中,takeWhile
返回从开头开始的尽量多的元素。
public void test1(){
List<Integer> list = Arrays.asList(45,56,33,77,44,98,76,78,33);
Stream<Integer> stream = list.stream();
stream.takeWhile(x -> x < 70).forEach(System.out::println);
System.out.println();
List<Integer> list1 = Arrays.asList(1,2,3,4,5,6,7,8);
list1.stream().takeWhile(x -> x < 5).forEach(System.out::println); // 输出:45 56 33
}
2
3
4
5
6
7
8
9
10
11
12
也就是找到不符合条件的元素后,停止寻找,虽然后面还有一些元素符合条件,也不会获取。
dropWhile()
的使用
dropWhile
的行为与 takeWhile
相反,返回剩余的元素。
public void test2(){
List<Integer> list = Arrays.asList(45,56,33,77,44,98,76,78,33);
Stream<Integer> stream = list.stream();
stream.dropWhile(x -> x < 70).forEach(System.out::println);
System.out.println();
List<Integer> list1 = Arrays.asList(1,2,3,4,5,6,7,8);
list1.stream().dropWhile(x -> x < 5).forEach(System.out::println); // 输出 77 44 98 76 78 33
}
2
3
4
5
6
7
8
9
10
11
12
也就是找到符合条件的元素后,停止寻找,虽然后面还有一些元素不符合条件,也不会获取。
ofNullable()
的使用
Java 8 中 Stream 不能完全为 null,否则会报空指针异常。而 Java 9 中的 ofNullable
方法允许我们创建一个单元素 Stream,可以包含一个非空元素,也可以创建一个空 Stream。
public void test3(){
Stream<Integer> stream1 = Stream.of(1, 2, 3, null);
stream1.forEach(System.out::println);
System.out.println();
// 如果只有单个元素,此元素不能为 null.否则报 NullPointerException
// Stream<Object> stream2 = Stream.of(null);
// jdk 9:新增 ofNullable(T t)
Stream<String> stream3 = Stream.ofNullable("Tom");
System.out.println(stream3.count()); // 1
Stream<String> stream4 = Stream.ofNullable(null);
System.out.println(stream4.count()); // 0
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
iterator()
重载的使用
Stream 的实例化:
- 通过集合的
stream()
- 通过数组工具类:
Arrays.stream()
- Stream 中静态方法:
of()
iterator()
/generate()
原来的 Java 8 控制终止方式加 limit:
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()
的使用
// Optional 类中提供了转换为 Stream 的方法:stream()
public void test1() {
List<String> list = new ArrayList<>();
list.add("Tom");
list.add("Jerry");
list.add("Tim");
Optional<List<String>> optional = Optional.ofNullable(list);
Stream<String> stream = optional.stream().flatMap(x -> x.stream());
// System.out.println(stream.count());
stream.forEach(System.out::println);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 输出流直接转输入流
InputStream 终于有了一个非常有用的方法:transferTo
,可以用来将数据直接传输到 OutputStream,这是在处理原始数据流时非常常见的一种用法。
如下示例:
public void test2() throws IOException {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
InputStream inputStream = classLoader.getResourceAsStream("file.txt"); // 包路径下
try (OutputStream outputStream = new FileOutputStream("file2.txt")) {
inputStream.transferTo(outputStream); // 把输入流中的所有数据直接自动地复制到输出流中
}
inputStream.close();
}
2
3
4
5
6
7
8
这样我们就会发现在 src 目录下发现 file2.txt 文件,并且内容与 file.txt 一样。
# 多分辨率图像API
官方 Feature
- 251:Multi-Resolution Images (opens new window)
- 263:HiDPI Graphics on Windows and Linux (opens new window)
# 产生背景
在 Mac 上,JDK 已经支持视网膜显示,但在 Linux 和 Windows 上,它并没有。在那里,Java 程序在当前的高分辨率屏幕上可能看起来很小,不能使用它们。这是因为像素用于这些系统的大小计算(无论像素实际有多大)。毕竟,高分辨率显示器的有效部分是像素非常小。
JEP 263 以这样的方式扩展了 JDK,即 Windows 和 Linux 也考虑到像素的大小。为此,使用比现在更多的现代 API:Direct2D for Windows 和 GTK +,而不是 Xlib for Linux 图形,窗口和文本由此自动缩放。
JEP 251 还提供处理多分辨率图像的能力,即 包含不同分辨率的相同图像的文件。根据相应屏幕的 DPI 度量,然后以适当的分辨率使用图像。
# 使用说明
- 新的 API 定义在
java.awt.image
包下 - 将不同分辨率的图像封装到一张(多分辨率的)图像中,作为它的变体
- 获取这个图像的所有变体
- 获取特定分辨率的图像变体,表示一张已知分辨率单位为 DPI 的特定尺寸大小的逻辑图像,并且这张图像是最佳的变体
- 基于当前屏幕分辨率大小和运用的图像转换算法,
java.awt.Graphics
类可以从接口 MultiResolutionImage 获取所需的变体 - MultiResolutionImage 的基础实现是
java.awt.image.BaseMultiResolutionImage
。
# 全新的HTTP客户端API
官方 Feature:110:
# 使用说明
HTTP,用于传输网页的协议,早在 1997 年就被采用在目前的 1.1 版本中。直到 2015 年,HTTP2 才成为标准。
HTTP/1.1 和 HTTP/2 的主要区别是如何在客户端和服务器之间构建和传输数据。HTTP/1.1 依赖于请求/响应周期。HTTP/2 允许服务器 push
数据:它可以发送比客户端请求更多的数据。这使得它可以优先处理并发送对于首先加载网页至关重要的数据。
Java 9 中有新的方式来处理 HTTP 调用。它提供了一个新的 HTTP 客户端(HttpClient),它将 替代仅适用于 blocking 模式的 HttpURLConnection
(HttpURLConnection 是在 HTTP 1.0 的时代创建的,并使用了协议无关的方法),并提供 对 WebSocket 和 HTTP/2 的支持。
此外,HTTP 客户端还提供 API 来处理 HTTP/2 的特性,比如流和服务器推送等功能。
全新的 HTTP 客户端 API 可以从 jdk.incubator.httpclient
模块中获取。因为在默认情况下,这个模块是不能根据 classpath 获取的,需要使用 add modules
命令选项配置这个模块,将这个模块添加到 classpath 中。
# 使用举例
public void testURL() {
//jdk 9 中 使用 HttpClient 替换原有的 HttpURLConnection
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder(URI.create("http://www.youngkbt.com")).GET().build();
HttpResponse<String> response = null;
response = client.send(req, HttpResponse.BodyHandler.asString());
System.out.println(response.statusCode());
System.out.println(response.version().name());
System.out.println(response.body());
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Deprecated的相关API
官方 Feature
- 211:Elide Deprecation Warnings on Import Statements (opens new window)
- 214:Remove GC Combinations Deprecated in JDK 8 (opens new window)
- 277:Enhanced Deprecation 289: Deprecate the Applet API (opens new window)
- 289:Deprecate the Applet API (opens new window)
- 291:Deprecate the Concurrent Mark Sweep (CMS) Garbage Collector (opens new window)
使用说明
Java 9 废弃或者移除了几个不常用的功能。其中最主要的是 Applet API
,现在是标记为废弃的。随着对安全要求的提高,主流浏览器已经取消对 Java 浏览器插件的支持。HTML5 的出现也进一步加速了它的消亡。开发者现在可以使用像 Java Web Start 这样的技术来代替 Applet,它可以实现从浏览器启动应用程序或者安装应用程序。
同时,appletviewer 工具也被标记为废弃。
# 智能Java编译工具
官方 Feature
- 139:Enhance javac to Improve Build Speed. (opens new window)
- 199:Smart Java Compilation, Phase Two (opens new window)
使用说明
智能 java 编译工具(sjavac) 的第一个阶段始于 JEP139 这个项目,用于 在多核处理器情况下提升 JDK 的编译速度。如今,这个项目已经进入第二阶段,即 JEP199,其目的是改进 Java 编译工具,并 取代目前 JDK 编译工具 javac,继而成为 Java 环境默认的通用的智能编译工具。
JDK 9 还 更新了 javac 编译器 以便能够将 Java 9 代码编译运行在低版本 Java 中。
# 统一的JVM日志系统和垃圾回收器
官方 Feature:
使用说明
日志是解决问题的唯一有效途径:曾经很难知道导致 JVM 性能问题和导致 JVM 崩溃的根本原因。不同的 JVM 日志的碎片化和日志选项(例如:JVM 组件对于日志使用的是不同的机制和规则),这使得 JVM 难以进行调试。
解决该问题最佳方法:对所有的 JVM 组件引入一个单一的系统,这些 JVM 组件支持细粒度的和易配置的 JVM 日志。
Java 9 已经将 G1 作为默认的垃圾回收器,统一处理新生代和老年代的垃圾回收。
# javadoc的HTML5支持
官方 Feature
使用说明
JDK 8:生成的 Java 帮助文档是在 HTML 4 中,而 HTML 4 已经是很久的标准了。
JDK 9:javadoc 的输出,现在符合兼容 HTML 5 标准。
下图是 Java 8 中生成的 HTML 页面,如果想要找到一些类文档,必须在 Google 中搜索。
下图是在 Java 9 中,添加了一个搜索框。
# Javascript引擎升级 - Nashorn
官方 Feature
- 236:Parser API for Nashorn (opens new window)
- 292:Implement Selected ECMAScript 6 Features in Nashorn (opens new window)
使用说明
Nashorn 项目在 JDK 9 中得到改进,它为 Java 提供轻量级的 Javascript 运行时。Nashorn 项目跟随 Netscape 的 Rhino 项目,目的是为了在 Java 中实现一个高性能但轻量级的 JavaScript 运行时。Nashorn 项目使得 Java 应用能够嵌入 JavaScript。它在 JDK 8 中为 Java 提供一个 JavaScript 引擎。
JDK 9 包含一个用来解析 Nashorn 的 ECMAScript 语法树的 API。这个 API 使得 IDE 和服务端框架不需要依赖 Nashorn 项目的内部实现类,就能够分析 ECMAScript 代码。
# java的动态编译器
官方 Feature:
- 243:Java-Level JVM Compiler Interface (opens new window)
- 295:Ahead-of-Time Compilation (opens new window)
# 产生背景
Oracle 一直在努力提高 Java 启动和运行时性能,希望其能够在更广泛的场景达到或接近本地语言的性能。但是,直到今天,谈到 Java,很多 C/C++ 开发者还是会不屑地评价为 启动慢,吃内存。
简单说,这主要是因为 Java 编译产生的类文件是 Java 虚拟机可以理解的二进制代码,而不是真正的可执行的本地代码,需要 Java 虚拟机 进行解释和编译,这带来了额外的开销。
# 使用说明
JIT(Just-in-time)编译器可以在运行时将热点编译成本地代码,速度很快。但是 Java 项目现在变得很大很复杂,因此 JIT 编译器需要 花费较长时间才能热身完,而且有些 Java 方法还没法编译,性能方面也会下降。AOT 编译就是为了解决这些问题而生的。
在 JDK 9 中,AOT(JEP 295:Ahead-of-Time Compilation)作为实验特性被引入进来,开发者可以利用新的 jaotc 工具将重点代码转换成类似类库一样的文件。虽然仍处于试验阶段,但这个功能使得 Java 应用在被虚拟机启动之前能够先将 Java 类编译为原生代码。此功能旨在改进小型和大型应用程序的启动时间,同时对峰值性能的影响很小。
但是 Java 技术供应商 Excelsior 的营销总监 Dmitry Leskov 担心 AOT 编译技术不够成熟,希望 Oracle 能够 等到 Java 10 时有个更稳定版本才发布。
另外 JVMCI(JEP 243:Java-Level JVM Compiler Interface)等特性,对于整个编程语言的发展,可能都具有非常重要的意义,虽然未必引起了广泛关注。目前 Graal Core API 已经被集成进入 Java 9,虽然还只是初始一小步,但是完全用 Java 语言来实现的可靠的、高性能的动态编译器,似乎不再是遥不可及,这是 Java 虚拟机开发工程师的福音。
与此同时,随着 Truffle 框架和 Substrate VM 的发展,已经让个别信心满满的工程师高呼 One VM to Rule Them All!
, 也许就在不远的将来 Ploygot 以一种另类的方式成为现实。
# 总结
# 在java9中看不到什么
一个标准化和轻量级的 JSON API
一个标准化和轻量级的 JSON API 被许多 Java 开发人员所青睐。但是由于资金问题无法在 Java 9 中见到,但并不会削减掉。Java 平台首席架构师 Mark Reinhold 在 JDK 9 邮件列中说:「这个 JEP 将是平台上的一个有用的补充,但是在计划中,它并不像 Oracle 资助的其他功能那么重要,可能会重新考虑 JDK 10 或更高版本中实现。」
新的货币 API
对许多应用而言货币价值都是一个关键的特性,但 JDK 对此却几乎没有任何支持。严格来讲,现有的 java.util.Currency
类只是代表了当前 ISO 4217 货币的一个数据结构,但 并没有关联的值或者自定义货币。JDK 对货币的运算及转换也没有内建的支持,更别说有一个能够代表货币值的标准类型了。
此前,Oracle 公布的 JSR 354 定义了一套新的 Java 货币 API:JavaMoney,计划会在 Java 9 中正式引入。但是目前没有出现在 JDK 9 中。
不过,如果你用的是 Maven 的话,可以做如下的添加,即可使用相关的 API 处理货币:
<dependency>
<groupId>org.javamoney</groupId>
<artifactId>moneta</artifactId>
<version>0.9</version>
</dependency>
2
3
4
5
代码参考,可以访问 https://github.com/JavaMoney
,里面已经给出了使用说明和示例。
# 展望
随着云计算和 AI 等技术浪潮,当前的计算模式和场景正在发生翻天覆地的变化,不仅对 Java 的发展速度提出了更高要求,也深刻影响着 Java 技术的发展方向。传统的大型企业或互联网应用,正在被云端、容器化应用、模块化的微服务甚至是函数(FaaS,Function-as-a-Service)所替代。
Java 虽然标榜面向对象编程,却毫不顾忌的 加入面向接口编程思想,又扯出 匿名对象 之概念,每增加一个新的东西,对 Java 的根本所在的面向对象思想的一次冲击。反观 Python,抓住面向对象的本质,又能在函数编程思想方面游刃有余。Java 对标 C/C++,以抛掉内存管理为卖点,却又陷入了 JVM 优化的噩梦。选择比努力更重要,选择 Java 的人更需要对它有更清晰的认识。
Java 需要在新的计算场景下,改进开发效率。这话说的有点笼统,我谈一些自己的体会,Java 代码虽然进行了一些类型推断等改进,更易用的集合 API 等,但仍然给开发者留下了过于刻板、形式主义的印象,这是一个长期的改进方向。