Java

Java 20 和 IntelliJ IDEA

Read this post in other languages:

本人撰写有关新 Java 版本的文章已有一段时间(自 Java 10 以来),我很喜欢开发者们每六个月就有机会了解和使用新的 Java 功能这种模式。

相比之前的一些版本,Java 20 的新增功能相对较少。 它引入了作用域值作为孵化 API,通过在线程内和跨线程共享不可变数据来支持虚拟线程。 在它的第二个预览版中,记录模式改进了对泛型记录模式的支持,并支持在增强 for 语句中使用记录模式。 在它的第四个预览版中,switch 的模式匹配改进了它在处理详尽 switch、简化 switch 标签和推断泛型记录模式的类型实参时的使用。

在 Java 20 的第二个预览版中,Foreign Function & Memory API 继续改进其功能,使 Java 代码能够与 JVM 外部的代码和数据进行对话。 虚拟线程是一种轻量级线程,它将彻底改变您创建多线程应用程序的方式。虚拟线程受结构化并发支持,在最新 Java 版本的另一个预览版中推出。 Vector API 目前处于第五个预览版阶段,可以帮助您在代码中进行矢量计算。

在这篇博文中,我将介绍 IntelliJ IDEA 在语言功能方面的支持,例如记录模式和 switch 的模式匹配,并将特别介绍最有趣的变化,例如改进了泛型记录模式的类型推断、详尽 switch 语句和表达式,以及增强 for 语句头中的记录模式方面的支持。

我们开始吧。

IntelliJ IDEA 配置

IntelliJ IDEA 2023.1 中提供了对 Java 20 的支持。 未来的 IntelliJ IDEA 版本将提供更多支持。

要使用 Java 20 中诸如记录模式和 switch 的模式匹配等新语言功能,请转到 ProjectSettings | Project(项目设置 | 项目),将 Project SDK(项目 SDK)设置为 20,将 Project language level(项目语言级别)设置为 20 (Preview) – Record patterns (second preview), Pattern Matching for switch (fourth preview)

 

您可以使用系统上已经下载的任意版本 JDK,也可以点击 Edit(编辑),然后选择 Add SDK > Download JDK…(添加 SDK > 下载 JDK…)来下载其他版本。 您可以从供应商列表中选择要下载的 JDK 版本。

在 Modules(模块)标签页上,确保为模块选择相同的语言级别 – 20 (Preview) – Record patterns (second preview), Pattern Matching for switch (fourth preview)

 

选择此选项后,可能会出现以下弹出窗口,通知您 IntelliJ IDEA 可能会在后续版本中停止对 Java 预览语言功能的支持。 因为预览功能不是永久性的,并且可能在未来的 Java 版本中发生变化(甚至被移除)。
我们先快速回顾一下 Java 19 中引入的记录模式。

 

记录模式

对于记录实例,您显然会执行的一项操作就是提取其组件的值以便在应用程序中使用。 这正是记录模式的功能。

首先,让我们快速回顾一下记录模式是什么以及您为什么需要它们。

记录模式快速回顾

 

记录提供了一种简单明了的方式来为您的数据创建透明的载体。 它们使您能够将多个值(也称为组件)汇总到一起。 另一方面,记录模式可以将记录实例分解为其组件,从而使您能够轻松地使用组件的值。

例如,对于以下记录:

record Name       (String fName, String lName) { }
record PhoneNumber(String areaCode, String number) { }
record Country    (String countryCode, String countryName) { }
record Passenger  (Name name,
                  PhoneNumber phoneNumber,
                  Country from,
                  Country destination) { }

以下代码演示了如何将记录模式与 instanceof 运算符搭配使用,并定义简洁明了的代码以将记录实例的组件分解为一组模式变量:

boolean checkFirstNameAndCountryCodeAgain (Object obj) {
  if (obj instanceof Passenger(Name (String fName, var lName),
                               var phoneNumber,
                               Country from,
                               Country (var countryCode, String countryName) )) {
      if (fName != null && countryCode != null) {
          return fName.startsWith("Simo") && countryCode.equals("PRG");
      }
  }
  return false;
}

如果您对记录模式完全陌生,建议您参阅我的博文 Java 19 和 IntelliJ IDEA,其中详细介绍了记录模式 – 它们是什么,为什么以及如何使用它们。 在本文中,我将介绍记录模式从 Java 19 到 Java 20 发生的变化。

Java 20 不再使用最初在 Java 19 中添加的命名记录模式。 Java 20 还改进了对推断泛型记录类型实参的支持,并支持在增强 for 循环语句头中使用记录模式。

 

记录组件类型推断

 

Java 19 支持记录组件的推断 – 您可以使用 var 而不是记录组件的显式类型。 我们回顾一下上一部分中提供的记录模式示例,并注意 var 的用法:

boolean checkFirstNameAndCountryCodeAgain (Object obj) {
  if (obj instanceof Passenger(Name (String fName, var lName),
                               var phoneNumber,
                               Country from,
                               Country (var countryCode, String countryName) )) {
      if (fName != null && countryCode != null) {
          return fName.startsWith("Simo") && countryCode.equals("PRG");
      }
  }
  return false;
}

适用于所有使用 var 定义的局部变量,IntelliJ IDEA 可以显示使用保留关键字 var 定义的变量的显式类型:

 

泛型记录模式

 

Java 20 支持推断泛型记录模式的类型实参。 我们以向好友送礼为例来理解这一内容,您可能想向好友赠送手表或书籍之类的礼物。

假设以下非泛型类 BookWristWatch 以及泛型记录 Gift 定义:

class Book {...}
class WristWatch {...}
record Gift<T>(T t) {}

当您的好友收到您的礼物并试图解开礼物包装时会发生什么? 假设他们调用下方定义的解包方法。 在下面的代码中,方法 unwrap 使用记录模式 Gift<wristwatch> (var watch)。 由于此模式已经使用记录名称 Gift 指定了泛型类型 WristWatch,模式变量 watch 可被推断为类型 WristWatch

void unwrap(Gift<wristwatch> obj) {
   if (obj instanceof Gift<wristwatch> (var watch)) {
       watch.setAlarm(LocalTime.of(10, 25));
   }
}

以下示例在 Java 19 中不起作用,但在 Java 20 中可以:

void unwrapAndRevealSurprise(Gift<WristWatch> obj) {
    if (obj instanceof Gift<WristWatch> (var watch)) {
        System.out.println(watch);
    }
}

void unwrapAndUseGift(Gift<WristWatch> obj) {
    if (obj instanceof Gift(var gift)) {
        gift.setAlarmTime(LocalTime.now());
    }
}

void birthdayGift(Gift<DiamondStudded<WristWatch>> gift) {
    if (gift instanceof Gift<DiamondStudded<WristWatch>>(DiamondStudded(var personalizedGift))) {
        System.out.println(personalizedGift);
    }
}

void performanceBonus(Gift<DiamondStudded<WristWatch>> personalizedGift) {
    if (personalizedGift instanceof Gift(DiamondStudded(var actualGift))) {
        System.out.println("Wrist watch" + actualGift);
    }
}

让我们在下一部分中了解这会如何改变详尽 switch 构造的工作方式。

 

详尽 switch 构造与泛型记录

 

开始之前,我们先来梳理一下基础知识。 下图展示了选择器表达式在 switch 语句或 switch 表达式中引用的内容(传递给 switch 构造的变量或表达式):

switch 语句和 switch 表达式的语法规定,当您尝试将选择器表达式的值与其 case 标签中的类型模式或记录模式相匹配时,它必须是详尽的。 换句话说,选择器表达式必须至少与 case 标签中定义的一个值相匹配。

对于诸如 Object 类型这种子类型数量不确定的类型(即扩展该类型的其他类的数量不固定),您可以定义 default case 标签或 Object 类型本身作为 case 标签之一,从而使其成为详尽 switch 构造。 以下是详尽 switch 表达式和详尽 switch 语句的有效示例:

String exhaustiveSwitchExpression(Object obj) {
   return switch (obj) {
       case String s -> "String";
       case Apple apple -> "Apple";
       default -> "everything else";
   };
}

void exhaustiveSwitchStatement(Object obj) {
   switch (obj) {
       case String s -> System.out.println("String");
       case Apple apple -> System.out.println("Apple");
       case Object object -> System.out.println("everything else");
   };
}

某些类型(如密封类)具有明确的子类型,因此您可能不需要 default 标签即可定义详尽 switch 语句或 switch 表达式。 示例如下:

sealed interface HighProtein permits Egg, Cheese {}
final class Egg implements HighProtein {}
final class Cheese implements HighProtein {}
 
int processHighProtein(HighProtein protein) {
   return switch (protein) {
       case Egg egg -> 2;
       case Cheese cheese -> 10;
   };
}

但是,如果您将接口 HighProtein 定义为常规接口,也就是非密封接口,那么上面的代码就无法编译。

现在,我们来聊一聊如果选择器表达式为泛型记录,它与记录模式匹配时如何定义详尽 case 标签。 以下是一个泛型类 Apple 的示例,它被以下内容扩展:另一个类 HimalayanApple、由类 EggCheese 实现的密封接口 HighProtein,以及接受类型形参并定义该类型的两个组件的泛型记录 Dish

public class Apple {}
public class HimalayanApple extends Apple{}

sealed public interface HighProtein permits Egg, Cheese {}
public final class Egg implements HighProtein {}
public final class Cheese implements HighProtein {}

public record Dish<T> (T x, T y){}

我们来聊一聊您在切换此泛型记录类 Dish 的实例时可以使用或不可以使用的多种组合。

让我们从方法开始,例如 orderAppleDish,它接受 Dish<Apple> appleDish 类型的方法形参并进行切换,进而与记录模式相匹配。 您认为以下代码是否可行:

    int orderAppleDish(Dish<Apple> appleDish) {
        return switch (appleDish) {
            case Dish<Apple>(Apple apple, HimalayanApple himalayanApple) -> 1;
            case Dish<Apple>(HimalayanApple himalayanApple, Apple apple) -> 2;
        };
    }

由于 HimalayanApple 类扩展 Apple 类,需要以下两种组合才能使前面的 switch 表达式成为详尽 switch:

Apple, Apple
HimalayanApple, HimalayanApple

以下 GIF 图展示了 IntelliJ IDEA 如何检测到前面的代码存在问题并帮助您导航代码并更正错误:

以下是上方 gif 中的最终代码,供您参考:

class Apple {}
class HimalayanApple extends Apple{}
record Dish<T> (T ingredient1, T ingredient2) {}

public class FoodOrder {

    int orderAppleDish(Dish<Apple> appleDish) {
        return switch (appleDish) {
            case Dish<Apple>(HimalayanApple apple1, HimalayanApple apple2) -> 4;
            case Dish<Apple>(Apple apple, HimalayanApple himalayanApple) -> 1;
            case Dish<Apple>(HimalayanApple himalayanApple, Apple apple) -> 2;
            case Dish<Apple>(Apple apple1, Apple apple2) -> 3;
        };
    }

}

我们来看另一个示例,它将以下密封接口及其实现与泛型记录 Dish 结合使用:

sealed public interface HighProtein permits Egg, Cheese {}
public final class Egg implements HighProtein {}
public final class Cheese implements HighProtein {}

public record Dish<T> (T x, T y){}

想象一个人在点菜,他传递了一个 Dish 实例,以接口 HighProtein 作为其类型形参。 在这种情况下,以下 switch 表达式为详尽:

以下代码供您参考:

public class FoodOrder {

    int orderHighProteinDish(Dish<HighProtein> proteinDish) {
        return switch (proteinDish) {
            case Dish<HighProtein>(HighProtein protein, Egg egg) -> 1;
            case Dish<HighProtein>(HighProtein protein, Cheese cheese) -> 2;
        };
    }

}

由于 HighProtein 是一个密封接口,以下 switch 构造中的第一个 case 标签涵盖了将 EggCheese 作为第二个值传递给记录 Dish 的可能性。 因此,即使仅定义了三个 case 标签,它也是详尽 switch 表达式:

public class FoodOrder {

    int orderHighProteinDish(Dish<HighProtein> proteinDish) {
        return switch (proteinDish) {
            case Dish<HighProtein>(Egg protein, HighProtein highProtein) -> 1;
            case Dish<HighProtein>(Cheese cheese, Egg egg) -> 2;
            case Dish<HighProtein>(Cheese cheese1, Cheese cheese2) -> 4;
        };
    }

}

在关于详尽 switch 和泛型记录的最后一个示例中,switch 表达式中的以下初始 case 标签集并不详尽,因为它不包含端上一道第一种原料为 HighProtein 的实例以及第二个值为 Egg 的实例的高蛋白菜肴的可能性:

    int orderHighProteinDish(Dish<HighProtein> proteinDish) {
        return switch (proteinDish) {
            case Dish<HighProtein>(Egg protein, Cheese cheese) -> 1;
            case Dish<HighProtein>(Cheese cheese, Egg egg) -> 2;
            case Dish<HighProtein>(HighProtein highProtein, Cheese cheese) -> 3;
        };
    }

让我们在前面的代码中添加另一个 case 标签,使其成为详尽 switch 表达式:

            case Dish<HighProtein>(HighProtein highProtein, Egg egg) -> 10;

 

将记录模式与增强 for 语句结合使用

 

使用 Java 20,您可以在增强 for 循环语句头中使用记录模式。 但是,对于可以在其中指定多个 case 标签的 switch 语句或表达式,您在增强 for 循环语句头中使用的单个记录模式必须与您在 for 循环中遍历的所有值相匹配。 否则,您的代码将抛出运行时异常。

以下示例采用了 PointTriangle 记录以及增强 for 循环,后者在其语句头中使用记录模式来遍历 Triangle 实例的列表:

record Point (int x, int y) { }
record Triangle(Point pointA, Point pointB, Point PointC) { }

long addLowerRightCoordinates(List<Triangle> triangles) {
    long sum = 0;
    for (Triangle(Point a, Point b, Point (int x, int y)) : triangles) {
        sum += x + y;
    }
    return sum;
}

以下是一些不正确的示例:

public class Test {

    sealed interface UpperWithPermit permits PermittedRecord {}
    record PermittedRecord(int x) implements UpperWithPermit {}
    interface Upper {}
    record Record(List<String> x) implements Upper {}

    void test1(List<Upper> lists) {
        for (Record(List<String> x) : lists) {} 
    }

    void test2(List<? super UpperWithPermit> lists) {
        for (PermittedRecord(var x) : lists) {} 
    }

    void test3(List<? super PermittedRecord> lists) {
        for (PermittedRecord(var x) : lists) {} 
    }

    void test4(List lists) {
        for (PermittedRecord(var x) : lists) {} 
    }

    void test5(List<?> lists) {
        for (PermittedRecord(var x) : lists) {} 
    }
}

 

switch 的模式匹配

在 Java 20 中,switch 的模式匹配已进入第四个预览版阶段。 如果您对模式匹配完全陌生,建议您参见此链接来首先了解 instanceof 模式匹配。 如果您尚不熟悉 switch 的模式匹配,请参见此链接

Java 20 中对此功能进行了一些更改。 与枚举类结合使用时,如果详尽 switch 语句或 switch 表达式在运行时找不到匹配的标签,则 switch 的模式匹配现在会抛出 MatchException,而不是抛出 ImcompatibleClassChangeError。 Java 20 中此功能的另一个变化是关于 case 标签中泛型记录模式的类型形参推断。 我已经在这篇博文的 详尽 switch 构造与泛型记录部分中介绍了此功能。

总结

 

IntelliJ IDEA 继续降低开发者使用最新 Java 功能的认知负担。 IntelliJ IDEA 2023.1 支持 Java 20 中添加的对“switch 的模式匹配”和“记录模式”等语言功能的更改。 这些功能中最有趣的变化是支持在增强 for 语句头中使用记录模式,以及改进了泛型记录模式类型实参的类型推断。

快乐编程!

 

本博文英文原作者:

Sue

Mala Gupta

image description

Discover more