Java13 - 新特性
笔记
2019 年 9 月 17 日,国际知名的 OpenJDK 开源社区发布了 Java 编程语言环境的最新版本 OpenJDK13。
2022-02-08 @Young Kbt
# Java13概述
2019 年 9 月 17 日,国际知名的 OpenJDK 开源社区发布了 Java 编程语言环境的最新版本 OpenJDK13。
根据 Oracle 的统计信息,如上图所示,在所有为社区 JDK 13 有代码贡献的公司中,排名前五的为:Oracle、Red Hat、SAP、Google 和龙芯。龙芯位列全球第 5,全国第 1,为社区贡献了几十个 Patch。
Features:总共有 5 个新的 JEP(JDK Enhancement Proposals):http://openjdk.java.net/projects/jdk/13/
。
各个 build 的更新说明可以查看:https://jdk.java.net/13/release-notes
。
Features
- 350:Dynamic CDS Archives 动态 CDS 档案
- 351:ZGC: Uncommit Unused Memory ZGC 取消使用未使用的内存
- 353:Reimplement the Legacy Socket API 重新实现旧版套接字 API
- 354:Switch Expressions (Preview) Switch 表达式(预览)
- 355:Text Blocks (Preview) 文字块(预览)
# Switch表达式(预览)
在 JDK 12 中引入了 Switch 表达式作为预览特性。JDK 13 提出了第二个 Switch 表达式预览。JEP 354 修改了这个特性,它引入了 yield 语句,用于返回值。这意味着,Switch 表达式(返回值)应该使用 yield,Switch 语句(不返回值)应该使用 break。
在 JDK 12 中有一个,但是要进行一个更改:要从 Switch 表达式中生成一个值 break,要删除 with value
语句以支持 a yield
声明。目的是扩展,Switch 以便它可以用作语句或表达式,因此两个表单既可以使用 case ...
: 带有连贯符号的传统标签,也可以使用新 case … ->
标签,而不需要通过,还有一个新的语句用于从 Switch 表达式中产生值。这些更改将简化编码并为模式匹配做好准备。
在以前,我们想要在 Switch 中返回内容,还是比较麻烦的,一般语法如下:
@Test
public void testSwitch1(){
String x = "3";
int i;
switch (x) {
case "1":
i = 1;
break;
case "2":
i = 2;
break;
default:
i = x.length();
break;
}
System.out.println(i);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在 JDK13 中使用以下语法:
@Test
public void testSwitch2(){
String x = "3";
int i = switch (x) {
case "1" -> 1;
case "2" -> 2;
default -> {
yield 3;
}
};
System.out.println(i);
}
2
3
4
5
6
7
8
9
10
11
12
或者
@Test
public void testSwitch3() {
String x = "3";
int i = switch (x) {
case "1":
yield 1;
case "2":
yield 2;
default:
yield 3;
};
System.out.println(i);
}
2
3
4
5
6
7
8
9
10
11
12
13
在这之后,Switch 中就多了一个关键字用于跳出 Switch 块了,那就是 yield,他用于返回一个值。和 return 的区别在于:return 会直接跳出当前循环或者方法,而 yield 只会跳出当前 Switch 块。
# 文本块(预览)
在 JDK 12 中引入了 Raw String Literals 特性,但在发布之前就放弃了。这个 JEP 与引入多行字符串文字(text block)在意义上是类似的。
这条新特性跟 Kotlin 里的文本块是类似的。
# 现实问题
在 Java 中,通常需要使用 String 类型表达 HTML,XML,SQL 或 JSON 等格式的字符串,在进行字符串赋值时需要进行转义和连接操作,然后才能编译该代码,这种表达方式难以阅读并且难以维护。
文本块就是指多行字符串,例如一段格式化后的 xml、json 等。而有了文本块以后,用户不需要转义,Java 能自动搞定。因此,文本块将提高 Java 程序的可读性和可写性。
# 目标
- 简化跨越多行的字符串,避免对换行等特殊字符进行转义,简化编写 Java 程序
- 增强 Java 程序中字符串的可读性
# 举例
会被自动转义,如有一段以下字符串:
<html>
<body>
<p>Hello, Java13</p>
</body>
</html>
2
3
4
5
将其复制到 Java 的字符串中,会展示成以下内容:
"<html>\n" +
" <body>\n" +
" <p>Hello, Java13</p>\n" +
" </body>\n" +
"</html>\n";
2
3
4
5
上面内容被自动进行了转义,这样的字符串看起来不是很直观,在 JDK 13 中,就可以使用以下语法了:
"""
<html>
<body>
<p>Hello, Java13</p>
</body>
</html>
""";
2
3
4
5
6
7
使用 """
作为文本块的开始符和结束符,在其中就可以放置多行的字符串,不需要进行任何转义。看起来就十分清爽了。
如常见的 SQL 语句:
select employee_id,last_name,salary,department_id
from employees
where department_id in (40,50,60)
order by department_id asc
2
3
4
原有方式:
String query = "select employee_id,last_name,salary,department_id\n" +
"from employees\n" +
"where department_id in (40,50,60)\n" +
"order by department_id asc";
2
3
4
使用新特性:
String newQuery = """
select employee_id,last_name,salary,department_id
from employees
where department_id in (40,50,60)
order by department_id asc
""";
2
3
4
5
6
# 具体使用
内容看不懂,可以看视频讲解:
https://www.bilibili.com/video/BV1jJ411M7kQ?p=27
https://www.bilibili.com/video/BV1jJ411M7kQ?p=28
基本使用
- 文本块是 Java 语言中的一种新文字。它可以用来表示任何字符串,并且提供更大的表现力和更少的复杂性
- 文本块由零个或多个字符组成,由开始和结束分隔符括起来
- 开始分隔符是由三个双引号字符("""),后面可以跟零个或多个空格,最终以行终止符结束。文本块内容以 开始分隔符的行终止符后的第一个字符开始
- 结束分隔符也是由三个双引号字符(""")表示,文本块内容以 结束分隔符的第一个双引号之前的最后一个字符结束
- 文本块中的内容可以直接使用 ",",但不是必需的
- 文本块中的内容可以直接包括行终止符。允许在文本块中使用
\n
,但不是必需的。
例如,文本块:
"""
line1
line2
line3
"""
2
3
4
5
相当于:
"line1\nline2\nline3\n"
或者一个连接的字符串:
"line1\n" +
"line2\n" +
"line3\n"
2
3
如果字符串末尾不需要行终止符,则结束分隔符可以放在最后一行内容上。例如:
"""
line1
line2
line3"""
2
3
4
相当于(line3 少了 \n)
"line1\nline2\nline3"
文本块可以表示空字符串,但不建议这样做,因为它需要两行源代码:
String empty = """
""";
2
以下示例是错误格式的文本块:
String a = """"""; // 开始分隔符后没有行终止符
String b = """ """; // 开始分隔符后没有行终止符
String c = """
"; // 没有结束分隔符
String d = """
abc \ def
"""; // 含有未转义的反斜线(请参阅下面的转义处理)
2
3
4
5
6
7
在运行时,文本块将被实例化为 String 的实例,就像字符串一样。从文本块派生的 String 实例与从字符串派生的实例是无法区分的。具有相同内容的两个文本块将引用相同的 String 实例,就像字符串一样。
编译器在编译时会删除掉这些多余的空格
下面这段代码中,我们用 .
来表示我们代码中的的空格,而这些位置的空格就是多余的。
String html = """
..............<html>
.............. <body>
.............. <p>Hello, world</p>
.............. </body>
..............</html>
..............""";
2
3
4
5
6
7
多余的空格还会出现在每一行的结尾,特别是当你从其他地方复制过来时,更容易出现这种情况,比如下面的代码:
String html = """
..............<html>...
.............. <body>
.............. <p>Hello, world</p>....
.............. </body>.
..............</html>...
..............""";
2
3
4
5
6
7
这些多余的空格对于程序员来说是看不到的,但是他又是实际存在的,所以 如果编译器不做处理,可能会导致程序员看到的两个文本块内容是一样的,但是这两个文本块却因为存在这种多余的空格而导致差异,比如哈希值不相等。
所以编译器在编译时会删除掉这些多余的空格,但是有个细节:
- 如果是字符串后面的空格,则无条件删除
- 如果是字符串前面的空格,那就看结束分隔符的空格数
- 如果结束分隔符没有空格,则不会删除字符串前面的空格
- 如果结束分隔符有 n 个空格,则删除字符串前面的 n 个空格
例如:
// 如果结束分隔符 """ 没有空格,则不会删除字符串前面的空格
String str = """
hello
"""
System.out.println(str.length()); // 6
// 如果结束分隔符 """ 有 n 个空格,则删除字符串前面的 n 个空格
String str1 = """
hello
"""
System.out.println(str1.length()); // 5
2
3
4
5
6
7
8
9
10
11
转义字符
允许开发人员使用 \n
,\f
和 \r
来进行字符串的垂直格式化,使用 \b
和 \t
进行水平格式化。比如下面的代码是合法的(输出的效果会额外被转义字符改变):
String html = """
<html>\n
<body>\n
<p>Hello, world</p>\n
</body>\n
</html>\n
""";
2
3
4
5
6
7
请注意,在文本块内自由使用 "
是合法的。例如(输出带有 ""
的内容):
String story = """
"When I use a word," Humpty Dumpty said,
in rather a scornful tone, "it means just what I
choose it to mean - neither more nor less."
"The question is," said Alice, "whether you
can make words mean so many different things."
"The question is," said Humpty Dumpty,
"which is to be master - that's all."
""";
2
3
4
5
6
7
8
9
但是,三个 "
字符的序列需要进行转义至少一个 "
,以避免模仿结束分隔符:
String code =
"""
String text = \"""
A text block inside a text block
\""";
""";
2
3
4
5
6
文本块连接
可以在任何可以使用字符串的地方使用文本块。例如,文本块和字符串可以相互连接:
String code = "public void print(Object o) {" +
"""
System.out.println(Objects.toString(o));
}
""";
2
3
4
5
但是,涉及文本块的连接可能变得相当笨重。以下面文本块为基础:
String code = """
public void print(Object o) {
System.out.println(Objects.toString(o));
}
""";
2
3
4
5
假设我们想把上面的 Object 改为来自某一变量 type,我们可能会这么写:
String code = """
public void print(""" + type + """
o) {
System.out.println(Objects.toString(o));
}
""";
2
3
4
5
6
可以发现这种写法可读性是非常差的,更简洁的替代方法是使用 String :: replace
或 String :: format
,比如:
String code = """
public void print($type o) {
System.out.println(Objects.toString(o));
}
""".replace("$type", type);
2
3
4
5
或
String code = String.format("""
public void print(%s o) {
System.out.println(Objects.toString(o));
}
""", type);
2
3
4
5
另一个方法是使用 String :: formatted
,这是一个新方法,比如:
String source = """
public void print(%s object) {
System.out.println(Objects.toString(object));
}
""".formatted(type);
2
3
4
5
# 动态CDS档案(动态类数据共享归档)
CDS,是 Java 12 的特性了,可以让不同 Java 进程之间共享一份类元数据,减少内存占用,它还能加快应用的启动速度。而 JDK13 的这个特性支持在 Java Application 执行之后进行动态 archive。存档类将包括默认的基础层 CDS 存档中不存在的所有已加载的应用程序和库类。也就是说,在 Java 13 中再使用 AppCDS 的时候,就不再需要这么复杂了。
该提案处于目标阶段,旨在提高 AppCDS 的可用性,并消除用户进行试运行以创建每个应用程序的类列表的需要。
使用示例:
# JVM 退出时动态创建共享归档文件:导出 jsa
java -XX:ArchiveClassesAtExit=hello.jsa -cp hello.jar Hello
# 用动态创建的共享归档文件运行应用:使用 jsa
java -XX:SharedArchiveFile=hello.jsa -cp hello.jar Hello
2
3
4
5
# ZGC:取消使用未使用的内存
# G1和Shenandoah
JVM 的 GC 释放的内存会还给操作系统吗?
GC 后的内存如何处置,其实是取决于不同的垃圾回收器。因为把内存还给 OS,意味着要调整 JVM 的堆大小,这个过程是比较耗费资源的。
- Java12 的 346: Promptly Return Unused Committed Memory from G1 (opens new window) 新增了两个参数分别是
G1PeriodicGCInterval
及G1PeriodicGCSystemLoadThreshold
用于 GC 之后重新调整 Java heap size,然后将多余的内存归还给操作系统 - Java12 的 189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental) (opens new window) 拥有参数
-XX:ShenandoahUncommitDelay = milliseconds
来指定 ZPage 的 page cache 的失效时间,然后归还内存
HotSpot 的 G1 和 Shenandoah 这两个 GC 已经提供了这种能力,并且对某些用户来说,非常有用。因此,Java13 则给 ZGC 新增归还 unused heap memory 给操作系统的特性。
# ZGC的使用背景
在 JDK 11 中,Java 引入了 ZGC,这是一款可伸缩的低延迟垃圾收集器,但是当时只是实验性的。号称不管你开了多大的堆内存,它都能保证在 10 毫秒内释放 JVM ,不让它停顿在那。但是,当时的设计是它不能把内存归还给操作系统。对于比较关心内存占用的应用来说,肯定希望进程不要占用过多的内存空间了,所以这次增加了这个特性。
在 Java 13 中,JEP 351 再次对 ZGC 做了增强,将没有使用的堆内存归还给操作系统。ZGC 当前不能把内存归还给操作系统,即使是那些很久都没有使用的内存,也只进不出。这种行为并不是对任何应用和环境都是友好的,尤其是那些内存占用敏感的服务,例如:
- 按需付费使用的容器环境
- 应用程序可能长时间闲置,并且和很多其他应用共享和竞争资源的环境
- 应用程序在执行期间有非常不同的堆空间需求,例如,可能在启动的时候所需的堆比稳定运行的时候需要更多的堆内存
# 使用细节
ZGC 的堆由若干个 Region 组成,每个 Region 被称之为 ZPage。每个 Zpage 与数量可变的已提交内存相关联。当 ZGC 压缩堆的时候,ZPage 就会释放,然后进入 Page Cache,即 ZPageCache。这些在 Page Cache 中的 ZPage 集合就表示没有使用部分的堆,这部分内存应该被归还给操作系统。回收内存可以简单的通过从 Page Cache 中逐出若干个选好的 ZPage 来实现,由于 Page Cache 是以 LRU(Least recently used,最近最少使用)顺序保存 ZPage 的,并且按照尺寸(小,中,大)进行隔离,因此逐出 ZPage 机制和回收内存相对简单了很多,主要挑战是设计关于何时从 Page Cache 中逐出 ZPage 的策略。
一个简单的策略就是设定一个超时或者延迟值,表示 ZPage 被驱逐前,能在 Page Cache 中驻留多长时间。这个超时时间会有一个合理的默认值,也可以通过 JVM 参数覆盖它。Shenandoah GC 用了一个类型的策略,默认超时时间是 5 分钟,可以通过参数 -XX:ShenandoahUncommitDelay = milliseconds
覆盖默认值。
像上面这样的策略可能会运作得相当好。但是,用户还可以设想更复杂的策略:不需要添加任何新的命令行选项。例如,基于 GC 频率或某些其他数据找到合适超时值的启发式算法。JDK13 将使用哪种具体策略目前尚未确定。可能最初只提供一个简单的超时策略,使用 -XX:ZUncommitDelay = seconds
选项,以后的版本会添加更复杂、更智能的策略(如果可以的话)。
uncommit 能力默认是开启的,但是无论指定何种策略,ZGC 都不能把堆内存降到低于 Xms。这就意味着,如果 Xmx 和 Xms 相等的话,这个能力就失效了。-XX:-ZUncommit
这个参数也能让这个内存管理能力失效。
# 重新实现旧版套接字API
# 现有问题
重新实现了古老的 Socket 接口。现在已有的 java.net.Socket
和 java.net.ServerSocket
以及它们的实现类,都可以回溯到 JDK 1.0 时代了。
- 它们的实现是混合了 Java 和 C 的代码的,维护和调试都很痛苦
- 实现类还使用了线程栈作为 I/O 的缓冲,导致在某些情况下还需要增加线程栈的大小
- 支持异步关闭,此操作是通过使用一个本地的数据结构来实现的,这种方式这些年也带来了潜在的不稳定性和跨平台移植问题。该实现还存在几个并发问题,需要彻底解决
在未来的网络世界,要快速响应,不能阻塞本地方法线程,当前的实现不适合使用了。
# 新的实现类
全新实现的 NioSocketImpl 来替换 JDK1.0 的 PlainSocketImpl。
- 它便于维护和调试,与 NewI/O(NIO)使用相同的 JDK 内部结构,因此不需要使用系统本地代码
- 它与现有的缓冲区缓存机制集成在一起,这样就不需要为 I/O 使用线程栈
- 它使用
java.util.concurrent
锁,而不是synchronized
同步方法,增强了并发能力 - 新的实现是 Java 13 中的默认实现,但是旧的实现还没有删除,可以通过设置系统属性
jdk.net.usePlainSocketImpl
来切换到旧版本
# 代码说明
运行一个实例化 Socket 和 ServerSocket 的类将显示这个调试输出。这是默认的(新的)。
Module java.base
Package java.net
Class SocketImpl
public abstract class SocketImpl implements SocketOptions {
private static final boolean USE_PLAINSOCKETIMPL = usePlainSocketImpl();
private static boolean usePlainSocketImpl() {
PrivilegedAction<String> pa = () ->
NetProperties.get("jdk.net.usePlainSocketImpl");
String s = AccessController.doPrivileged(pa);
return (s != null) && !s.equalsIgnoreCase("false");
}
/**
* Creates an instance of platform's SocketImpl
*/
@SuppressWarnings("unchecked")
static <S extends SocketImpl & PlatformSocketImpl> S createPlatformSocketImpl(boolean server) {
if (USE_PLAINSOCKETIMPL) {
return (S) new PlainSocketImpl(server);
} else {
return (S) new NioSocketImpl(server);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SocketImpl
的 USE_PLAINSOCKETIMPL 取决于 usePlainSocketImpl
方法,而它会从 NetProperties 读取 dk.net.usePlainSocketImpl
配置,如果不为 null 且不为 false,则 usePlainSocketImpl
方法返回 true;createPlatformSocketImpl
会根据 USE_PLAINSOCKETIMPL
来创建 PlainSocketImpl
或者 NioSocketImpl
。
# 其他解读
上面列出的是大方面的特性,除此之外还有一些 API 的更新及废弃,主要见 JDK 13 Release Notes:https://jdk.java.net/13/release-notes
这里举几个例子。
# 增加项
- 添加
FileSystems.newFileSystem(Path, Map) Method
- 新的
java.nio.ByteBuffer
:Bulk get/put Methods Transfer Bytes Without Regard to Buffer Position - 支持 Unicode 12.1
- 添加
-XX:SoftMaxHeapSize Flag
,目前仅仅对 ZGC 起作用 - ZGC 的最大 heap 大小增大到 16TB
# 移除项
- 移除
awt.toolkit System Property
- 移除
Runtime Trace Methods
- 移除
-XX:+AggressiveOpts
- 移除 Two Comodo Root CA Certificates、Two DocuSign Root CA Certificates
- 移除内部的
com.sun.net.ssl
包
# 废弃项
- 废弃
-Xverify:none
及-noverify
- 废弃
rmic Tool
,并准备移除 - 废弃
javax.security.cert
,并准备移除
# 已知问题
- 不再支持 Windows 2019 Core Server
- 使用 ZIP File System (zipfs) Provider 来更新包含 Uncompressed Entries 的 ZIP 或 JAR 可能造成文件损坏
# 其他事项
GraphicsEnvironment.getCenterPoint()
及getMaximumWindowBounds()
已跨平台统一- 增强了 JAR Manifest 的 Class-Path 属性处理
- 针对
Negatively Sized Argument
,StringBuffer(CharSequence)
及StringBuilder(CharSequence)
会抛出NegativeArraySizeException
- Linux 的默认进程启动机制已经使用 posix_spawn
Lookup.unreflectSetter(Field)
针对static final fields
会抛出IllegalAccessException
- 使用了
java.net.Socket.setSocketImplFactory
及java.net.ServerSocket.setSocketFactory
方法的要注意,要求客户端及服务端要一致,不能一端使用自定义的 factory 一端使用默认的 factory - SocketImpl 的
supportedOptions
、getOption
及setOption
方法的默认实现发生了变化,默认的supportedOptions
返回空,而默认的getOption
及setOption
方法抛出UnsupportedOperationException
- JNI NewDirectByteBuffer 创建的 Direct Buffer 为
java.nio.ByteOrder.BIG_ENDIAN
- Base64.Encoder 及 Base64.Decoder 可能抛出
OutOfMemoryError
- 改进了 Serial GC 的 Young pause time report
- 改进了
MaxRAM
及UseCompressedOops
参数的行为
# 小结
以上,就是 JDK13 中包含的主要特性。
- 语法层面,改进了
Switch Expressions
,新增了Text Blocks
,二者皆处于Preview
状态 - API 层面主要使用 NioSocketImpl 来替换 JDK1.0 的 PlainSocketImpl
- GC 层面则改进了 ZGC,以支持 Uncommit Unused Memory
而且,JDK13 并不是 LTS(长期支持)版本,如果你正在使用 Java 8(LTS)或者 Java 11(LTS),暂时可以不必升级到 Java 13。
这些年很多 Java 粉丝已经讨厌写那些冗长难看的代码了,有些转投到高效的 Python 门下,有的转用 Kotlin,有的去了新兴的 Go 那边。不过,Java 大叔凭着其广阔的领土,活跃高效的运转机构,以及其开放改革的心,还会有不少的粉丝追随他的。
# 采用新版本的注意事项
在采用新版本 Java 之前必须考虑的一些注意事项/风险。
# 注意1:被新版本系列绑架
如果采用了 Java 12 并使用新的语言特性或新的 API,这意味着实际上你已将项目绑定到 Java 的新版本系列。接下来你必须采用 Java 13、14、15、16 和 17。
使用了新版本,每个版本的使用寿命为六个月,并且在发布后仅七个月就过时了。这是因为每个版本只有在六个月内提供安全补丁,发布后 1 个月的第一个补丁和发布后 4 个月的第二个补丁。7 个月后,下一组安全补丁会发布,但旧版本不能获取更新。
因此,你要判断自身的开发流程是否允许升级 Java 版本,一个月的时间窗口方面会不会太狭窄?或者你是否愿意在安全基线以下的 Java 版本上运行?
# 注意2:升级的绊脚石
实际使用中有很多阻止我们升级 Java 的因素,下面列出一些常见的:
开发资源不足:你的团队可能会非常忙碌或规模太小,你能保证两年后从 Java 15 升级到 16 的开发时间吗
构建工具和 IDEA:使用的 IDEA 是否会在发布当天支持每个新版本?Maven? Gradle 呢? 如果不是,你有后备计划吗?请记住,你只有 1 个月的时间来完成升级、测试并将其发布到生产环境中。此外还包括 Checkstyle, JaCoCo,PMD,SpotBugs 等等其他工具
依赖关系:你的依赖关系是否都准备好用于每个新版本?请记住,它不仅仅是直接依赖项,而是技术堆栈中的所有内容。字节码操作库尤其受到影响,例如 ByteBuddy 和 ASM
框架:这是另一种依赖,但是一个大而重要的依赖。在一个月的狭窄时间窗口内,Spring 会每六个月发布一个新版本吗?Jakarta EE(以前的 Java EE)会吗?如果它们不这样做会怎么样
现在,任何阻挡者的传统方法都是等待:在开始升级之前等待 6 到 12 个月,以便为工具,库和框架提供修复任何错误的机会。
# 注意3:云/托管/部署
你是否可以控制代码在生产环境中的运行位置和方式?例如,如果你在 AWS(Amazon Web Service) Lambda 中运行代码,则无法控制。AWS Lambda 没有采用 Java 9 或 10,甚至没有采用 Java 11。所以除非 AWS 提供公共保证以支持每个新的 Java 版本,否则根本无法采用 Java 12。(我的工作假设是 AWS Lambda 仅支持主要的 LTS 版本,由 Amazon Corretto JDK 公告支持 (opens new window)。)
如何托管你的 CI 系统?Jenkins、Travis、Circle、Shippable、GitLab 会快速更新吗?如果不是,你会怎么做?
# 注意4:为采用新版本进行规划
如果正在考虑采用新版本的 Java,建议你准备一份现在所依赖的所有内容的清单,或者可能在未来 3 年内会依赖的。你需要保证该列表中的所有内容都能正常工作,并与新版本一起升级,或者如果该依赖项不再更新,请制定好计划。以某位互联网开发者为例,他列的清单如下:
- Amazon AWS
- Eclipse
- IntelliJ
- Travis CI
- Shippable CI
- Maven
- Maven 插件(compile、jar、source、javadoc 等)
- Checkstyle,以及相关的 IDE 插件和 Maven 插件
- JaCoCo,以及相关的 IDE 插件和 Maven 插件
- PMD 和相关的 Maven 插件
- SpotBugs 和相关的 Maven 插件
- OSGi bundle metadata tool
- Bytecode 工具(Byte buddy / ASM etc)
- 超过 100 个 jar 包依赖项
说了这么多,当然不是鼓励大家不进行升级,新语言特性带来的好处以及性能增强会让开发者受益,但升级背后的风险也应该考虑进去。
# 注意5:其他第三方厂商的声明
Spring 框架已经在视频中表达了对 Java 12 的策略。关键部分是(Spring 官方声明):
- Java 8 和 11 作为 LTS 版本会持续获得我们的正式支持,对于过渡版本,我们也会尽最大努力支持,但它们不会获得正式的生产环境支持。如果你升级到 Java 11,我们非常愿意和你合作,因为长期支持版本才是我们关注的重心,对于 Java 12 及更高版本我们会尽最大的努力
作为典型软件供应商的一个例子,Liferay 声明如下:
- Liferay 已决定不会对 JDK 的每个主要版本进行认证。我们将选择遵循 Oracle 的主导并仅认证标记为 LTS 的版本
个人的想法:
- 想象一下汽车制造商的类似行为:
- 每 6 个月重新设计和发布一次汽车
- 从 2018 年开始,每三年只提供一次完整的保修
- 如果客户购买 2019 型号并且在 6 个月内出现问题,他们必须等待并购买 2020 修复模型
- 2020 型号是电动的,但你的城镇的基础设施还不支持电充等设备支持,更不用说座椅已经改变并导致腰痛
- 不用担心,购买 2020.3 型号
# 最后
# 再谈JDK版本更新
在 Java 9 之前,当一个版本被宣布为首选版本,存在一个「培育」(bedded-in)新 GA 版本的重叠期。在此期间,上一个版本将会继续进行免费更新。为确保新旧版本间的干净切换,即便旧版本已不再是首选版本,通常也会继续维护 12 个月以上。但是 随着 Java 版本发布更改为遵循严格的时间表后,事实上宣告了传统的免费支持期将寿终正寝。
也许不会有太多公司直接选择 JDK12、JDK 13,但个别的生产实践并不遥远。比如,实际工作场景中,利用 JDK 12 的 Abortable Mixed Collections for G1,解决了 HDFS 在特定场景中 G1 Evacuation 时间过长的困扰,虽然最后团队选择将其 backport 到了自己的 JDK11 版本中,但如果没有快速交付的预览版 JDK12,也不会如此快速的得到结论。
而对另一个问题,目前看是非常成功的,解开了 Java/JVM 演进的许多枷锁,至关重要的是,OpenJDK 的权力中心,正在转移到开发社区和开发者手中。在新的模式中,既可以利用 LTS 满足企业长期可靠支持的需求,也可以满足各种开发者对于新特性迭代的诉求。你可能注意到了 Switch Expressions
被打上了预览(Preview)的标签, Shenandoah GC 则是实验(Experimental)特性,这些都是以往的发布周期下不大现实的,因为用 2-3 年的最小间隔粒度来实验一个特性,基本是不现实的。
可以预计,JDK8 在未来的一段时间仍将是主流,我们已经注意到 Amazon、Alibaba、Redhat、AdoptOpenJDK 等等厂商或社区,纷纷发布了自己的 JDK8 等产品,开始竞赛长期支持版本 JDK 的主导权,这是非常好的迹象,反映了主流厂商对于 Java 的投资力度增大。
是否会带来 Java/JVM 的碎片化呢?多少会发生一些,但从目前的合作模式来看,OpenJDK 仍然是合作的中心,主导这 Java 历史版本维护和未来的演进路线。
# Oracle撒手,OpenJDK继续向前
Java 8 是目前使用率最高的一个 Java 版本,发布于 2014 年,而 Oracle 对 Java 8 的官方支持时间持续到 2020 年 12 月,之后将不再为个人桌面用户提供 Oracle JDK 8 的修复更新;在 2019 年 1 月之后,不再提供免费的商业版本更新,届时想要继续获得 Oracle 的商业支持和维护,需付费订阅。
近日,Oracle 的销售代表发出的一封邮件引起了热议,该邮件称「Java 8 的非公开可用的关键补丁更新」将于 2019 年 4 月 16 日发布,拥有有效许可证的客户才可以享用。邮件继续称,如果没有安装这些更新,可能导致「你的服务器和桌面环境暴露且易受攻击」。
但在许多 Java 用户看来,这封邮件像是一种敲诈勒索或恐吓策略。
虽然 Oracle 官方选择了不再支持,但 Java 社区却把担子接了下来。红帽 Java 平台团队的首席工程师 Andrew Haley 曾表示,红帽计划在 2023 年之前继续提供对 OpenJDK 8 的支持:
- 在我看来,这算比较正常的。几年前,OpenJDK 6 更新(JDK6u)项目被 Oracle 放弃,我接管了它,然后 OpenJDK 7 也发生了同样的事情。最后,Azul 的 Andrew Brygin 接管了 OpenJDK 6。由来自多个组织成员组成的 OpenJDK Vulnerability Group 就重要的安全问题进行协作。在广大的 OpenJDK 社区和我的团队(Red Hat)的帮助下,我们定期为关键 Bug 和安全漏洞提供更新。我觉得这样的过程同样适用于 OpenJDK 8 和下一个长期支持版本,即 OpenJDK 11。如果可以得到社区的支持,我很高兴能够领导 JDK 8 更新项目和 JDK 11 更新项目
除了红帽以外,AWS 推出了 OpenJDK 长期支持版本 Amazon Corretto。阿里巴巴也开源了 OpenJDK 长期支持版本 Alibaba Dragonwell。
# 未来展望
Java 的变化速度从未如此之快——如今,该语言的新版本每六个月就会发布一次。
而之所以能够实现如此重大的转变,自然离不开一系列专注于 提高其性能 与 添加新功能 的协作性项目的贡献。这些项目的目标可谓雄心勃勃。正如 JetBrains 开发者布道师 Trisha Gee 在 QCon 伦敦 2019 大会上所言:Java 即将迎来很多超酷的东西。
而发展道路中的以下三大主要项目,将有助于确定 Java 的未来方向。
项目一:Loom 项目
尝试改进 Java 语言的并发处理方式,或者说是在对计算机在不同指令集执行之间切换能力的探索。
甲骨文公司 Loom 项目技术负责人 Ron Pressler 在 QCon 伦敦 2019 大会上向希望编写软件以处理并发任务的 Java 开发者们提出了两种都不够完美的选项:要么编写无法通过扩展处理大量并发任务的「简单同步阻塞代码」,要么编写可扩展但编写难度极高且调试过程复杂的异步代码。
为了寻求解决这个问题的方法,Loom 项目引入了一种将任务拆分为线程的新方法——所谓线程,即是指计算机在运行指令时的最小可能执行单元。在这方面,Loom 引入了被称为 fibers 的新型轻量级用户线程。
他在大会上指出:利用 fibers,如果我们确保其轻量化程度高于内核提供的线程,那么问题就得到了解决。大家将能够尽可能多地使用这些用户模式下的轻量级线程,且基本不会出现任何阻塞问题。
利用这些新的 fibers,用户将能够扩展 Java 虚拟机(JVM)以支持定界延续(delimited continuations)机制,从而使得指令集的执行实现暂停以及恢复。对这些延续进行暂停与恢复的任务将由 Java 中的 ForkJoinPool 调度程序以异步模式处理。
根据说明文档所言,fibers 将使用与 Java 现有 Thread 类非常相似的 API,这意味着 Java 开发人员的学习曲线应该不会太过陡峭。
项目二:Amber 项目
Amber 项目的目标,在于 支持「更小、面向生产力的 Java 语言功能」的开发,从而加快将新功能添加至 Java 语言中的速度。
这套方案非常适合自 Java 9 以来,以更快速度持续发布的各 Java 新版本。
目前,以下 JDK 增强提案(简称 JEP)正在进行当中,并隶属于 Amber 项目之内。
生字符串
生字符串使得开发人员能够更轻松地对文本进行适当格式化,且无需引入由转义字符带来的复杂性。
举例来说,开发人员不必使用转义字符来表示换行符,因此在以下字符串中:
Hello World
可以直接编写为:
`Hello World`
而非原本的:
"Hello World"
该提案的说明文档中提到,这一变更将使得各类文本客串的输入变得更加简单,包括文件路径以及 SQL 语句等等。
如大家所见,生字符串应被包含在反引号之内。
用于 JDK API 的 Java 编译器 Intrinsic:https://openjdk.java.net/jeps/348
。此项提案将允许开发人员对需要定期调用的重要代码段进行性能优化。
Pattern Matching
官网介绍地址:https://openjdk.java.net/jeps/305
。
Pattern Matching 能够简化利用 Java 中 instanceof 运算符检查对象是否属于特定类的过程,而后提取该对象的组件以进行进一步处理。
如此一来,以下操作语法:
if (obj instanceof Integer) { int intValue = ((Integer) obj).intValue(); // use
intValue }
2
将可被简化为:
if (x instanceof Integer i) { // can use i here, of type Integer }
Switch Expressions
官网介绍地址:https://openjdk.java.net/jeps/325
。
Switch expressions 已经在 Java 12 当中以预览版形式推出,允许开发人员利用更简单的语法通过 Switch 语句为输入内容指定不同的响应方式。
举例来说,现在我们不再需要始终在以下语法当中使用 Switch 语句:
switch (port) { case 20: type = PortType.FTP; break; }
而可以采取以下更为简洁的表达方式:
Switch (port) { case 20 -> PortType.FTP; }
项目三:Valhalla 项目
Valhalla 项目专注于支持高级 JVM 与语言功能的开发。
目前 Valhalla 项目的候选提案还比较有限,具体包括:
Value Types
官网介绍地址:https://openjdk.java.net/jeps/169
。
此项提案旨在允许 JVM 处理一种新的类型,即 Value Types。
这些新的不可变类型将拥有与 int 等基元类似的内存效率,但同时又与普通类一样能够保存一整套基元集合。提案说明文档中指出,其目标在于「为 JVM 基础设施提供处理不可变与无引用对象的能力,从而实现使用非基元类型进行高效按值计算的目标。」
Generic Specialization
官网介绍地址:https://openjdk.java.net/jeps/218
。
此项提案 扩展了适用于泛型的具体类型,其中包括基元以及即将推出的 Value Types。