从Java9到18以来的新语言特性

当 Java 8 引入 Streams 和 Lambdas 时,这是一个很大的变化,使得函数式编程风格能够用更少的样板来表达。 从那时起,Java 切换到更快的发布节奏,每六个月就会出现一个新的 Java 版本。 这些版本不断为语言带来新功能。 在最近的特性中,最重要的可能是Records记录、Pattern matching(模式匹配) 和 Sealed(密封类),它们使得使用纯数据进行编程变得更加容易。

Java 18

switch的模式匹配(预览 🔍)

从以下版本可用:JDK 18 JDK 17 中预览

以前,switch 非常有限:case 只能测试完全相等,并且只能测试几种类型的值:数字、枚举类型和字符串。

此预览功能 增强了 switch 以适用于任何类型并匹配更复杂的模式

这些添加是向后兼容,使用传统常量的 switch 就像以前一样工作,例如,使用 Enum 值:

var symbol = switch (expression) {
  case ADDITION       -> "+";
  case SUBTRACTION    -> "-";
  case MULTIPLICATION -> "*";
  case DIVISION       -> "/";
};

但是,现在它也适用于 JEP 394:instanceof 的模式匹配 引入的类型模式

return switch (expression) {
  case Addition expr       -> "+";
  case Subtraction expr    -> "-";
  case Multiplication expr -> "*";
  case Division expr       -> "/";
};

一个模式支持 guards,写成 type pattern && guard expression

String formatted = switch (o) {
    case Integer i && i > 10 -> String.format("a large Integer %d", i);
    case Integer i           -> String.format("a small Integer %d", i);
    default                  -> "something else";
};

这与 if 语句中使用的类型模式非常对称,因为类似的模式可以用作条件:

if (o instanceof Integer i && i > 10) {
  return String.format("a large Integer %d", i);
} else if (o instanceof Integer i) {
  return String.format("a large Integer %d", i);
} else {
  return "something else";
}

类似于 if 条件中的类型模式,模式变量的范围是流敏感的。 例如,在下面的情况下,i 的范围是"guard表达式" 和 “右侧表达式”:

case Integer i && i > 10 -> String.format("a large Integer %d", i);

一般来说,它的工作原理与您预期的一样,但涉及许多规则和边缘情况。 如果您有兴趣,我建议您阅读相应的 JEP 或查看 [Pattern matching for instanceof](https://advancedweb.hu/new-language-features-since-java-8-to-18/#pattern-matching -for-instanceof) 章节。

Switch 现在也可以匹配 null。 传统上,当向 switch 提供 null 值时,它会抛出 NullPointerException。 当尝试在常量上匹配 null 时,情况仍然如此。 但是,现在可以添加 null 的显式案例:

switch (s) {
  case null  -> System.out.println("Null");
  case "Foo" -> System.out.println("Foo");
  default    -> System.out.println("Something else");
}

switch 不完整或一个 case 完全支配另一个 case 时,Java 编译器会发出错误:

Object o = 1234;

// OK
String formatted = switch (o) {
    case Integer i && i > 10 -> String.format("a large Integer %d", i);
    case Integer i           -> String.format("a small Integer %d", i);
    default                  -> "something else";
};

// Compile error - 'switch' 表达式不涵盖所有可能的输入值
String formatted = switch (o) {
    case Integer i && i > 10 -> String.format("a large Integer %d", i);
    case Integer i           -> String.format("a small Integer %d", i);
};

// Compile error - 第二种情况由前面的 case 标签支配
String formatted = switch (o) {
    case Integer i           -> String.format("a small Integer %d", i);
    case Integer i && i > 10 -> String.format("a large Integer %d", i);
    default                  -> "something else";
};

在 Java 18 中 模式匹配仍处于预览阶段,根据对 Java 17 的反馈进行了一些调整:

  • 出于可读性原因,优势检查已更新为强制常量大小写标签出现在相应的基于类型的模式之前。 目标是始终首先处理更具体的案例。 例如,以下代码段中的案例仅按此确切顺序有效。 如果你试图重新排列它们,你会得到一个编译错误。
switch(o) {
    case -1, 1 -> "special case"
    case Integer i && i > 0 -> "positive number"
    case Integer i -> "other integer";
}
  • 当涉及到通用密封类时,现在对密封层次结构进行详尽检查更加精确。 考虑 JEP 中的示例代码:
sealed interface I<T> permits A, B {}
final class A<X> implements I<String> {}
final class B<Y> implements I<Y> {}

static int testGenericSealedExhaustive(I<Integer> i) {
    return switch (i) {
        // Exhaustive as no A case possible!
        case B<Integer> bi -> 42;
    }
}
  • JEP 405,添加数组和记录模式也是 Java 18 的目标,但不幸的是它被推迟到了 Java 19。希望下次我们能尝到这种滋味。

此功能在 preview 🔍 中,必须使用 --enable-preview 标志显式启用。

Java 17

密封类

从以下版本可用: JDK 17(在JDK 15JDK 16 中预览)

密封的类和接口可用于限制哪些其他类或接口可以扩展或实现它们。 它提供了一种工具来更好地设计公共 API,并提供了 Enums 的替代方法来对固定数量的替代方法进行建模。

较早的 Java 版本也提供了一些机制来实现类似的目标。 用 final 关键字标记的类根本不能扩展,并且使用访问修饰符可以确保类仅由同一包的成员扩展。

在这些现有设施之上密封类添加了一种细粒度的方法,允许作者明确列出子类。

public sealed class Shape
    permits Circle, Quadrilateral {...}

在这种情况下,只允许使用 CircleQuadrilateral 类扩展 Shape 类。 实际上,permits 一词可能有点误导,因为它不仅允许,而且要求列出的类直接扩展密封类

此外,正如人们对此类授权所期望的那样,如果任何其他类尝试扩展密封类,则会出现编译错误

扩展密封类的类必须遵守一些规则。

作者必须始终通过在允许的子类上使用以下修饰符之一来明确定义密封类型层次结构的边界

  • final: 子类根本无法扩展
  • sealed: 子类只能由一些允许的类扩展
  • non-sealed: 子类可以自由扩展

因为子类也可以密封,这意味着可以定义固定替代品的整个层次结构

public sealed class Shape
    permits Circle, Quadrilateral, WeirdShape {...}

public final class Circle extends Shape {...}

public sealed class Quadrilateral extends Shape
    permits Rectangle, Parallelogram {...}
public final class Rectangle extends Quadrilateral {...}
public final class Parallelogram extends Quadrilateral {...}

public non-sealed class WeirdShape extends Shape {...}

在这里插入图片描述
如果这些类很短并且主要是关于数据的,那么在同一个源文件中声明它们可能是有意义的,在这种情况下,可以省略 permits 子句

public sealed class Shape {
  public final class Circle extends Shape {}

  public sealed class Quadrilateral extends Shape {
    public final class Rectangle extends Quadrilateral {}
    public final class Parallelogram extends Quadrilateral {}
  }

  public non-sealed class WeirdShape extends Shape {}
}

记录类也可以作为叶子作为密封层次结构的一部分,因为它们是隐含的最终类。

允许的类必须与超类位于同一个包中——或者,在使用 java 模块的情况下,它们必须驻留在同一个模块中。

⚠️ 提示:考虑使用密封类而不是枚举

Sealed classes 之前,只能使用 Enum types 对固定替代品进行建模。 例如。:

enum Expression {
  ADDITION,
  SUBTRACTION,
  MULTIPLICATION,
  DIVISION
}

但是,所有变体都需要在同一个源文件中,并且 Enum types 不支持需要实例而不是常量时的建模案例,例如 表示一种类型的单个消息。

Sealed classes 提供了Enum types 的一个很好的替代方案,使得使用常规类对固定替代方案进行建模成为可能。 一旦 Pattern Matching for switch 准备好生产,这将完全发挥作用,之后 Sealed classes 可以像枚举一样在 switch 表达式中使用,并且编译器可以自动检查是否涵盖了所有情况。

可以使用 values 方法枚举枚举值。 对于 Sealed 类和接口,可以使用 getPermittedSubclasses 列出允许的子类。

Java 16

记录类

从以下版本可用: JDK 16(在JDK 14JDK 15 中预览)

Record Classes 为语言引入了一个新的类型声明来定义不可变的数据类。 它允许我们使用紧凑语法,而不是通常的私有字段、getter 和构造函数:

public record Point(int x, int y) { }

上面的记录类很像一个常规类,它定义了以下内容:

  • 两个private``final字段,int xint y
  • 一个以 xy 作为参数的构造函数
  • x()y() 方法作为字段的 getter
  • hashCodeequalstoString,每一个都考虑到 xy

它们可以像普通类一样使用:

var point = new Point(1, 2);
point.x(); // returns 1
point.y(); // returns 2

记录类旨在成为其浅不可变数据透明载体。 为了支持这种设计,它们带有一组限制

Record Class 的字段在默认情况下不仅是 final,而且甚至不可能有任何非 final 字段

定义的标题必须定义有关可能状态的所有内容。 记录类的正文中不能有其他字段。 此外,虽然可以定义额外的构造函数来为某些字段提供默认值,但无法隐藏将所有记录字段作为参数的规范构造函数

最后,记录类不能扩展其他类,它们不能声明本机方法,它们是隐式最终不能是抽象的

向记录提供数据只能通过其构造函数进行。 默认情况下,Record Class 只有一个隐式的规范构造函数。 如果需要验证或规范化数据,规范构造函数也可以显式定义:

public record Point(int x, int y) {
  public Point {
    if (x < 0) {
      throw new IllegalArgumentException("x can't be negative");
    }
    if (y < 0) {
      y = 0;
    }
  }
}

隐含的规范构造器与记录类本身具有相同的可见性。 如果它是显式声明的,它的访问修饰符必须至少与 Record Class 的访问修饰符一样宽松。

也可以定义额外的构造函数,但它们必须委托给其他构造函数。 最后,规范构造函数将始终被调用。 这些额外的构造函数可能有助于提供默认值:

public record Point(int x, int y) {
  public Point(int x) {
    this(x, 0);
  }
}

可以通过其访问器方法从记录中获取数据。 对于每个字段x,记录类都有一个以x()形式生成的公共 getter 方法。

这些 getter 也可以显式定义:

public record Point(int x, int y) {
  @Override
  public int x() {
    return x;
  }
}

请注意,在这种情况下可以使用 Override 注解来确保方法声明明确定义了一个访问器,而不是意外地定义了一个额外的方法。

与 getter 类似,考虑所有字段,默认提供 hashCodeequalstoString 方法; 这些方法也可以显式定义。

最后,Record Classes 还可以具有静态和实例方法,可以方便地获取派生信息或充当工厂方法:

public record Point(int x, int y) {
  static Point zero() {
    return new Point(0, 0);
  }
  
  boolean isZero() {
    return x == 0 && y == 0;
  }
}

总结一下:Record Classes 只是关于它们携带的数据,没有提供太多的自定义选项。

由于这种特殊的设计,记录的序列化比[常规类](http://cr.openjdk.java.net/~briangoetz/amber/serialization.html)更容易和安全。 正如 JEP 中所写:

记录类的实例可以被序列化和反序列化。 但是,不能通过提供 writeObject、readObject、readObjectNoData、writeExternal 或 readExternal 方法来自定义流程。 记录类的组件控制序列化,而记录类的规范构造函数控制反序列化。

因为序列化完全基于字段状态并且反序列化总是调用规范构造函数,所以不可能创建具有无效状态的记录。

从用户的角度来看,启用和使用序列化可以像往常一样完成:

public record Point(int x, int y) implements Serializable { }

public static void recordSerializationExample() throws Exception {
  Point point = new Point(1, 2);

  // Serialize
  var oos = new ObjectOutputStream(new FileOutputStream("tmp"));
  oos.writeObject(point);

  // Deserialize
  var ois = new ObjectInputStream(new FileInputStream("tmp"));
  Point deserialized = (Point) ois.readObject();
}

请注意,不再需要定义 serialVersionUID,因为 Record Classes 放弃了匹配 serialVersionUID 值的要求。

资源:

⚠️ 提示:使用本地记录对中间转换进行建模

复杂的数据转换要求我们对中间值进行建模。 在 Java 16 之前,一个典型的解决方案是依赖库中的“Pair”或类似的持有者类,或者定义自己的(可能是内部静态的)类来保存这些数据。

这样做的问题是前者经常被证明是不灵活的,而后者通过引入仅在单个方法的上下文中使用的类来污染命名空间。 也可以在方法体内定义类,但由于它们冗长的性质,它很少适合。

Java 16 对此进行了改进,因为现在还可以在方法主体中定义本地记录

public List<Product> findProductsWithMostSaving(List<Product> products) {
  record ProductWithSaving(Product product, double savingInEur) {}

  products.stream()
    .map(p -> new ProductWithSaving(p, p.basePriceInEur * p.discountPercentage))
    .sorted((p1, p2) -> Double.compare(p2.savingInEur, p1.savingInEur))
    .map(ProductWithSaving::product)
    .limit(5)
    .collect(Collectors.toList());
}

Record Classes 的紧凑语法非常适合 Streams API 的紧凑语法。

除了记录之外,此更改还允许使用本地枚举甚至接口

⚠️ 提示: 检查您的库

记录类不遵守 JavaBeans 约定

  • 它们没有默认构造函数。
  • 他们没有设置方法。
  • 访问器方法不遵循 getX() 形式。

由于这些原因,一些需要 JavaBeans 的工具可能无法完全处理记录

一种这样的情况是记录不能用作 JPA(例如 Hibernate)实体。 有一个 关于将规范与 jpa-dev 邮件列表中的 Java 记录对齐的讨论,但到目前为止我还没有找到关于 开发过程的状态。 然而值得一提的是**记录可用于预测** 没有问题。

大多数**我检查过的其他工具**(包括 [Jackson](https://github.com/FasterXML /jackson)、Apache Commons LangJSON-PGuava ) 支持记录,但由于它很新,所以也有一些粗糙的边缘。 例如,流行的 JSON 库 Jackson 是 记录的早期采用者。 它的大部分功能,包括序列化和反序列化,对于 Record Classes 和 JavaBeans 都同样适用,但是一些 [操作对象的功能还有待调整](https://github.com/FasterXML/jackson-databind/issues/3079 )。

我碰到的另一个例子是 Spring,它也支持记录 在很多情况下都是开箱即用的。 该列表包括序列化甚至依赖注入,但是许多 Spring 应用程序使用的 ModelMapper不支持将 JavaBeans 映射到记录类

我的建议是在采用 Record Classes 之前升级并检查您的工具以避免意外,但通常假设流行的工具已经涵盖了它们的大部分功能是公平的。

查看我的 GitHub 上记录类的工具集成实验

instanceof 的模式匹配

从以下版本可用: JDK 16(在JDK 14JDK 15 中预览)

在大多数情况下,instanceof 通常后跟一个强制转换:

if (obj instanceof String) {
    String s = (String) obj;
    // use s
}

至少在过去,因为 Java 16 扩展了 instanceof 以使这种典型场景不那么冗长:

if (obj instanceof String s) {
    // use s
}

模式是 test (obj instanceof String) 和 pattern variable (s) 的组合。

test 的工作方式几乎与旧的 instanceof 的测试类似,除了它会导致编译错误,如果它总是保证通过:

// "old" instanceof, without pattern variable:
// compiles with a condition that is always true
Integer i = 1;
if (i instanceof Object) { ... } // works

// "new" instanceof, with the pattern variable:
// yields a compile error in this case
if (i instanceof Object o) { ... } // error

请注意,相反的情况,即模式匹配总是失败,即使使用旧的 instanceof,也已经是编译时错误。

只有当测试通过时,模式变量才会从目标中提取。 它几乎像常规的非最终变量一样工作

  • 它可以修改
  • 它可以隐藏字段声明
  • 如果存在同名的局部变量,则会导致编译错误

但是,对它们应用了特殊的范围规则:模式变量在它明确匹配的范围内,由流程范围分析决定。

最简单的情况就是上面的例子:如果测试通过,变量s可以在if块中使用。

但是“绝对匹配”的规则也适用于更复杂条件的部分:

if (obj instanceof String s && s.length() > 5) {
  // use s
}

s 可以用在条件的第二部分,因为它只在第一个成功并且 instanceof 运算符匹配时才被评估。

举一个更简单的例子,提前返回和异常也可以保证匹配:

private static int getLength(Object obj) {
  if (!(obj instanceof String s)) {
    throw new IllegalArgumentException();
  }

  // s is in scope - if the instanceof does not match
  //      the execution will not reach this statement
  return s.length();
}

流程范围分析的工作方式类似于现有的流程分析,例如检查 确定分配

private static int getDoubleLength(String s) {
  int a; // 'a' declared but unassigned
  if (s == null) {
    return 0; // return early
  } else {
    a = s.length(); // assign 'a'
  }

  // 'a' is definitely assigned
  // so we can use it
  a = a * 2;
  return a;
}

我真的很喜欢这个特性,因为它可能会减少在 Java 程序中导致显式强制转换的不必要的膨胀。 然而,与更现代的语言相比,这个功能似乎仍然有点冗长。

例如,在 Kotlin 中,您不需要定义模式变量:

if (obj is String) {
    print(obj.length)
}

在 Java 的情况下,模式变量被添加以确保向后兼容性,因为在 obj instanceof String 中更改 obj 的类型意味着当 obj 用作重载方法的参数时,调用可以解析为 不同版本的方法。

⚠️ 提示:请继续关注更新

模式匹配功能在目前的形式中可能看起来没什么大不了的,但很快它就会获得更多有趣的功能。

JEP 405 建议添加分解功能以匹配记录类或数组的内容:

if (o instanceof Point(int x, int y)) {
  System.out.println(x + y);
}

if (o instanceof String[] { String s1, String s2, ... }){
  System.out.println("The first two elements of this array are: " + s1 + ", " + s2);
}

然后,JEP 406 是关于在 switch 语句和表达式中添加模式匹配功能:

return switch (o) {
  case Integer i -> String.format("int %d", i);
  case Long l    -> String.format("long %d", l);
  case Double d  -> String.format("double %f", d);
  case String s  -> String.format("String %s", s);
  default        -> o.toString();
};

目前这两个 JEP 都处于 Candidate 状态并且没有具体的目标版本,但我希望我们能尽快看到他们的预览版本。

Java 15

文本块

从以下版本可用: JDK 15(在JDK 13JDK 14 中预览)

与其他现代语言相比,在 Java 中表达包含多行的文本是出了名的困难:

String html = "";
html += "<html>\n";
html += "  <body>\n";
html += "    <p>Hello, world</p>\n";
html += "  </body>\n";
html += "</html>\n";

System.out.println(html);

为了使这种情况对程序员更友好,Java 15 引入了称为文本块的多行字符串文字:

String html = """
          <html>
            <body>
              <p>Hello, world</p>
            </body>
          </html>
          """;

System.out.println(html);

它们类似于旧的字符串文字,但它们可以包含新行和引号而无需转义

文本块以 """ 开头,后跟一个新行,并以 """ 结尾。 结束标记可以位于最后一行的末尾,也可以位于单独的行中,例如上面的示例。

它们可以在任何可以使用旧字符串字面量的地方使用,并且它们都产生相似的字符串对象。

对于源代码中的每个换行符,结果中都会有一个 \n 字符。

String twoLines = """
          Hello
          World
          """;

这可以通过以 \ 字符结束行来防止,这在您想将非常长的行分成两行以保持源代码可读的情况下很有用。

String singleLine = """
          Hello \
          World
          """;

文本块可以与相邻的 Java 代码对齐,因为自动删除附带的缩进。 编译器检查每行中用于缩进的空格以找到缩进最少的行,并将每行向左移动这个最小的公共缩进。

这意味着如果关闭的 """ 在单独的行中,可以通过将关闭标记向左移动来增加缩进。

String noIndentation = """
          First line
          Second line
          """;

String indentedByToSpaces = """
          First line 
          Second line
        """;

开头的 """ 不计入缩进删除,因此没有必要将文本块与其对齐。例如,以下两个示例生成具有相同缩进的相同字符串:

String indentedByToSpaces = """
         First line 
         Second line
       """;

String indentedByToSpaces = """
                              First line 
                              Second line
                            """;

String 类还提供了一些编程方式来处理缩进。 indent 方法接受一个整数并返回一个具有指定附加缩进级别的新字符串,而 stripIndent 返回原始字符串的内容,没有所有附带的缩进。

文本块不支持插值,我真的很怀念这个功能。 正如 JEP 所说,将来可能会考虑,在此之前我们可以使用 String::formattedString::format

var greeting = """
    hello
    %s
    """.formatted("world");

资源:

⚠️ 提示:保留结尾空格

文本块中的结尾空格被忽略。 这通常不是问题,但在某些情况下它们确实很重要,例如在单元测试的上下文中,将方法结果与基线值进行比较。

如果是这种情况,请注意它们,如果一行以空格结尾,请在行尾添加 \s\t 而不是最后一个空格或制表符。

⚠️ 提示:为 Windows 生成正确的换行符

行尾 在 Unix 和 Windows 上用不同的控制字符表示。 前者使用单个换行符(\n),而后者使用回车后跟换行符(\r\n)。

但是,无论您选择使用何种操作系统或如何在源代码中编码新行,文本块都会为每个新行使用单个 \n,这可能会导致兼容性问题。

Files.writeString(Paths.get("<PATH_TO_FILE>"), """
    first line
    second line
    """);

如果使用仅与 Windows 行结束格式兼容的工具(例如记事本)打开此类文件,它将仅显示一行。 如果您还面向 Windows,请确保使用正确的控制字符,例如通过调用 String::replace 将每个 "\n" 替换为 "\r\n"

⚠️ 提示:注意一致的缩进

文本块适用于任何类型的缩进:制表符空格甚至这两者的混合。 重要的是对块中的每一行使用一致的缩进,否则无法删除附带的缩进。

大多数编辑器提供自动格式设置,并在您按 Enter 时自动在每一行添加缩进。 确保使用这些工具的最新版本以确保它们与文本块很好地配合,并且不要尝试添加错误的缩进。

有用的 NullPointerExceptions

从以下版本可用: JDK 15 (在 JDK 14 中使用 -XX:+ShowCodeDetailsInExceptionMessages 启用)

这个小宝石并不是真正的语言功能,但它非常好,我想将它包含在这个列表中。

传统上,遇到 NullPointerException 是这样的:

node.getElementsByTagName("name").item(0).getChildNodes().item(0).getNodeValue();

Exception in thread "main" java.lang.NullPointerException
        at Unlucky.method(Unlucky.java:83)

从异常来看,在这种情况下哪个方法返回 null 并不明显。 出于这个原因,许多开发人员过去常常将此类语句分布在多行中,以确保他们能够找出导致异常的步骤。

从 Java 15 开始,不需要这样做,因为 NPE 会在语句中描述哪个部分为空。 (此外,在 Java 14 中,您可以使用 -XX:+ShowCodeDetailsInExceptionMessages 标志启用它。)

Exception in thread "main" java.lang.NullPointerException:
  Cannot invoke "org.w3c.dom.Node.getChildNodes()" because
  the return value of "org.w3c.dom.NodeList.item(int)" is null
        at Unlucky.method(Unlucky.java:83)

(查看 GitHub 上的示例项目)

详细消息包含无法执行的操作(无法调用 getChildNodes)和失败原因(item(int)null),从而更容易找到问题的确切根源。

所以总的来说这个特性有利于调试,也有利于代码的可读性,因为出于技术原因而牺牲它的理由更少。

有用的 NullPointerExceptions 扩展是在 JVM 中实现的,因此您可以在使用旧 Java 版本编译的代码以及使用其他 JVM 语言(例如 Scala 或 Kotlin)时获得相同的好处。

请注意,并非所有 NPE 都会获得此额外信息,只有那些由 JVM 创建和抛出的信息:

  • 在 null 上读取或写入字段
  • 在 null 上调用方法
  • 访问或分配数组元素(索引不是错误消息的一部分)
  • 拆箱 null

另请注意,此功能不支持序列化。 例如,当在通过 RMI 执行的远程代码上抛出 NPE 时,异常将不包括有用的消息。

目前,有用的NullPointerExceptions 默认禁用,必须使用 -XX:+ShowCodeDetailsInExceptionMessages 标志启用。

⚠️ 提示:检查您的工具

升级到 Java 15 时,请务必检查您的应用程序和基础架构,以确保:

  • 敏感变量名不会出现在日志文件和 Web 服务器响应中
  • 日志解析工具可以处理新的消息格式
  • 构建额外细节所需的额外开销是可以的

Java 14

Switch 表达式

从以下版本可用: JDK 14(在JDK 12JDK 13 中预览)

Java 14 对旧的switch进行了改头换面。虽然 Java 继续支持旧的 switch 语句,但它添加了 新的 switch 表达式 到语言:

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    default      -> {
        String s = day.toString();
        int result = s.length();
        yield result;
    }
};

最显着的区别是这种新形式可以用作表达式。 如上例所示,它可用于填充变量,并且可用于任何接受表达式的地方:

int k = 3;
System.out.println(
    switch (k) {
        case  1 -> "one";
        case  2 -> "two";
        default -> "many";
    }
);

但是,switch 表达式和 switch 语句之间还有一些其他更细微的区别。

首先,对于 switch 表达式cases don’t fall-through。 因此,不会再出现因缺少“breaks”而导致的细微错误。 为避免失败,可以在逗号分隔的列表中为每种情况指定多个常量

其次,每个 case 都有自己的范围

String s = switch (k) {
    case  1 -> {
        String temp = "one";
        yield temp;
    }
    case  2 -> {
        String temp = "two";
        yield temp;
    }
    default -> "many";
}

一个分支要么是一个单一的表达式,要么如果它由多个语句组成,它必须被包装在一个块中。

第三,switch 表达式的用例是详尽的。 这意味着对于字符串、原始类型及其包装器,必须始终定义“默认”情况。

int k = 3;
String s = switch (k) {
    case  1 -> "one";
    case  2 -> "two";
    default -> "many";
}

对于 enums 要么必须存在 default 案例,要么必须明确涵盖所有案例。 依靠后者可以很好地确保考虑所有值。 向 enum 添加一个额外的值将导致所有使用它的 switch 表达式的编译错误。

enum Day {
   MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

Day day = Day.TUESDAY;
switch (day) {
    case  MONDAY -> ":(";
    case  TUESDAY, WEDNESDAY, THURSDAY -> ":|";
    case  FRIDAY -> ":)";
    case  SATURDAY, SUNDAY -> ":D";
}

由于所有这些原因,优先使用 switch 表达式而不是 switch 语句可以导致更可维护的代码。

⚠️提示:使用箭头语法

Switch 表达式不仅可以与类似 lambda 的箭头形式的情况一起使用。 带有冒号形式的旧 switch 语句也可以用作表达式 使用 yield

int result = switch (s) {
    case "foo":
    case "bar":
        yield 2;
    default:
        yield 3;
};

这个版本也可以用作表达式,但它更类似于旧的 switch 语句,因为

  • 案例落空
  • 案例共享相同的范围

我的建议? 不要使用这种形式,而是使用带有箭头语法的 switch 表达式来获得所有好处。

Java 11

局部变量类型推断

从以下版本可用: JDK 11JDK 10 中不支持 lambda)

自 Java 8 以来最显着的语言改进可能是添加了 var 关键字。 它最初是在 Java 10 中引入的,并在 Java 11 中进一步改进。

这个特性允许我们通过省略显式类型规范来减少局部变量声明的仪式:

var greetingMessage = "Hello!";

虽然它看起来类似于 Javascript 的 var 关键字,但这与动态类型无关

引用 JEP 中的这段话:

我们寻求通过减少与编写 Java 代码相关的仪式来改善开发人员体验,同时保持 Java 对静态类型安全的承诺。

声明变量的类型是在编译时推断的。 在上面的示例中,推断的类型是 String。 使用 var 代替显式类型可以减少这段代码的冗余,因此更易于阅读。

这是类型推断的另一个很好的候选者:

MyAwesomeClass awesome = new MyAwesomeClass();

很明显,在许多情况下,此功能可以提高代码质量。 但是,有时最好坚持使用显式类型声明。 让我们看几个例子,其中用 var 替换类型声明会适得其反。

⚠️ 提示:记住可读性

第一种情况是从源代码中删除显式类型信息使其可读性降低。

当然,IDE 可以在这方面提供帮助,但在代码审查期间或当您只是快速扫描代码时,它可能会损害可读性。 例如,考虑工厂或构建器:您必须找到负责对象初始化的代码来确定类型。

这是一个小谜题。 以下代码使用 Java 8 的日期/时间 API。 猜测以下代码段中的变量类型:

var date = LocalDate.parse("2019-08-13");
var dayOfWeek = date.getDayOfWeek();
var dayOfMonth = date.getDayOfMonth();

做了什么?解决方案:

第一个非常直观,parse 方法返回一个LocalDate 对象。 但是,对于接下来的两个,您应该更熟悉 API:dayOfWeek 返回一个 java.time.DayOfWeek,而 dayOfMonth 只返回一个 int

另一个潜在的问题是,使用 var 时,读者必须更多地依赖上下文。 考虑以下:

private void longerMethod() {
    // ...
    // ...
    // ...

    var dayOfWeek = date.getDayOfWeek();

    // ...
    // ...
    // ...
}

根据前面的例子,我敢打赌你会猜到它是一个java.time.DayOfWeek。 但这一次,它是一个整数,因为这个例子中的 date 来自 Joda 时间。 这是一个不同的 API,行为略有不同,但你看不到它,因为它是一个更长的方法,而且你没有阅读所有行。 (JavaDoc: Joda 时间 / [Java 8 日期/时间 API](https: //docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html#getDayOfWeek–))

如果存在显式类型声明,那么弄清楚 dayOfWeek 的类型将是微不足道的。 现在,对于 var,读者首先必须找出 date 变量的类型并检查 getDayOfWeek 做了什么。 使用 IDE 很简单,仅扫描代码时就不那么简单了。

⚠️ 提示:注意保留重要的类型信息

第二种情况是使用 var 会删除所有可用的类型信息,因此甚至无法推断。 在大多数情况下,这些情况都会被 Java 编译器捕获。 例如,var 无法推断 lambda 或方法引用的类型,因为对于这些功能,编译器依赖左侧表达式来确定类型。

但是,也有一些例外。 例如,var 不能很好地与 Diamond 运算符配合使用。 Diamond 运算符是一个很好的功能,可以在创建泛型实例时从表达式的右侧删除一些冗长:

Map<String, String> myMap = new HashMap<String, String>(); // Pre Java 7
Map<String, String> myMap = new HashMap<>(); // Using Diamond operator

因为它只处理泛型类型,所以仍然需要删除冗余。 让我们尝试使用 var 使其更简洁:

var myMap = new HashMap<>();

这个例子是有效的,Java 11 甚至没有在编译器中发出关于它的警告。 然而,通过所有这些类型推断,我们最终根本没有指定泛型类型,类型将是 Map<Object, Object>

当然,这可以通过删除 Diamond Operator 轻松解决:

var myMap = new HashMap<String, String>();

var 与原始数据类型一起使用时,可能会出现另一组问题:

byte   b = 1;
short  s = 1;
int    i = 1;
long   l = 1;
float  f = 1;
double d = 1;

如果没有显式类型声明,所有这些变量的类型将被推断为 int。 在处理原始数据类型时使用类型文字(例如1L),或者在这种情况下根本不使用var

⚠️ 提示:请务必阅读官方风格指南

最终由您决定何时使用类型推断,并确保它不会损害可读性和正确性。作为经验法则,坚持良好的编程实践,比如良好的命名和最小化局部变量的作用域,肯定会有很大帮助。请务必阅读关于var的官方风格指南FAQ

因为 var 有很多陷阱,所以它被保守地引入并且只能用于局部变量,这很好,其范围通常非常有限。

另外,它被谨慎地引入,var 不是一个新的关键字,而是一个保留的类型名称。 这意味着它只有在用作类型名称时才具有特殊含义,在其他任何地方 var 仍然是一个有效的标识符。

目前,var 没有不可变的对应项(例如 valconst)来声明最终变量并使用单个关键字推断其类型。 希望我们能在未来的版本中得到它,在此之前,我们可以求助于final var

资源:

Java 9

允许接口中的私有方法

从以下版本可用: JDK 9(Milling Project Coin)

从 Java 8 开始,可以向接口添加默认方法。 在 Java 9 中,这些默认方法甚至可以调用私有方法来共享代码,以防您需要重用,但又不想公开功能。

虽然这不是什么大不了的事,但它是一个合乎逻辑的添加,允许在默认方法中整理代码。

匿名内部类的菱形运算符

从以下版本可用: JDK 9(Milling Project Coin)

Java 7 引入了菱形运算符 (<>),通过让编译器推断构造函数的参数类型来减少冗长:

List<Integer> numbers = new ArrayList<>();

但是,此功能以前不适用于匿名内部类。 根据 关于项目邮件列表的讨论 这不是作为原始钻石操作员功能的一部分添加的 ,因为它需要大量的 JVM 更改。

在 Java 9 中,这个小的毛边被移除了,使操作符更普遍适用:

List<Integer> numbers = new ArrayList<>() {
    // ...
}

允许在 try-with-resources 语句中将有效最终变量用作资源

从以下版本可用: JDK 9(Milling Project Coin)

Java 7 引入的另一个增强功能是“try-with-resources”,它使开发人员不必担心释放资源。

为了说明它的强大,首先考虑在这个典型的 Java 7 之前的示例中正确关闭资源所付出的努力:

BufferedReader br = new BufferedReader(...);
try {
    return br.readLine();
} finally {
    if (br != null) {
        br.close();
    }
}

使用 try-with-resources 可以自动释放资源,无需太多仪式:

try (BufferedReader br = new BufferedReader(...)) {
    return br.readLine();
}

尽管它很强大,但 try-with-resources 有一些 Java 9 解决的缺点。

尽管这种结构可以处理多种资源,但它很容易使代码更难阅读。 与通常的 Java 代码相比,在 try 关键字之后的列表中声明这样的变量有点不合常规:

try (BufferedReader br1 = new BufferedReader(...);
    BufferedReader br2 = new BufferedReader(...)) {
    System.out.println(br1.readLine() + br2.readLine());
}

此外,在 Java 7 版本中,如果您已经有一个要使用此构造处理的变量,则必须引入一个虚拟变量。 (例如,参见 JDK-8068948。)

为了减轻这些批评,“try-with-resources”得到了增强,除了新创建的变量之外,还可以处理最终或有效的最终局部变量:

BufferedReader br1 = new BufferedReader(...);
BufferedReader br2 = new BufferedReader(...);
try (br1; br2) {
    System.out.println(br1.readLine() + br2.readLine());
}

在此示例中,变量的初始化与它们在try-with-resources构造中的注册是分开的。

⚠️提示:注意释放的资源

需要记住的一个警告是,现在可以引用已经由 try-with-resources 释放的变量,这在大多数情况下会失败:

BufferedReader br = new BufferedReader(...);
try (br) {
    System.out.println(br.readLine());
}
br.readLine(); // Boom!

下划线不再是有效的标识符名称

从以下版本可用: JDK 9(Milling Project Coin)

在 Java 8 中,当使用“_”作为标识符时,编译器会发出警告。 Java 9 更进一步使唯一的下划线字符作为标识符非法,保留此名称以在将来具有特殊语义:

int _ = 10; // Compile error

改进的警告

从以下版本可用: JDK 9

最后,让我们谈谈较新 Java 版本中与编译器警告相关的更改。

现在可以使用@SafeVarargs注解私有方法来标记类型安全:通过varargs参数的潜在堆污染警告误报。 (事实上,这个变化是之前讨论过的 JEP 213: Milling Project Coin 的一部分)。 阅读更多关于 Varargs、[Generics](https://docs.oracle.com/javase/ 8/docs/technotes/guides/language/generics.html) 和 潜在问题 可以通过结合这些功能而出现 在官方文档中。

此外,从 Java 9 开始,当导入不推荐使用的类型时,编译器不会针对导入语句发出警告。 这些警告信息不丰富且多余,因为在实际使用已弃用成员时始终会显示单独的警告。

总结

本文介绍了自Java 9以来与Java语言相关的改进。密切关注Java平台是很重要的,因为随着新的快速发布节奏,每六个月就会发布一个新的Java版本,交付对平台和语言的更改。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值