渣翻:从 Java 8 到 Java 18 的新语言特性

原文作者更新了,我也更新一波。在20年,咱们说假如要大规模晋级java版别,要等到2022年之后,嗯……今年是 2023 年。而且 2022 年发布的 Spring Framework 6.0SpringBoot 3.0 所支撑的最低 java 版别为 java 17,所以今年晋级 JDK 版别还是一个不错的挑选的,之后咱们也会晋级来体会一下 JDK 17。今日先来看下有哪些更新。

原文地址:advancedweb.hu/new-languag…

当Java 8引进Streams和Lambdas时,这是一个很大的改变,使函数式编程风格能够用更少的样板来表达。从那以后,Java切换到更快的发布节奏,每六个月呈现一个新的Java版别。这些版别不断为言语带来新功用。在最近的功用中,最重要的功用或许是记载,方式匹配和密封类,这使得运用纯数据编程变得愈加简单。

Java 8 之后的言语增强

Java 18

  • switch 的方式匹配。

Java 17

  • 密封类:

    • 留意:考虑比照一下密封类与枚举类

Java 16

  • Record

    • 留意:运用本地 Record 类对中心转化进行建模
    • 留意:考虑下你的依靠
  • 关于 instancesof 的方式匹配

    • 留意:请持续重视更新

Java 15

  • 文本块

    • 留意:保存尾随空格
    • 留意:为 Windows 生成正确的换行符
    • 留意:留意共同的缩进
  • 有用的 NullPointerExecption

    • 留意:查看你的东西

Java 14

  • switch 表达式

    • 留意:运用箭头语法

Java 11

  • 局部变量类型揣度

    • 留意:留意坚持可读性
    • 留意:留意保存重要的类型信息
    • 留意:请有必要阅览官方的风格攻略

Java 9

  • 接口类中答应私有办法

  • 内部匿名类的钻石操作符

  • 答应将有用终究变量用作 try-with-resources 句子中的资源

    • 留意:当心开释的资源
  • 下划线不再是一个有用的标识符称号

  • 提高了正告

从版别 8 发布到版别 18,Java 由 203 个 JDK 增强提案 (JEP) 和更多较小的更新组成,每个更新都为渠道带来了一些改善。此页面是最重要的改善的分类和精选列表。

新的言语特性

从Java 8开端,对言语进行了许多改善。本节是对过去几年发生的工作的快速回忆。

switch 的方式匹配(Pattern Matching for switch)

可用版别自:能够在 JDK 18 JDK 17 中预览

曾经,switch 十分有限:这些 case 只能测验准确持平性,而且只能测验几种类型的值:数字、枚举类型和字符串。

此预览功用增强了 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       -> "/";
};

也有支撑守卫方式,写为 type parttern && 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 small Integer %d", i);
} else {
  return "something else";
}

if 条件中的类型方式类似,方式变量的效果域对流敏感。例如,鄙人面的比如中,i 的规模是保护表达式和右侧表达式:

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

一般,它的工作办法如你所期望的那样,但涉及许多规则和边际状况。假如你有疑问,我主张阅览相应的 JEP 或查看下面 关于 instanceof 的方式匹配 的章节。

现在 switch 也能够匹配 null 值。传统上,当 switch 句子中呈现了 null 值的 case ,一般会抛出 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 彻底主导另一个时,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' expression does not cover all possible input values
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 - the second case is dominated by a preceding case label
// 第二个 case 由前面的 case 主导,或许说走不到第二个 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 给出的反馈进行了一些调整:

  • 出于可读性原因,已更新位置的查看,以强制常量 case 显现在相应的基于类型的方式之前。方针是一直首要具有更具体的 case。例如,以下代码片段中的 case 便是按此切当顺序有用。假如你测验重新排列它们,则会呈现编译过错。

    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,增加的数组和 record 方式也是针对 Java 18 的,但不幸的是它被推迟到Java 19。期望咱们下次能测验。

此功用处于预览状况,有必要运用 --enable-preview 标志显式启用。

密封类(Sealed Classes)

可用版别自JDK17 (JDK15 JDK16 预览)

密封类和接口可用于约束哪些其他类或接口能够扩展或完成它们。它供给了一种东西来更好地规划公共 API,并供给了枚举的代替计划来对固定数量的代替计划进行建模。

之前的 Java 版别还供给了一些完成类似方针的机制。标有 final 关键字的类底子无法扩展,而且运用拜访修饰符能够保证类仅由同一包的成员扩展。

在这些现有根底之上,密封类增加了一种细粒度的办法,答应作者清晰列出自己的子类。

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

在这个比如里,答应承继 Shape 类的只要 Circle 以及 Quadiilateral 两个子类。事实上,术语 “答应” 或许会有一些误导, 由于它不只答应,而且要求列出的类直接扩展密封类

别的,正如人们或许从这样的授权中所期望的那样,假如任何其他类企图承继密封类,则会呈现编译过错。

public abstract sealed class Shape
    permits Circle, Quadrilateral {...}
public final class Circle extends Shape {...} // OK
public final class Quadrilateral extends Shape {...} // OK
public final class Triangle extends Shape {...} // Compile error

承继密封类的类有必要符合一些规则。

作者们有必要一直经过在答应的子类上显式界说密封类型层次结构的鸿沟,仅能运用以下修饰符之一:

  • 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 {...}

渣翻:从 Java 8 到 Java 18 的新语言特性

假如这些类很短,而且主要与数据有关,则在同一源文件中声明一切这些类或许是有意义的,在这种状况下,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 {}
}

Record 类也能够作为叶子成为密封层次结构的一部分,由于它们是隐式的 final

答应的子类有必要与父类坐落同一包中 —— 或许在运用 java 模块的状况下,它们有必要坐落同一个模块中。

留意:考虑一下为何运用 密封类 而非 枚举类

在密封类之前,只能运用枚举类型对固定的选项进行建模。例如:

enum Expression {
  ADDITION,
  SUBTRACTION,
  MULTIPLICATION,
  DIVISION
}

可是,一切改变都需求在同一个源文件中,而且 Enum 类型不支撑在需求实例而不是常量(例如表明某个类型的单条音讯)时进行建模的状况。

密封类供给了一个 Enum 类型的十分好的替换计划,它能够运用惯例的类来对那些固定选项进行建模。一旦 switch 方式匹配准备好,这将到达悉数功率,之后密封类能够像枚举相同在 switch 表达式中运用,编译器能够主动查看是否涵盖了一切状况。

枚举类的值能够运用 values() 办法枚举。关于密封类和接口,答应的子类能够与 getPermittedSubclasses() 办法一同列出。

Record 类(Record Classes)

可用版别自JDK16JDK14 JDK15 预览)

记载类向言语引进了新的类型声明,以界说不可变的数据类。与一般的私有字段、getters 办法 和结构函数的办法不同,它答应咱们运用紧凑语法:

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

上面的Record类很像界说以下内容的惯例类:

  • 两个私有的 final 特点,int xint y
  • 一个接纳 xy 作为参数的结构函数
  • x()y() 作为特点的 getter 办法
  • hashCode()equals()toString() 办法,都会运用 xy 变量

它们能够像正常的类那样运用:

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

Record 类旨在成为其浅层不可变数据的透明载体。为了支撑这种规划,它们带有一组约束

默许状况下,记载类的字段不只仅 final 字段,乃至不或许有任何非 final 字段。

首要,界说声明有必要界说有关或许状况的一切内容。它不能在 Record 类的正文中包含其他字段。此外,虽然能够界说其他结构函数来为某些字段供给默许值,但将一切记载字段作为参数的标准结构函数,是无法躲藏的。

终究,Record 类不能承继其他类,它们不能声明原生办法,而且它们是隐式final 的,不能是抽象的。

只能经过它的结构函数来给记载类赋值。默许状况下,一个 Record 类只要一个隐式的标准结构函数。假如需求验证或标准化数据,还能够显式界说标准结构函数:

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;
    }
  }
}

隐式的标准结构函数与记载类自身具有相同的可见性。假如显式声明,则其拜访修饰符有必要至少与记载类的拜访修饰符相同宽松。

也能够界说其他结构函数,但它们有必要委托给其他结构函数。终究,将一直调用标准结构函数。这些额定的结构函数关于供给默许值或许很有用:

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 办法,会涵盖一切字段;当然也能够显式界说这些办法。

终究,记载类还能够具有静态和实例办法,这些办法能够方便地获取派生信息或充任工厂办法:

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

总结一下:记载类仅仅关于它们带着的数据,没有供给太多的自界说选项。

由于这种特别的规划,记载的序列化比惯例类更简单和安全。正如JEP中所写:

记载类的实例能够序列化和反序列化。可是,不能经过供给 writeObjectreadObjectreadObjectNoDatawriteExternalreadExternal 办法来自界说该进程。记载类的组件操控序列化,而记载类的标准结构函数操控反序列化。

由于序列化彻底基于字段状况,而且反序列化一直调用标准结构函数,因而无法创建具有无效状况的 Record 类。

从用户的角度来看,启用和运用序列化能够像平常相同完成:

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,由于记载类免除了匹配 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());
}

记载类的紧凑语法与流 API 的紧凑语法十分匹配。

除了记载之外,此更改还答应运用本地枚举乃至接口

留意:查看一下你的依靠

记载类并不遵守 JavaBeans 约定:

  • 它们没有默许的结构函数
  • 它们没有 setter 办法
  • 拜访函数也不是 getX() 的方式

由于这些原因,一些期望 JavaBeans 的东西或许无法彻底处理 Record 类。

其间一种状况是记载不能用作 JPA(例如 Hibernate)实体。在 jpa-dev 邮件列表中有一个关于将标准与 Java Records 对齐的评论,但到现在为止,我没有找到有关开发进程状况的音讯。可是,值得一提的是,Record 能够毫无问题地用于映射。

我查看过的大多数其他东西(包含 JacksonApache Commons LangJSON-PGuava )都支撑 record,但由于它是适当新的,也有一些粗糙的边际。例如,流行的JSON库 Jackson 是记载的早期选用者。它的大多数功用,包含序列化和反序列化,相同适用于记载类和 JavaBeans,但一些操作目标的功用没有调整。

我遇到的另一个比如是 Spring,在许多状况下,它也支撑开箱即用的记载类。该列表包含序列化乃至依靠注入,可是许多 Spring 应用程序运用的 ModelMapper 库不支撑将 JavaBeans 映射到记载类。

我的主张是在选用 Record Class 之前晋级并查看您的东西以避免意外,但一般来说,假设流行的东西现已涵盖了它们的大部分功用,这样也是适当不错的。

查看我在 GitHub 上记载类的东西集成试验。

instanceof 的方式匹配(Pattern Matching for instanceof)

可用版别自: JDK 16JDK 14``JDK 15预览)

在大多数状况下,实例一般后跟一个强制转化:

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

至少,在过去,由于Java 16扩展了实例以使这个典型场景不那么冗长:

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

方式是(obj instanceof String)和方式变量(s)的组合。

该测验的工作办法简直与旧 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 也会呈现编译时反常。

仅当测验经过时,才会从方针中提取方式变量。它的工作办法简直类似于惯例的非 final 变量:

  • 它能够被修正
  • 它能够躲藏字段声明
  • 假如存在同名的局部变量,则会导致编译过错

可是,也有一些特别的规模规则:方式变量在它必定匹配的规模内,由流程规模剖析决议。

最简单的状况是上面的比如:假如测验经过,变量 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 实例中的 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 是关于增加方式匹配功用来切换句子和表达式:

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 都处于候选状况,没有具体的方针版别,但我期望咱们能很快看到它们的预览版别。

文本块(Switch Expressions)

可用版别自: JDK15JDK13 JDK14 预览)

与其他现代言语比较,在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 键时主动在每个新行上增加缩进。请保证运用这些东西的最新版别,以保证它们与文本块合作杰出,而且不要测验增加过错的缩进。

有用的 NullPointerException(## Helpful NullPointerExceptions)

可用版别自: JDK 15JDK 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)

具体音讯包含无法执行的操作(无法调用 getChildNodes )和失利原因( item(int)null ),然后更简单找到问题的切当来源。

因而,整体而言,此功用有利于调试,也有利于代码可读性,出于技术原因而摒弃它的理由就少了一个。

这些有用的 NullPointerException 扩展是在 JVM 中完成的,因而关于运用旧 Java 版别编译的代码以及运用其他 JVM 言语(如 ScalaKotlin)的代码,能够取得相同的优点。

请留意,并非一切 NPE 都会取得这些额定信息,只要那些由 JVM 创建和抛出的信息:

  • 读取或写入空值上的字段
  • null 上调用办法
  • 拜访或分配数组的元素(索引不是过错音讯的一部分)
  • null 进行拆箱

另请留意,此功用不支撑序列化。例如,当在经过 RMI 执行的远程代码上抛出 NPE 时,反常将不包含有用的音讯。

现在,默许状况下禁用了有用的 NullPointerException,而且有必要运用 -XX:+ShowCodeDetailsInExceptionMessages 标志启用。

留意:查看你的东西

晋级到 Java 15 时,请有必要查看应用程序和根底架构以保证:

  • 敏感变量称号不会呈现在日志文件和 Web 服务器响应中
  • 日志解析东西能够处理新的音讯格局
  • 构建其他具体信息所需的额定开销是可承受的

switch 表达式(Switch Expressions)

可用自: JDK 14JDK 12 JDK 13 预览)

旧式的 switch 在Java 14中进行了改头换面。虽然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 表达式,不会呈现直通的 case。因而,不再因缺少 break 而导致的细微过错。为了直通的需求,能够在逗号分隔的列表中为每种状况指定多个常量。

其次,每个 case 都有自己的效果域

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

分支要么是单个表达式,要么假如它由多个句子组成,则有必要将其包装在一个块中。

第三, switch 表达式是要穷举的。这意味着,不管是关于字符串,还是原始类型或许其包装类,default 都是有必要要界说的。

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

关于 enum 有必要存在 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 句子的 case 中也能够运用 yield 用作表达式:

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

这种写法也能够当作表达式来用,但它与旧的 switch 句子更类似,由于:

  • 用例状况会直通。
  • 用例状况同享相同的规模。

我的主张?不要运用这种格局,而是运用带箭头语法的 switch 表达式来取得一切的优点。

局部变量类型推理(Local-Variable Type Inference)

可用版别自: JDK 11(JDK 10 中不支撑 lambda 中运用)

自Java 8以来,最重要的言语改善或许是增加了 var 关键字。它开端是在Java 10中引进的,并在Java 11中得到了进一步的改善。

此功用答应咱们经过省掉显式类型标准来削减局部变量声明的典礼:

var greetingMessage = "Hello!";

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

引证JEP中的这句话:

咱们力求经过削减与编写 Java 代码相关的典礼来改善开发人员体会,一同坚持 Java 对静态类型安全的承诺。

声明变量的类型是在编译时揣度的。在上面的示例中,揣度的类型是字符串。运用 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,行为略有不同,但你看不到它,由于它是一个更长的办法,而且你没有阅览一切的行。

假如存在显式类型声明,则弄清楚 dayOfWeek 的类型将变得微不足道。现在,运用 var ,读者首要有必要找出 date 变量的类型并查看 getDayOfWeek 的效果。这关于 IDE 来说很简单,而仅仅扫描代码就不那么简单了。

关键:留意保存重要的类型信息

第二种状况是运用 var 会删去一切可用的类型信息,因而乃至无法揣度。在大多数状况下,Java 编译器会捕获这些状况。例如, var 无法揣度 lambda 或办法引证的类型,由于关于这些功用,编译器依靠于左边表达式来找出类型。

可是,也有一些例外。例如, var 不能很好地与钻石运算符合作运用。钻石 运算符是一个很好的功用,能够在创建泛型实例时从表达式的右侧删去一些冗长的内容:

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>

当然,这能够经过不运用钻石运算符轻松处理:

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 的官方风格攻略和常见问题解答。

由于 var 有许多陷阱,所以它被保存地引进而且只能用于局部变量,这很好,而局部变量的规模一般十分有限。

别的,现已谨慎地引进了, var 不是新关键字,而是保存类型称号。这意味着它只要在用作类型称号时才具有特别含义,其他任何地方 var 仍然是有用的标识符。

现在, var 没有不可变的对应项(例如 valconst )来声明终究变量并运用单个关键字揣度其类型。期望咱们能在将来的版别中得到它,在此之前,咱们能够求助于 final var

答应在接口中运用私有办法(## Allow private methods in interfaces)

可用版别自: JDK 9(Milling Project Coin,即细小改善)

从 Java 8 开端,能够向接口增加默许办法。在 Java 9 中,这些默许办法乃至能够调用私有办法来同享代码,以防需求重用,但又不想揭露功用。

虽然这不是什么大改动,但它是一个逻辑增加,答应在默许办法中收拾代码。

匿名内部类的钻石运算符(Diamond operator for anonymous inner classes)

可用版别自: JDK 9(Milling Project Coin,即细小改善)

Java 7引进了钻石运算符( <> ),经过让编译器揣度结构函数的参数类型来削减冗长:

可是,此功用曾经不适用于匿名内部类。依据项目邮件列表上的评论,这并没有作为原始钻石运算符功用的一部分增加,由于它需求很多的JVM更改。

在 Java 9 中,这个小的毛边被移除,使运算符更遍及适用:

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

答应将有用终究变量用作try-with-resources句子中的资源(## Allow effectively-final variables to be used as resources in try-with-resources statements)

可用版别自: 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!

下划线不再是有用的标识符称号(Underscore is no longer a valid identifier name)

可用版别自: JDK 9(Milling Project Coin,即细小改善)

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

int _ = 10; // Compile error

提高正告(Improved Warnings)

可用版别自: JDK 9

终究,让咱们谈谈与较新 Java 版别中的编译器正告相关的更改。

现在能够运用 @SafeVarargs 注解私有办法,以符号 Type safety: Potential heap pollution via varargs parameter 正告误报。(事实上,此更改是之前评论的 JEP 213:细小改善 的一部分)。在官方文档中阅览有关 Varargs 、泛型和组合这些功用或许发生的潜在指针的更多信息。

相同从 Java 9 开端,编译器在导入已弃用的类型时不会对导入句子发出正告。这些正告没有信息且冗余,由于在实际运用已弃用的成员时一直显现独自的正告。

总结

这篇文章涵盖了自Java 8以来与Java言语相关的改善。密切重视Java渠道十分重要,由于跟着新的快速发布节奏,每六个月发布一个新的Java版别,对渠道和言语进行更改。

相关链接:

  • Java 版别支撑 roadmap: www.oracle.com/java/techno…
  • advancedweb.hu/a-categoriz…
  • www.marcobehler.com/guides/a-gu…