作者:Falk Sippach
语言必须发展,否则它们就有变得无关紧要的风险,” Brian Goetz (甲骨文公司)在2019年11月在 Devoxx 举行的“ Java 语言期货”演讲中说。作为 Java 语言架构师,他扮演了一个重要的角色,尽管 Java 已经发展了25年,但仍然远远没有过时。在本文中,我们将研究 JDK 14的创新
近年来,甲骨文做出了一些突破性的决定。他们包括新的半年发布模式与预览功能和更短的发布和反馈周期的新功能。许可模式也发生了变化,Oracle JDK 不再免费提供。这加剧了竞争,因此您现在可以从包括 Oracle 在内的各种供应商获得免费的 OpenJDK 发行版。自从 java11以来,它已经与 oraclejdk 实现了二进制兼容,并且采用开源许可证。
一年半以前,最后一个 LTS 版本 Java 11于2019年秋季发布。从那时起,随后的两个主要版本只有有限数量的新特性。JDK 孵化器项目(Amber, Valhalla, Loom …) ,然而,正在致力于许多新想法,所以不足为奇的是,刚刚发布的 JDK 14的功能范围再次显著扩大。即使只有少数人会在生产环境中使用新版本,您仍然应该关注新特性,并尽早就预览功能提供反馈。这是确保新特性投入生产直到下一个 LTS 版本最终完成的唯一方法,该版本将在2021年秋季以 Java 17的形式发布。
下列Java增强建议(JEP)已经实现。在本文中,我们将从开发人员的角度来仔细研究感兴趣的主题。
JEP 305: Pattern Matching for instanceof (Preview)
JEP 343: Packaging Tool (Incubator)
JEP 345: NUMA-Aware Memory Allocation for G1
JEP 349: JFR Event Streaming
JEP 352: Non-Volatile Mapped Byte Buffers
JEP 358: Helpful NullPointerExceptions
JEP 359: Records (Preview)
JEP 361: Switch Expressions (Standard)
JEP 362: Deprecate the Solaris and SPARC Ports
JEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
JEP 364: ZGC on macOS
JEP 365: ZGC on Windows
JEP 366: Deprecate the ParallelScavenge + SerialOld GC Combination
JEP 367: Remove the Pack200 Tools and API
JEP 368: Text Blocks (Second Preview)
JEP 370: Foreign-Memory Access API (Incubator)
JEP 305: Pattern matching for instanceof
自20世纪60年代以来,模式匹配语言的概念已经在各种编程语言中得到了应用。其中最现代的例子是 Haskell 和 Scala。模式是一个谓词的组合,该谓词匹配目标结构和该模式中的一组变量。如果这些变量匹配,则为它们分配相应的内容。其目的是破坏对象,也就是将它们分解为它们的组件。
到目前为止,Java 只能区分 switch 语句中的数据类型 integer、 string 和 enum。然而,随着 Java 12中开关表达式的引入,迈向模式匹配的第一步已经迈出。使用 Java 14,我们现在可以额外地使用模式匹配操作符 instanceof。避免了不必要的强制转换,减少的冗余提高了可读性。
例如,在此之前,必须按照以下步骤检查空字符串或空集合:
boolean isNullOrEmpty( Object o ) {
return == null ||
instanceof String && ((String) o).isBlank() ||
instanceof Collection && ((Collection) o).isEmpty();
}
现在您可以在使用 instanceof 检查时直接将值赋给变量,并对其执行进一步调用:
boolean isNullOrEmpty( Object o ) {
return o == null ||
o instanceof String s && s.isBlank() ||
o instanceof Collection c && c.isEmpty();
}
这种区别似乎微不足道,然而,Java 开发人员中的纯粹主义者节省了一个小而烦人的冗余。
开关表达式最早是在 Java 12和13中引入的,在这两种情况下都是作为一个预览特性。它们现已在 jep361中最后确定。这为开发人员提供了两种新的语法变体,它们具有更短、更清晰和更不容易出错的语义。表达式的结果可以分配给变量,或者作为方法的值返回(清单1)。
JEP 358: Helpful NullPointerExceptions
对空引用的无意访问也是 Java 开发人员所担心的。根据托尼•霍尔爵士(Sir Tony Hoare)自己的说法,他发明的零Y引用是一个错误,其后果高达数十亿美元。这仅仅是因为在20世纪60年代阿尔戈语的发展过程中,它是如此容易实现。
在 Java 中,编译器和运行时环境都不支持处理零引用。这些恼人的异常可以通过各种变通方法来避免。最简单的方法是将检查设置为零。不幸的是,这个过程非常繁琐,当我们需要它的时候我们往往会忘记它。
使用自 JDK 8以来包含的包装器类 Optional,您可以通过 API 显式地告诉调用者,一个值可以为零,并且它必须对此进行响应。因此,您不能再意外地遇到空引用,而必须显式地处理可能为空的值。这个过程对于公共接口的返回值非常有用,但是也会消耗额外的间接层,因为您总是需要解压实际值。
在其他语言中,辅助工具早已构建到语法和编译器中,比如Groovy中 NullObjectPattern 和 Safe Navigation 操作符(some?.method())。在 Kotlin,可以明确区分可能不为空的类型和可能作为引用为 null 的类型。我们将来也必须使用 Java 中的 nullpointerexception。
但是,作为预览特性引入的有用的NullPointerExceptions可以简化异常的故障排除。为了在抛出 NullPointerException 时插入必要的信息,必须在启动时激活选项 -XX: + ShowCodeDetailsInExceptionMessages。如果调用链中的一个值为零,那么您将收到一条有用的消息:
man.partner().name()
Result: java.lang.NullPointerException: Cannot invoke "Person.name()" because the return value of "Person.partner()" is null
Lambda表达式需要特殊处理。例如,如果lambda函数的参数为零,则默认情况下将收到清单2所示的错误消息。要显示正确的参数名称,必须使用-g:vars选项编译源代码。结果如下:
java.lang.NullPointerException: Cannot invoke "Person.name()" because "p" is null
Listing 2 清单2
Stream.of( man, woman )
.map( p -> p.partner() )
.map( p -> p.name() )
.collect( Collectors.toUnmodifiableList() );
Result: java.lang.NullPointerException: Cannot invoke "Person.name()" because "<parameter1>" is null
不幸的是,当一个空参数时,目前没有方法引用的指示:
Stream.of( man, woman )
.map( Person::partner )
.map( Person::name )
.collect( Collectors.toUnmodifiableList() )
Result: java.lang.NullPointerException
但是,如本例所示,如果将每个流方法调用放在新行中,那么麻烦的代码行可以很快地缩小范围。NullPointerExceptions在自动装箱/拆箱中也具有挑战性。如果在这里也激活了编译器参数-g:vars,您还将收到新的有用的错误消息(清单3)。
Listing 3 清单3
int calculate() {
Integer a = 2, b = 4, x = null;
return a + b * x;
}
calculate();
Result: java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "x" is null
JEP 359: Records
也许最令人兴奋的同时也是最令人惊讶的创新是记录类型的引入 。它们是在Java 14发行中相对较晚实现的,是一种类声明的限制形式,类似于枚举。
记录是在Valhalla项目中开发的。与Kotlin中的Data Classes和Scala中的Case Classes有某些相似之处。紧凑的语法可能会使Lombok之类的库在将来过时。
Kevlin Henney还看到了以下优点:“我认为Java记录功能的有趣的副作用之一是,实际上,它将帮助揭示多少Java代码实际上是面向 getter / setter而非面向对象的。” 一个人有两个字段的简单定义可以在这里看到 :
public record Person( String name, Person partner ) {}
一个带有附加构造函数的扩展变量,因此只有字段name 是强制的,也可以实现:
public record Person( String name, Person partner ) {
public Person( String name ) { this( name, null ); }
public String getNameInUppercase() { return name.toUpperCase(); }
}
编译器生成一个不可变类,除了这两个属性和它自己的方法之外,它还包含访问器的实现(没有 getter!) 、构造函数 equals / hashcode 和 toString (清单4)。
Listing 4 清单4
public final class Person extends Record {
private final String name;
private final Person partner;
public Person(String name) { this(name, null); }
public Person(String name, Person partner) { this.name = name; this.partner = partner; }
public String getNameInUppercase() { return name.toUpperCase(); }
public String toString() { /* ... */ }
public final int hashCode() { /* ... */ }
public final boolean equals(Object o) { /* ... */ }
public String name() { return name; }
public Person partner() { return partner; }
}
使用的行为符合预期,您无法从调用方判断记录类型是实例化的(清单5)。
Listing 5 清单5
var man = new Person("Adam");
var woman = new Person("Eve", man);
woman.toString(); // ==> "Person[name=Eve, partner=Person[name=Adam, partner=null]]"
woman.partner().name(); // ==> "Adam"
woman.getNameInUppercase(); // ==> "EVE"
// Deep equals
new Person("Eve", new Person("Adam")).equals( woman ); // ==> true
顺便说一下,records 不是经典的 javabean,因为它们不包含真正的 getter。但是,您可以使用相同名称的方法访问成员变量。记录也可以包含注释或 Javadocs。
此外,还可以在主体中声明静态字段、方法、构造函数或实例方法。不允许在记录头之外定义其他实例字段。
JEP 368: Text Blocks
最初计划作为 java12的原始字符串,java13引入了一个更轻量级的版本,称为文本块的多行字符串的形式。特别是对于 HTML 模板和 SQL 脚本,它们极大地提高了可读性(清单6)。
Listing 6 清单6
// 未使用 Text Blocks
String html = "<html>\n" +
" <body>\n" +
"
Hello, Escapes
\n" +
" </body>\n" +
"</html>\n";
// With Text Blocks
String html = """
<html>
<body>
Hello, Text Blocks
</body>
</html>""";Listing 6 清单6
// Without Text Blocks
String html = "<html>\n" +
" <body>\n" +
"
Hello, Escapes
\n" +
" </body>\n" +
"</html>\n";
// 使用 Text Blocks
String html = """
<html>
<body>
Hello, Text Blocks
</body>
</html>""";
还添加了两个新的转义序列,您可以使用它们调整文本块的格式。例如,如果要使用不应在输出中显式出现的换行符,则只需在行尾插入\(反斜杠)即可。这为您提供了一个带有长行的字符串,但是为了清楚起见,您可以在源代码中使用换行符(清单7)。
Listing 7 清单7
String text = """
Lorem ipsum dolor sit amet, consectetur adipiscing \
elit, sed do eiusmod tempor incididunt ut labore \
et dolore magna aliqua.\
""";
// 代替+
String literal = "Lorem ipsum dolor sit amet, consectetur adipiscing " +
"elit, sed do eiusmod tempor incididunt ut labore " +
"et dolore magna aliqua.";
新的转义序列\ s被转换为空格。例如,这可以用于确保行尾的空白不会被自动截断(修剪),并获得每行固定的字符宽度:
String colors = """
red \s
green\s
blue \s
""";
还有什么新鲜事吗?
除了所描述的功能(对于开发人员来说主要是有趣的)之外,还有其他一些更改。在JEP 352中,对FileChannel API进行了扩展,以允许创建MappedByteBuffer实例。与易失性存储器(RAM)不同,它们在非易失性数据存储(NVM,非易失性存储器)上工作。
但是,目标平台是Linux x64。关于垃圾收集也发生了很多事情。并发标记扫描(CMS)垃圾收集器已被删除。因此,ZGC现在也可用于macOS和Windows。
对于关键的Java应用程序,建议在生产中激活飞行记录功能。以下命令使用Flight Recording启动Java应用程序,并将信息写入record.jfr,并始终保留一天的数据:
java \
-XX:+FlightRecorder \
-XX:StartFlightRecording=disk=true, \
filename=recording.jfr,dumponexit=true,maxage=1d \
-jar application.jar
通常,您随后可以使用工具JDK Mission Control(JMC)读取和分析数据。JDK 14的另一个新功能是,您还可以从应用程序异步查询事件(清单8)。
Listing 8 清单8
import jdk.jfr.consumer.RecordingStream;
import java.time.Duration;
try ( var rs = new RecordingStream() ) {
rs.enable( "jdk.CPULoad" ).withPeriod( Duration.ofSeconds( 1 ) );
rs.onEvent( "jdk.CPULoad", event -> {
System.out.printf( "%.1f %% %n", event.getFloat( "machineTotal" ) * 100 );
});
rs.start();
}
在JDK 8中,我们拥有工具javapackager,但是不幸的是,它在版本11中与JavaFX一起从Java中删除。现在,在Java 14中引入了后继jpackage(JEP 343:打包工具),利用它我们可以再次创建独立的Java安装文件。
它们的基础是包括运行时环境的Java应用程序。该工具使用此输入来构建包含所有依赖项的可执行二进制工件(格式:dmg中的msi,exe,pkg,dmg中的app,deb和rpm)。
总结
Java没有死,Java万岁!半年两次的OpenJDK版本使语言和平台都受益。这次,新功能比Java 12和13还要多。而且,仍有许多功能需要在将来的版本中实现。
因此,我们的Java开发人员不会感到无聊,并且未来的前景仍然一片光明。到2020年9月,我们可以预见Java 15的到来。
往期推荐
如何在选择一门编程语言后,进行深入造化?
进阶必备!35 个 Java 代码优化细节
18 个 Java8 处理日期的新花样,肯定没用过!
10 分钟实现 Java 发送邮件功能
IDEA 的 Debug 技巧大全
????关注 程序员干货分享