Java 14 带来了许多新功能

作者: Raoul-Gabriel Urma
2020年2月27

Java14计划于3月17日发布。版本14包含的JEP(Java增强建议)比 Java 12 和 13 的总和还多。那么,对于每天编写和维护代码的Java开发人员来说,哪些才是最重要的呢? 在本文中,我研究了以下新特性:

  • 改进 switch 表达式,最初作为预览出现在Java 12和Java 13中,现在已经完全成为Java 14的一部分

  • instanceof 模式匹配(语言特性)

  • 更清晰的 NullPointerExceptions (JVM特性) 如果您阅读本文并在代码中尝试这些特性,期待您通过向Java团队提供反馈来分享您的经验。这样做,您就有机会为Java的开发做出贡献。

Switch 表达式

在Java 14中,switch表达式已经成为正式功能(译者:Java13 中是预览功能)。如果您需要了解 switch 表达式是什么,前面两篇文章中已经详细介绍了它们。 新的 switch 表达式的优点包括:由于没有失败行为,减少了bug的范围;由于使用了表达式和复合形式,它使编写变得非常容易。作为复习示例,开关表达式现在可以利用箭头语法,例如在本示例中:

var log = switch (event) {
    case PLAY -> "User has triggered the play button";
    case STOP, PAUSE -> "User needs a break";
    default -> {
        String message = event.toString();
        LocalDateTime now = LocalDateTime.now();
        yield "Unknown event " + message +
              " logged on " + now;
    }
};

文本块(Text Blocks)

Java 13 引入了预览特性文本块。文本块使使用多行字符串文字更容易。这个特性将在Java 14中进行第二轮预览,并包含一些调整。回顾一下,编写具有许多字符串连接和转义序列的代码以提供足够的多行文本格式是很常见的。下面的代码显示了HTML格式的示例:

String html = "<HTML>" +
"\n\t" + "<BODY>" +
"\n\t\t" + "<H1>\"Java 14 is here!\"</H1>" +
"\n\t" + "</BODY>" +
"\n" + "</HTML>";

使用文本块,可以简化此过程,并使用分隔文本块开头和结尾的三个引号编写更优雅的代码:

String html = """
<HTML>
  <BODY>
    <H1>"Java 14 is here!"</H1>
  </BODY>
</HTML>""";

与普通字符串文本相比,文本块还提供更高的表达能力。您可以在前面的 文章 中阅读更多关于这个的内容。 在Java 14中添加了两个新的转义符。
首先,可以使用新的转义符 \s 来表示单个空格。
其次,可以使用反斜杠 \ 来禁止在行尾插入换行字符。
当您有一个很长的行文本使用 \ 改写成多行时看起来会更清晰。 例如,当前处理多行字符串的方法是

String literal =
         "Lorem ipsum dolor sit amet, consectetur adipiscing " +
         "elit, sed do eiusmod tempor incididunt ut labore " +
         "et dolore magna aliqua.";

使用文本块中的 \ 转义符,可以表示为:

String text = """
                Lorem ipsum dolor sit amet, consectetur adipiscing \
                elit, sed do eiusmod tempor incididunt ut labore \
                et dolore magna aliqua.\
                """;

instanceof 模式匹配

Java 14 引入了一个预览特性,它有助于消除 instanceof 条件检查之前进行显式转换。例如,请考虑以下代码:

if (obj instanceof Group) {
  Group group = (Group) obj;
  // use group specific methods
  var entries = group.getEntries();
}

可以使用这个预览功能重构为:

if (obj instanceof Group group) {
  var entries = group.getEntries();
}

在第一个片段中 条件检查断言 obj 属于 Group 类型,为什么还需要使用条件块再次声明 obj 属于 Group 类型?这种需要可能会增加错误的范围。 较短的语法将从典型Java程序中减少许多类型转换。( 研究论文从2011年开始,提出了一个相关的语言特性,报告说,大约24%的类型转换遵循条件语句中的 instanceof。) JEP 305涵盖了这一变化,并指出了Joshua Bloch的 Effective Java一书中的一个示例,该示例用以下等式方法说明了这一点:

@Override public boolean equals(Object o) {
    return (o instanceof CaseInsensitiveString) &&
            ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}

通过将多余的显式强制转换为 CaseInsensitiveString ,可以将前面的代码简化为以下形式:

@Override public boolean equals(Object o) {
    return (o instanceof CaseInsensitiveString cis) &&
            cis.s.equalsIgnoreCase(s);
}

这是一个有趣的预览功能,因为它为更一般的模式匹配打开了大门。模式匹配的思想是在特定条件下,为提取对象的成分提供一种更简洁的语法。 instanceof 运算符就是这种情况,因为条件是类型检查,提取操作正在调用适当的方法或访问特定字段。 换句话说,这个预览功能只是一个开始,您可以期待一些更简洁的语法,它可以帮助进一步减少冗长,从而减少出现错误的可能性。

Records

还有另一个预览语言功能:Records 。与目前提出的其他想法一样,这个特性遵循了帮助开发人员编写更简洁Java代码的趋势。 Records 集中于某些领域 class,这些领域 class 的目的只是将数据存储在字段中,并且不声明任何自定义行为。(译者:如各种DTO / VO / Entity )
例如有一个简单的领域class BankTransaction ,它用三个字段对事务建模:日期、金额和描述。在声明类时,需要考虑这些:

  • 构造函数(constructor)

  • Getter 方法

  • toString()

  • hashCode() and equals() 这些代码通常由IDE自动生成,占用大量空间。下面是 BankTransaction 类完整生成后的代码:

public class BankTransaction {
    private final LocalDate date;
    private final double amount;
    private final String description;
    public BankTransaction(final LocalDate date,
                           final double amount,
                           final String description) {
        this.date = date;
        this.amount = amount;
        this.description = description;
    }
    public LocalDate date() {
        return date;
    }
    public double amount() {
        return amount;
    }
    public String description() {
        return description;
    }
    @Override
    public String toString() {
        return "BankTransaction{" +
                "date=" + date +
                ", amount=" + amount +
                ", description='" + description + '\'' +
                '}';
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        BankTransaction that = (BankTransaction) o;
        return Double.compare(that.amount, amount) == 0 &&
                date.equals(that.date) &&
                description.equals(that.description);
    }
    @Override
    public int hashCode() {
        return Objects.hash(date, amount, description);
    }
}

Java 14提供了一种方法来消除冗长,并使意图明确,您所需要的只是一个类,该类仅聚合数据并实现了 equalshashCodetoString 方法。可以按如下方式重构 BankTransaction

public record BankTransaction(Date date,
                              double amount,
                              String description) {}

有了 record,除了构造函数和getter之外,还“自动”实现了equals、hashCode和toString。 要尝试此示例,请记住需要使用preview flag编译文件:

javac --enable-preview --release 14 BankTransaction.java

record的字段是隐式的 final。这意味着您不能给字段重新赋值。但是,请注意,这并不意味着整个记录是不可变的;存储在字段中的对象本身是可变的。(译者:比如 public record A ( List<Integer> list ) {} list.add(…​) 是可以的)

如果您对 record 更详细的文章感兴趣,请查看BenEvans最近发表在 java Magazine中的文章。 敬请期待。 record 还从教育的角度为下一代Java开发人员提出了有趣的问题。例如,如果你指导初级开发人员,什么时候应该在课程中引入 record:在引入OOP和类之前还是之后?

更清晰的 NullPointerExceptions

有人说 throw NullPointerExceptions 应该是Java中新的 “Hello world” ,因为你无法逃离它们。开玩笑的是,当代码在生产环境中运行时,它们经常出现在应用程序日志中,这会导致调试困难,因为原始代码不易获得。例如,考虑下面的代码:

var name = user.getLocation().getCity().getName();

在Java 14之前,可能会出现以下错误:

Exception in thread "main" java.lang.NullPointerException
    at NullPointerExample.main(NullPointerExample.java:5)

不幸的是,如果在第5行,有一个具有多个方法调用的赋值,— getLocation()getCity() — 都可能返回 null 。实际上,变量 user 也可能为 null 。所以,搞不清楚是什么导致了 NullPointerException 。 现在,Java 14有一个新的 JVM 特性,可以获得更多的诊断信息:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Location.getCity()" because the return value of "User.getLocation()" is null
    at NullPointerExample.main(NullPointerExample.java:5)

错误消息现在有两个清晰的组成部分:

  • The consequence: Location.getCity() cannot be invoked.

  • The reason: The return value of User.getLocation() is null.

    只有使用以下选项运行Java时,才会启用增强诊断: -XX:+ShowCodeDetailsInExceptionMessages

    如:

java -XX:+ShowCodeDetailsInExceptionMessages NullPointerExample

+ 在Java的未来版本中,默认情况下可能会启用此功能,如 所述。 此增强功能不仅可用于方法调用,还可用于其他可能导致 NullPointerException 的地方,包括字段访问、数组访问和赋值。

Conclusion

In Java 14, there are new preview language features and updates that help developers in their daily work. For example, Java 14 introduces instanceof pattern matching, which is a way to reduce explicit casts. Also, Java 14 introduces records, which are a new construct to concisely declare classes that are used solely to aggregate data. In addition, NullPointerException messages have been enhanced with better diagnostics and switch expressions are now part of Java 14. Text blocks, a feature that helps you work with multiline string values, are going through another preview round after introducing two new escape sequences. One other change that will be of interest to a subset of technicians in Java operations is event streaming in the JDK Flight Recorder. That option is discussed in this article by Ben Evans. As you can see, Java 14 brings a lot of innovation to the table. You should definitely give it a whirl and send feedback on the preview features to the Java team.

数码
沪ICP备19006215号-4