1
头图

随着 Java 16 的正式发布(以及长期支持版 17 的即将到来),还在用 Java 8 的小伙伴们可能已经觉得有太多东西要学了。本人将整理从 Java 9 到 Java 16 带来的主要新特性,按照分类陆续展示供大家参考。

本文针对的是语法元素的变化。它们不会影响代码逻辑,但了解它们有助于用更简洁的方式编写代码。本文的代码示例基本上都是来自 JEP 文档中的例子。

JEP 213: Milling Project Coin

发布版本:Java 9

这个标题直译是 “磨掉项目硬币”,意思是一些微小改进,以消除语法中令人不爽的地方。它们包括:

1. 允许在私有方法中使用 @SafeVarargs 注解

在这之前 @SafeVarargs 是不允许添加在非 final 的实例方法上的,也就是说只能添加在:

  • 静态方法
  • 构造方法
  • final 方法

这是不合理的,因为我们知道一个方法上的注解并不能传递给子类,所以我们想让一个方法添加这个注解,又想允许子类覆写这个方法的话,就做不到了。

2. 无需为 try-with-resource 临时声明变量

try 块中可以使用任何之前声明的,实际为 final 的变量。在这之前,自动释放的变量必须在 try 块中声明。例子:

// 旧语法:自动释放的变量必须在 try 块中声明
Reader reader = new StringReader(str);
...
try(Reader reader1 = reader) {
    ...
}

// 新语法:try 块中可以使用任何实际为 final 的变量
final Reader reader = new StringReader(str);
...
try(reader) {
    ...
}

3. 允许匿名类使用 <>(省略的泛型标记)

下面的代码在 Java 8 及以下版本是无法编译通过的,在 Java 9 及以上版本中可以:

public class Sample {
  static class Value<T> {}
  private void go() {
    Value<String> stringValue = new Value<>() {};  // 这里会报错
  }
}

4. 不允许用 "_" 作为变量名

例子:

// 旧语法
Consumer<String> stringConsumer = _ -> {};  // 在 Java 9+ 中将无法编译通过

// 新语法
Consumer<String> stringConsumer = __ -> {}; // 简单的解决办法就是多加一个下划线

5. 接口中允许定义私有方法

没错,现在接口中允许定义私有方法了。例子:

interface I {
    default void go() {
        privateGo();
        privateStaticGo();
    }
    private void privateGo() {
    }
    private static void privateStaticGo() {
    }
}

注意:因为私有方法仅允许接口本身调用,而能够编写调用语句地方又只有默认方法,所以不要想着它们可以直接给实现类用。

JEP 286: Local-Variable Type Inference(本地变量类型推断)

发布版本:Java 10

该特性能够简化本地变量的声明。可推断类型的变量声明语句不需以类型开头,而是用关键字 var 代替。

例子:

ArrayList<String> list = new ArrayList<>();  // 不使用本地变量类型推断
var list = new ArrayList<String>();           // 使用本地变量类型推断

不适用的情况:下面一些本地变量的声明不适用类型推断:

// 下面是错误使用 var 关键字的示例
var x;                // 没有初始化表达式
var x = null;        // 初始化为 null
var x = () -> {};      // 无法确定对应哪种函数式接口
var x = this::f;    // 无法确定对应哪种函数式接口
var x = {1, 2};        // 初始化数组必须严格声明元素类型,如 var x = new int[]{1, 2};

JEP 361: Switch Expressions(Switch表达式)

发布版本:Java 14

该特性能够简化 switch 语句的编写,并避免因为忘记在分支上用 break; 语句结束而造成 BUG。

例子:

// 旧语法
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}

// 新语法
switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}

此外,switch 本身也可以作为一个表达式来输出值。例如:

T result = switch (arg) {
    case L1 -> e1;
    case L2 -> e2;
    default -> e3;
};

// 注意:如果 switch 表达式的一个分支是代码块,则需要用 yield 关键字返回结果。
// 这里不能用 return,因为 return 关键字已经被用于从当前方法返回。
int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result;
    }
};

// 下面是一个错误使用 switch 表达式的例子:
int i = switch (day) {
    case MONDAY -> {
        System.out.println("Monday"); 
        // 错误!该分支需包含 yield 声明
    }
    default -> 1;
};

JEP 378: Text Blocks(文本块)

发布版本:Java 15

该特性使得多行字符串常量的表达方式更加直观。

例子:

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

注意:

  • 文本块的开始和结束都是 """,同一文本块的开始和结束符不能在同一行。
  • 文本块中每行的行首空白字符被视为“缩进”,编译时取缩进大小最小的行作为整个文本块的左边界。
  • 文本块中每行的行尾空白字符会被忽略掉。
  • 不论源代码文件中的换行符是 \r\n 还是 \r\n,编译出来统一以 \n (LF)作为换行符。
  • 如果文本块中包含 """,则需要转义为 \""""\""""\"。例如:

    System.out.println("""
         1 "
         2 ""
         3 ""\"
         4 ""\""
         5 ""\"""
         6 ""\"""\"
         7 ""\"""\""
         8 ""\"""\"""
         9 ""\"""\"""\"
        10 ""\"""\"""\""
        11 ""\"""\"""\"""
        12 ""\"""\"""\"""\"
    """);
  • 如果某行以 \ 结尾,则它与下一行内容之间没有换行符。例如:

    // 下面两个表达式结果相同
    String s = "abc";
    String s = """
        a\
        b\
        c""";
  • 如果想要用空格填充使得每行长度一致,可以用 \s 填充:

    String s = """
        red  \s
        green\s
        blue \s
        """;
  • 如果想在文本块内加入变量,可以直接用 +

    String type = getType();
    String code = """
        public void print(""" + type + """
         o) {
            System.out.println(Objects.toString(o));
        }
        """;

    但是为了代码的可读性和可维护性,还是建议保持文本块自身的连贯。例如:

    String source = """
        public void print(%s object) {
            System.out.println(Objects.toString(object));
        }
        """.formatted(type);

JEP 394: Pattern Matching for instanceof(instanceof 操作的匹配模板)

发布版本:Java 16

该特性简化了 “类型判断+强制类型转换” 逻辑的代码,特别是在编写 equals() 方法的时候。

例子:

// 旧语法
if (obj instanceof String) {
    String s = (String) obj;
    flag = s.contains("jdk");
}

// 新语法
if (obj instanceof String s) {
    flag = s.contains("jdk");
}

// 当后面跟着 && 时,新的变量 s 可以在后面的判断表达式中立即使用
if (obj instanceof String s && s.length() > 5) {
    flag = s.contains("jdk");
}

// 反之则不然
if (obj instanceof String s || s.length() > 5) {    // 错误!
    ...
}

equals() 方法的代码可以得到极大简化。例如:

// 旧语法
public boolean equals(Object o) {
    if (!(o instanceof Point))
        return false;
    Point other = (Point) o;
    return x == other.x
        && y == other.y;
}

// 新语法
public boolean equals(Object o) {
    return (o instanceof Point other)
        && x == other.x
        && y == other.y;
}

注意:使用本特性所定义的模板变量也是一种本地变量。和其他本地变量一样,它可能会覆盖所在类的成员名。下面是一个例子:

class Example2 {
    Point p;

    void test2(Object o) {
        if (o instanceof Point p) {
            // p 指的是本地模板变量
            ...
        } else {
            // p 指的是所在类的成员
            ...
        }
    }
}

上面的例子中,不同 if 分支下的变量 p 指代的是完全不同的对象。因此请谨慎命名,以免造成理解上的混乱。

JEP 395: Records(记录类)

发布版本:Java 16

该特性简化了只读数据传输对象的定义。注意,它并不能代替现有的 java bean 概念,因为 java bean 的属性是可写的。

例子:

// 旧语法
class Point {
    private final int x;
    private final int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    int x() { return x; }
    int y() { return y; }

    public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point other = (Point) o;
        return other.x == x && other.y == y;
    }

    public int hashCode() {
        return Objects.hash(x, y);
    }

    public String toString() {
        return String.format("Point[x=%d, y=%d]", x, y);
    }
}

// 新语法
record Point(int x, int y) { }

// 如果要对参数进行校验和处理
record Point(int x, int y) {
    Point {
        if (x < 0) throw new IllegalArgumentException("x不能为负数");
        y = Math.abs(y);
    }
}

// 记录类的使用
Point p = new Point(1, 1);
System.out.println("x=" + p.x() + ", y=" + p.y());

使用限制:

  • 记录类不可从其他类继承,因为所有的记录类都是 java.lang.Record 的子类。一个记录类甚至不可以是另一个记录类的子类。
  • 记录类不可以是抽象的,也不可以有子类。
  • 记录类的成员都是 final 的,即不可重新赋值。
  • 记录类不可以再定义额外的成员,也不可以再定义额外的构造方法。

除此之外,记录类和普通 class 一样:

  • 使用 new 关键字来创建实例;
  • 可以实现其他接口;
  • 可以是外部的,可以是内部的,也可以包含泛型;
  • 可以定义静态方法、静态成员和非静态方法;

本地记录类:你可以在一个方法内声明本地记录类,以帮助实现业务逻辑。例如:

List<Merchant> findTopMerchants(List<Merchant> merchants, int month) {
    // 声明本地记录类
    record MerchantSales(Merchant merchant, double sales) {}

    return merchants.stream()
        .map(merchant -> new MerchantSales(merchant, computeSales(merchant, month)))
        .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
        .map(MerchantSales::merchant)
        .collect(toList());
}

注意,内部记录类(在类中声明)和本地记录类(在方法中声明)都是静态的。

代码兼容性问题:

因为默认包下新增了 java.lang.Record 类,所以任何其他包下的 Record 类的使用都会受到影响。例如你有一个类叫做 a.b.Record,那么如果代码中使用 import a.b.*;,这种情况下是无法使用你的 Record 类的。你必须改为 import a.b.Record; 才能编译通过。


捏造的信仰
2.8k 声望272 粉丝

Java 开发人员