文章目录
- JDK8-17的特性发生了哪些变化
- 垃圾回收器
- Java交互式编程(无需启动idea也可执行代码)
- 接口定义扩展(默认方法、静态方法属性、私有方法)
- String底层结构变更(char[] to bytes[])
- of 创建不可变序列(List.of、Set.of、Map.of)
- HTTP 2 协议接口
- 引入 var 关键字(类型声明简化)
- 字符串增强(去空格、拷贝)
- lambda 表达式类型推导(通过var特性省略具体类型定义)
- switch 增强(支持返回值)
- 支持文本块定义
- instanceof 模式匹配(省略强转)
- 引入record 关键字(类上标注该关键字省略get,set,构造方法)
- 新增密封类sealed的定义(不允许继承)
- switch二度加强(类型匹配)
- 模块化特性
- 鸣谢
JDK8-17的特性发生了哪些变化
垃圾回收器
JDK 8 默认的垃圾回收器组合是:Parallel Scavenge ( 新生代 ) 和 Parallel ( 老年代 )
。
在 JDK 9 之后,G1
成为默认的垃圾收集器。它将内存空间无差别地划分为 region 块,且将大型对象单独放在 Humongous 区域管理。其设计目标是建立 “可停顿时间模型”,保证用户在 M 秒时间片内,回收器工作不超过 N ( GC 的资源用量占比不超过 N/M )。
CMS ( Concurrent Mark Sweep )
在 Oracle JDK 14 当中被正式移除,同时弃用了 ParallelScavenge + serialOld GC
组合。
Oracle JDK 11 引入了新一代 ZGC
垃圾回收器,其支持 TB 级内存容量,暂停时间低 ( <10ms ),对整个程序吞吐量的影响小于 15%。在 JDK 15 之后,ZGC 可正式投入到生产环境中,并通过 -XX:+UseZGC
启动,且性能在 JDK 16,17 之后得到了进一步增强。
Open JDK 推出的则是Shenandoah
垃圾回收器,由 Red Hat 团队开发,是 ZGC
垃圾回收器的 “竞品”。但由于它并不是 Oracle 的 “亲儿子”,因此在一定程度上遭到了排挤,Oracle 明确拒绝将这款垃圾回收器纳入其中。
Error occurred during initialization of VM
Option -XX:+UseShenandoahGC not supported
JDK 9 之后的版本至今,默认的垃圾回收器仍然为 G1
。
Java交互式编程(无需启动idea也可执行代码)
简单讲就是不用启动idea也可以通过终端的方式运行Java代码了,那通过什么启动?当然是我们的jshell
了。默认在jdk的安装目录的jdk17/bin/jshell.exe
。效果类似如下了:
PS C:\Users\liJunhu\IdeaProjects\testForJava17> jshell
| Welcome to JShell -- Version 17.0.2
| For an introduction type: /help intro
jshell> int i = 10
i ==> 10
jshell> System.out.println(i)
10
接口定义扩展(默认方法、静态方法属性、私有方法)
JDK 8,9 两个版本中均对接口的概念做了少许拓展,现在接口支持定义默认方法,静态方法/属性,私有方法
interface IService{
// implements 该接口的类将自动获取该方法。 - jdk 8.
default void h(){}
// 在接口定义的属性直接被视为 static。 -jdk 8.
double Pi = 3.1415;
// 可以在接口直接定义静态方法。 - jdk 8.
static void f(){}
// 可以在接口内直接定义私有方法。 -jdk 9.
private void g(){}
}
String底层结构变更(char[] to bytes[])
在 JDK 8 及之前的版本当中,String 的底层使用char[]
数组存储内容;而在 JDK 9 之后,字符串的内容实际改用bytes[]
字节形式存储,并配合 encodingFlag 配合编码解码。这个修改对上层代码透明,即用户层面的各种 String 操作没有发生任何变化
of 创建不可变序列(List.of、Set.of、Map.of)
集合提供了创建不可变的集合实例方法
List<Integer> list = List.of(1, 2, 3, 4, 5);
Set<Integer> set = Set.of(1, 2, 3, 4, 5);
// k1,v1,k2,v2,...,形式创建
Map<Integer, String> map1 = Map.of(1, "v1", 2, "v2");
// 通过 Map.entry 形式创建
Map<Integer, String> map2 = Map.ofEntries(
Map.entry(1, "v1"),
Map.entry(2, "v2"),
Map.entry(3, "v3"),
Map.entry(4, "v4")
);
HTTP 2 协议接口
JDK 9 中引入了新的 API 以支持 HTTP 2.0
版本。注,部分 API 在 JDK 11 之后的版本发生了更替
// throws IOException, InterruptedException
// 构建 client, 支持 http 2.0
HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build();
// 构建请求
HttpRequest req = HttpRequest.newBuilder(URI.create("https://www.baidu.com")).build();
// 发送请求
HttpResponse<String> resp = client.send(req,HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());
引入 var 关键字(类型声明简化)
JDK 10 支持使用 var
简化局部变化的类型声明,比如:
// 显然 arrayList 是一个 ArrayList<Integer> 类型。
ArrayList<Integer> arrayList = new ArrayList<>();
适当的精简可以提高代码的可读性,其 arrayList 的实际类型将交给编译器进行推断。
var arrayList = new ArrayList<Integer>();
字符串增强(去空格、拷贝)
JDK 11 新增了部分对字符串的处理方法
println("".isBlank()); // 判断字符串是否为 "".
println(" welcome to java 17 ".strip()); // 去掉首尾空格
println(" welcome to java 17".stripLeading()); // 去掉首部空格
println("welcome to java 17 ".stripTrailing()); // 去掉尾部空格
println("word".repeat(2)); // "wordword"
lambda 表达式类型推导(通过var特性省略具体类型定义)
JDK 11 允许将 var
关键字应用到 lambda 表达式中,起到简化声明的作用。比如:
@FunctionalInterface
interface Mapper<A,B>{
B map(A a);
}
// var 关键字现可以用于 lambda 表达式。
Mapper<String,Integer> string2int = (var a) -> a.length();
switch 增强(支持返回值)
增强版 switch 在 JDK 12 作为预览特性引入。在 JDK 14 之后,增强版 switch
语句块具备返回值。
var simple = switch (lang) {
case "java" -> "j";
case "go" -> "g";
default -> "<non>";
};
在新版本的switch
语句中,分支创建可以使用 ->
符号,且 cases
的判断不会引发穿透现象,因此不需要显式地在每一个分支后面标注break
了。下面例子展示了增强 switch
的更多特性:
var simple = switch (lang) {
// 1. 允许在一个 case 中匹配多个值
case "java","scala","groovy" -> "jvm";
case "c","cpp" -> "c";
case "go" -> "g";
default -> {
// 2. 复杂分支内返回值使用 yield 关键字 (相当于 return)
if(lang == null){
yield "<unknown>";
}else yield "<non>";
}
};
支持文本块定义
JDK 13 允许使用三引号"""
表示长文本内容。
var block = """
lang: java
version: 13
dbname: mysql
ip: 192.168.140.2
usr: root
pwd: 1000
""";
// 6 行
block.lines().count();
文本块内允许插入\
阻止换行,如:
var block = """
lang: java\
version: 13\
dbname: mysql\
ip: 192.168.140.2\
usr: root\
pwd: 1000
""";
// 实际效果:
// "lang:javaversion: 13dbname: mysql..."
// 1 行
block.lines().count();
另外注意,每行末尾的空格会被忽略,除非主动将其替换为 /s
。
instanceof 模式匹配(省略强转)
如果一个上转型对象的具体类型已经被 instanceof
关键字确定,那么其后续操作可省略强制转换,见下面的示例:
// o 可能是 String 类型或者是 Double 类型。
Object o = new Random().nextInt() % 2 == 0 ? "java16" : 1000.0d;
// s 相当于被确定类型之后的 o。
if(o instanceof String s){
// 不再需要 ((String)o).length()
System.out.println(s.length());
}else {
System.out.println();
}
引入record 关键字(类上标注该关键字省略get,set,构造方法)
被record
关键字修饰的类相当于 Scala
的样例类 case class
,或者可看成是 Lombok 中 @Data
注解的一个 "低配"
替代。编译器会一次性代替用户生成 getters,constructors,toString
等方法。
record Student(String name,Integer age){}
// ....
var student = new Student("Wang Fang",13);
student.age();
student.name();
新增密封类sealed的定义(不允许继承)
其 sealed class
的概念和 Scala
类似,密封类不允许在包外被继承拓展,密封类必须具备子类。
// 密封类
public sealed class People {}
// 密封类 People 必须至少有一个子类。
// 非密封的 People 子类
non-sealed class Teacher extends People{}
// 密封的 People 子类。
sealed class Driver extends People{}
non-sealed class TruckDriver extends Driver{}
密封类可以声明子类,但必须需要严格声明其子类是密封( sealed )
或者是非密封 ( non-sealed )
的。以下是 JDK 17 的预览功能:permits
关键字可进一步限制同一个包下有哪些类允许继承它。
// 即使在一个包下,也只有 Teacher
和 Driver
被允许继承 People
。
public sealed class People permits Teacher,Driver{}
non-sealed class Teacher extends People{}
non-sealed class Driver extends People{}
switch二度加强(类型匹配)
JDK 17 对 switch
语句做了进一步增强,它现在支持匹配类型以及判空的功能:
Number v1 = 3.1415d;
switch (v1) {
case null -> println("null");
case Float f -> println(f);
case Double d -> println(d);
default -> println("NaN");
}
模块化特性
JDK 9 引入了模块化特性,称 Java Platform Module System
,简称 JPMS
,或者 Project jigsaw
。模块化的优势有三点:
- 改进组件之间的依赖管理,引入比 jar 包粒度更大的 Module。
- 使得 java 程序更容易轻量级部署。
- 改进性能和安全性。
模块化还使得精简庞大的 JRE 成为可能
Why we need module
JDK9-JPMS模块化wshten-CSDN博客jpms
在提及模块化之前,首先要谈谈 “为什么不能只依赖 jar 包”。直到 JDK 8 之前,一切 Java 工程都是基于 jar 包构建的,而 jar 包本质上只是 “多个 package 的压缩包”,自身完全不携带任何描述控制权限和引用依赖等信息。
因此,jar 包本身不会告知 JVM 它还依赖哪些 jar 包,这完全需要开发者自行判断 jar 包之间的依赖关系并下载缺失的 jar 包。好在后来用户拥有了 Maven,Gradle,Sbt 这类包管理工具,才避免了 Jar-hell 的问题 ( 类似地,还有 Windows 的 dll-hell )。
模块 module 本质上仍然是 jar 包 ( 下文称作为模块的 jar 包为 module-jar
),但它额外增加了一个描述模块规则的 module-info.java
文件,可以理解成 "模块 = 普通 jar 包 + 权限控制"
。jar 包之间的关联现在就是靠module-info.java
文件来组织的,其格式为:
// ${moduleName} 填写模块名称。
module ${moduleName} {
// 描述此 module 的更多规则,见后文。
}
原则上模块的命名应当和项目名保持一致,但这并不是必须的。注,模块 module 之间是平级的,只有相互依赖的关系,没有 has-a
的 “父子关系”。
"导入 jar 还是 module-jar
" 对 用户透明,用户可以放心地将模块的 jar 包当成普通 jar 包来使用,也可以将普通 jar 包当成 module-jar 包来使用 ( 原因见后文的模块分类部分 )。在 IntelliJ IDEA 编译器下打包一个 module-jar
, 除了 在项目根目录下 新建一个 module-info.java
文件之外,其它步骤和打包一个普通 jar 没有什么区别。
( 注:From modules with dependencies...
选项可让 IDE 补充 Jar 包所需的 META-INF/MANIFEST.MF
)
特别注意,用户如果打算打包一个 module-jar
,那么类文件将不能声明在项目的顶级目录,通俗的说就是不能把类文件直接扔在src
源文件根目录下了。否则会报错:
Caused by: java.lang.module.InvalidModuleDescriptorException: XXX.class found in top-level directory (unnamed package not allowed in module)
JDK 9 之后有两种执行模式:作为普通的 jar 包执行 ( 向后兼容 ),或者是作为module-jar
执行。
# 视作普通的 jar 包运行
java -jar ${JarPath}
# 运行 module-jar 包,
# --module-path, -p : 提供目标模块及依赖的 .jar, .jmods 所在的目录。
# --module, -m : 提供模块名称以及主程序类全限定名。
java --module-path ${ModulePath} --module ${moduleName}/${MainClass}
# 示例
# java --module-path . --module priv.jdk9test.utils/priv.jdk9test.utils.TimePrinter
--module-path
和 --class-path
的概念很相似。不过 --module-path
中的 *.jar
或者是*.jmod
文件 ( 该文件格式见后文的 JMOD ) 被当作模块来处理,而--class-path
中的的jar
包仍按照传统的方式处理。
下面简单介绍
module-info.java
文件的几个权限控制规则:
exports [ to ... ]
opens [ to ... ]
requires [ static | transitive ]
provide ... with ...
uses ...
注意,模块导出 ( exports
) 以包 ( package
) 为单位,而导入 (requires
) 以模块为单位,只有声明导入了模块,才能继续在代码中import
此模块 导出的包。
模块 ( 包括后文的 ) 约束 只对模块起作用。如果用户的项目只是普通项目 ( 没有编写 module-info.java
文件 ),那么反而不会受限制,这听起来虽然有点奇怪,但这种考量是向后兼容的权衡之举,主要是为了照顾那些没有引入 JDK 9 模块化的旧工程。
最基本的关键字是 exposes
,它表明模块将导出哪些包 ( package
) 供其它模块引用。
module priv.jdk9test.helper {
exposes priv.jdk9test.helper;
}
越界 import
模块内容会被编译器拦截。大意为:在 xxx
模块下定义的ppp
包并没有导出给 yyy
。
Package 'ppp' is declared in module 'xxx', which does not export it to module 'yyy'
同时,越界反射模块内容也会在运行期被拦截,并抛出 java.lang.IllegalAccessException。若想要限制某些包在运行期可被反射获取,但在编译期不可用,则可以用opens
替代 exports
:
// 编译期禁止出现对 priv.jdk9test.unsafe 的 import。
// 但是允许在运行期通过反射获取。
opens priv.jdk9test.unsafe
可以指定将包开放给指定的模块,使用 exports ... to ...
语法,比如:
module priv.jdk9test.helper{
exports priv.jkd9test.helper;
// 导出给多个模块可使用 , 分隔。
exports priv.jdk9test.services to priv.jdk9test.user;
}
而依赖方需要通过 requires
将其它模块纳入声明中,比如:
module priv.jdk9test.user {
requires priv.jdk9test.helper;
requires priv.jdk9test.service;
}
导入声明首先会强制要求指定的 module-jar
已经被加载进依赖路径中,否则编译会不通过,这避免了运行期出现 ClassNotFound
的问题。另一方面,只有主动声明导入( requires )
之后才可以继续在本模块内import
其 module-jar exports
给自身的内容。需要注意,如果模块C requires
的模块 A
事实上并没有向模块 C exports
任何包,那么这条 requires
声明不会报错,但实际上不会起任何作用。
顺带一提,如果用户正在 IntelliJ IDEA 环境下开发多个子项目,且子项目之间相互引用 ( 比如 priv.jdk9test.user 需要使用 priv.jdk9test.helper 编译出的 jar 包 ),可以在 project structures
进行如下设置 ( 重点是黄字部分 ):
在 requires
基础之上还附带 transitive
传递规则。它的作用是:若模块 B requires transitive
另一个模块 A
,现有另一模块 C requires
模块 B
,则它相当于隐式地声明了 requires A
。
// Module A -> module-info.java
module A {
exports a1 to B;
exports a2 to C;
}
// Module B -> module-info.java
module B {
// 传递导入模块 A
requires transitive A;
// 模块 B 声明导出的包,其中 b2 仅向 C 导出。
exports b1;
exports b2 to C;
}
// Module C -> module-info.java
module C {
requires B;
// 由于 B 传递导入了模块 A,因此
// 模块 C 相当于隐式地声明了:
// requires A;
}
在上面的例子当中,即使在模块 C 的规则文件中不主动声明 requires A
,它也能够直接访问模块 A
开放的a2
包。如果模块 B
对模块 A
的导入并不是传递性质的,那么模块 C
就必须主动附加上这条声明。
如果某些依赖模块只在编译时需要,那么可以为其添加static
关键字:
requires static priv.jdk9test.initializer
模块化中另一个特殊的导入导出是 uses
和 provides with
,它们类似于Java
的服务发现机制 SPI,实现了接口和实现类的解耦。服务的提供方需要开放自己的接口,并使用 provides ... with ...
声明此服务接口的具体实现类。
// 开放服务接口
exports priv.jdk9test.services to priv.jdk9test.user;
// 提供服务接口的实现
provides priv.jdk9test.services.MyService with priv.jdk9test.servicesImpl.IPService;
客户端则需要在module-info.java
文件中声明对接口的使用uses
,其具体的实现由 ServiceLoader
负责加载。
// module-info.java
// uses priv.jdk9test.services.MyService
// 通过 ServiceLoader 获取服务接口的实现。
// 这段代码没有声明任何关于 priv.jdk9test.servicesImpl.* 的任何声明。
// 返回的服务实现用 Provider<T> 包装,因此还需要借助 map 提取出来。
List<MyService> services = ServiceLoader.load(
MyService.class
).stream().map(
ServiceLoader.Provider::get
).toList();
// 实际上调用的是 priv.jdk9test.servicesImpl
services.forEach(MyService::getService);
在 JDK 9 当中,JDK 被分为了 94 个 modules
,现在只需加载用户程序依赖的modules
。Java 保留了一个重要的基础模块 java.base
,它仅对外导出而没有任何导入:
module java.base {
exports java.io;
exports java.lang;
exports java.module;
exports java.math;
exports java.nio;
exports java.util;
...
}
所有的模块默认依赖 java.base
模块。
模块分类
JDK 9 中的模块化实际上分为四种:普通模块,开放模块,自动模块,未命名模块
。其中,普通模块遵循上述的细则进行权限控制。
开放模块的声明格式如下:
open module ${module-name} {
// 不适用 opens 规则。
}
开放模块意味着内部所有的声明都是默认可以在运行期被反射获取的。和普通模块相比,开放模块可以声明 exports
,require
s,provide
s & uses
,但是不包括 opens
。
当一个普通 jar 包 ( 通常都是在 JDK 8 及之前编译的 ) 被放在模块搜索路径 --module-path
时,它将被视作一个自动模块,这个模块的名称和版本由 jar 包文件的名称派生。此举是 JDK 9 为向后兼容而设计的机制,自动模块总是读取所有模块,且打开 ( open ) 并导出 ( exports ) 所有包,这也解释了在前文删除 module-info.java
文件之后,所有的模块约束反而都 “消失” 的原因。
由于并不是所有的依赖库的厂商目前都提供了模块化版本,因此,若要将旧项目迁移到 JDK 9 及更新的版本,可以将各种依赖放到 --module-path
下,将它们变为自动模块。
可以将 jar
或者module-jar
放在类搜索路径 --class-path
。这样,当类型加载器在--module-path
找不到类文件时,模块系统便尝试在类路径中寻找匹配的类型。如果成功了,则此类型会被归到 unnamed
模块 ( 未命名模块 ) 下。注意,其它模块无法对这个模块声明 requires
。
JLink
使用 JLink
可以生成一个 Java 程序的运行时镜像,它仅包含精简版 JRE + 项目代码。这意味着:
节省了内存,提高了性能。
允许开发仅提供很小内存的微服务。
更加适合物联网设备。
以下是几个重要的可选参数:
# 模块所在的路径,自动包含 jdk jmods.
--module-path <path>
-p <path>
# 添加的模块,至少要有一个。多个模块可以用 , 分割
# 模块的来源可以是 *.jar,也可以是 *.jmod。
# jlink 在 --module-path 指定的路径下搜索模块。
--add-modules <mod>[,<mod>...]
# 输出文件夹
--output <path>
下面的 jlink
中,在当前目录 . 下寻找模块,并向 ./jdk9Project
输出两个模块。
jlink --module-path . --add-modules priv.jdk9test.utils,priv.jdk9test.helper --output ./jdk9Project
所有的用户程序及其依赖被压缩到lib/moudles
内。进入到输出路径的 /bin
目录下即可通过 java 命令执行这个镜像:
cd bin
# -p : 依赖模块的路径
# -m : 作为程序入口的模块
java -m testForJava/priv.jdk9test.utils.TimePrinter
IntelliJ IDEA 在 Project Structure 中提供了打包 Jlink 的选项。
JMOD
JMOD
文件被设计成可以打包比 jar 更丰富的内容,包括本地代码,配置文件,本地命令和其它类型的数据,所以 JMOD 适合于那些依赖本地环境的模块,比如在 Windows 版本的 JDK 中 java.base.jmods/lib
携带*.dll
文件,而在 Linux 版本下则是 *.so
文件 ( 因此 JMOD 本身不一定是跨平台的 )。
JDK 9 版本之后,jdk 目录下原有的jre
目录被移除,取而代之的是jmods
目录,内部保存着 jdk 被模块化拆分后的各种 *.jmod
文件,以供 JLink 工具提取并构建最小化的运行时镜像。JMOD 文件通过 JDK 提供的 jmod 工具打包,它位于 %JAVA_HOME%\bin
目录下。
打包 JMOD 之前需要将文件归类后存放到不同的路径下,由 jmod 工具转储到 *.jmod
包下的不同目录中去 ( 目录的命名遵从jmod
自身定义的规范,如配置文件保存在 conf
目录下 )。
# 编译的 class 文件路径
--class-path <path>
# 本地命令路径
--cmds <path>
# 用户可编辑的配置文件路径
--config <path>
# 本地链接库路径,.dll,.so 等。
--libs <path>
# 头文件路径
--header-files <path>
# module 路径
--module-path <path>
-p <path>
下面的命令演示了如何将各种文件打包为一个 jmod
。其中--class-path
是必须的:
jmod create --config configs/ --class-path classes/* aJmod.jmod
Maven 提供jmod 插件,见:Apache Maven JMod Plugin – jmod:create
关于 JMOD 的文件格式还是一个开放的 issuse,目前它是基于 .zip 格式的,因此只需简单更改后缀名即可查看*.jmod
的内容。JMOD 仅限在编译或者在链接时使用,比如在 JLink 工具中可以直接将其内部的模块添加到 --add-modules
当中,相关的配置,头文件等也会自动迁移到镜像的对应目录。
鸣谢
- 非常感谢你从头到尾阅读了这篇文章,希望其中的内容对你有所启发和帮助。如果你还有其他问题或需要进一步的了解,欢迎随时关注我的动态并留言
- 最后可以给作者点个关注和小赞赞嘛,谢谢!
- 觉得有收藏价值可以进行收藏