IntelliJ IDEA

Java 19 和 IntelliJ IDEA

Read this post in other languages:

Java 比以往任何时候都更充满活力。 它更短的发布节奏让我们每六个月就可以试用新的语言或平台功能。 IntelliJ IDEA 帮助我们更流畅地发现和使用这些新功能。

在这篇博文中,我将只介绍 Java 19 的语言功能:记录模式switch 模式匹配(第三版预览)。 我特意避开其他 Java 19 功能,例如预览 API 虚拟线程。 IntelliJ IDEA 支持虚拟线程的基本语法高亮显示,团队正努力在调试器和分析器中添加对虚拟线程的支持。

记录模式简化了对记录组件的访问。 比较记录模式和记录析构 – 当实例与记录结构匹配时,将记录组件的值提取到一组变量。 起初,这似乎并不值得一提。 但是,将它与 switch 和密封类的模式匹配等其他语言功能结合使用时,结果会相当惊人。

switch 的模式匹配将模式添加到 switch 语句和 switch 表达式中的 case 标签。 可以与 switch 一起使用的选择器表达式的类型扩展为任意引用值。 另外,case 标签不再限于常量值。 它还有助于将 if-else 语句链替换为 switch,提高代码可读性。 在这篇博文中,我将介绍 switch 模式匹配第三版预览中引入的更改。

我们先从为使用 Java 19 功能对 IntelliJ IDEA 进行配置开始。

IntelliJ IDEA 配置

IntelliJ IDEA 2022.3 中提供了对 Java 19 的支持。 未来的 IntelliJ IDEA 版本将提供更多支持。 要通过 Java 19 使用 switch 的模式匹配,先转到 Project Settings | Project(项目设置 | 项目),将 Project SDK(项目 SDK)设为 19,然后将项目语言级别设置为 19 (Preview) – Record patterns, pattern matching for switch (third preview)

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

在 Modules(模块)选项卡上,确保为模块选择相同的语言级别 – 19 (Preview) – Record patterns, pattern matching for switch (third preview)

 

选择此选项后,可能会出现以下弹出窗口,通知您 IntelliJ IDEA 可能会在后续版本中停止对 Java 预览语言功能的支持。 因为预览功能(暂且)不是永久性的,并且它可能在未来的 Java 版本中发生变化(甚至被移除)。
接下来,我将介绍什么是记录模式、它的好处,并使用实际示例演示。

为什么需要记录模式?

数据是大多数应用程序的核心。 通常,您使用的应用程序可以为您查找数据,或者以帮助您做出决策的方式处理数据。 当然,如果应用程序无法存储、检索或处理其数据,这是不可行的。

在最近的一个 Java 版本(第 16 版)中,记录被添加到 Java 语言中,让开发者可以轻松处理数据。 记录大幅简化了对不可变数据建模的方式。 它们充当数据的透明载体或包装器。 只需使用一行代码,就可以定义一条记录及其组件。

例如,以下单行代码会创建一条新记录 Person,后者可以存储其组件 name 的字符串值和 age 的整数值:

record Person (String name, int age) { }

记录让您不必编写样板代码。 记录会隐式生成其构造函数的默认实现、其组件的访问器方法,以及 toStringequalshashCode 等效用函数方法。 使用记录作为数据的包装器时,您很可能需要将其展开来访问其组件。 例如,对于记录 Person 的实例,您可能想要检查其年龄组件以确定它所代表的人是否有资格投票。 isEligibleToVote 这样的方法可以完成这个行为:

boolean isEligibleToVote(Object obj) {
   if (obj instanceof Person person) {
       return person.age() >= 18;
   }
   return false;
}

前面的示例使用了 instanceof 的模式匹配,它声明了一个模式变量 person,因此您不需要创建局部变量来将 obj 转换为 Person

记录模式更进一步。 它不仅将实例与记录类型 Person 比较,声明了记录组件的变量,因此无需定义局部变量或使用模式变量来访问记录的组件。 这都要归功于编译器知道记录组件的确切数量和类型。

使用记录模式重写前面的方法。 将记录类型与 instanceof 运算符配合使用或在 switch case 标签中使用时,IntelliJ IDEA 可以检测到它并建议使用记录模式:

这是修改后的代码:

boolean isEligibleToVote(Object obj) {
   if (obj instanceof Person(String name, int age)) {
       return age >= 18;
   }
   return false;
}

在前面的代码中,记录模式 Person(String name, int age) 似乎允许使用变量 age 代替 person.age()。 然而,继续阅读,您将了解记录模式能够简化代码的意图,还有助于创建简洁的数据处理代码。

如果您之前没有接触过记录,或者想更多地了解什么是记录,或者 IntelliJ IDEA 如何提供支持,请参考我之前关于记录的博客

命名记录模式

记录模式后面可以跟随一个记录模式变量。 这种情况下,记录模式被称为命名记录模式(虽然没有得到确认,但 Java 20 的记录模式的第二版预览可能会放弃对命名记录模式的支持)。

记录模式还可以为其组件定义模式变量。 使用命名记录模式并尝试使用记录模式变量访问其组件时,IntelliJ IDEA 会提示您为其组件使用模式变量。 您将看到此类代码以黄色背景高亮显示。 您可以使用 Alt+Enter 查看建议,并接受修改代码的建议:

记录模式和 null

我们回顾一下上一部分中的 isEligibleToVote 方法示例。 如果将 null 值传递给以下方法会发生什么:

boolean isEligibleToVote(Object obj) {
   if (obj instanceof Person(String name, int age)) {
       return age >= 18;
   }
   return false;
}

由于 null 不是记录模式 Person(String name, int age) 的实例,instanceof 运算符返回 false,并且模式变量 nameage 未初始化。 这很方便,因为记录模式会处理 null,您不需要定义非 null 检查。

但是,如果组件 name 的值为 null,则模式将被匹配。

嵌套记录模式 – 简洁的代码和明确的意图

将另一条记录定义为其组件的记录相当常见。 例如:

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) { }

如果没有可以检查 null 组件值的记录模式,您将需要几个 null 检查运算来处理记录 PassengerfNamecountryCode 的组件值,如下所示:

boolean checkFirstNameAndCountryCode (Object obj) {
   if (obj != null) {
       if (obj instanceof Passenger passenger) {
           Name name = null;
           Country destination = null;
 
           if (passenger.name() != null) {
               name = passenger.name();
 
               if (passenger.destination() != null) {
                   destination = passenger.destination();
 
                   String fName = name.fName();
                   String countryCode = destination.countryCode();
 
                   if (fName != null && countryCode != null) {
                       return fName.startsWith("Simo") &&
                              countryCode.equals("PRG");
                   }
               }
           }
       }
   }
   return false;
}

同样的行为可以通过嵌套记录模式实现,这也将使代码的意图更加清晰。 如果记录组件 namedestination 为 null,instanceof 检查将失败:

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

如前面的示例代码所示,您可以有选择地添加主记录组件的记录模式。 例如,前面的示例没有为组件 from 使用记录模式。 但它为主记录 Passenger 的记录组件目标使用记录模式。 简而言之,定义记录模式时,您可以控制要提取到模式变量的详细信息。 这一功能非常适合数据处理密集型应用程序。

将 var 与记录模式一起使用

来回顾一下前面示例中的方法 checkFirstNameAndCountryCodeAgain,并将一些模式变量的类型定义为 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 可以显示:

记录模式和泛型

如果记录是泛型,则其记录模式必须使用泛型类型。 例如,假设类 WristWatch 和泛型记录 Gift 的定义如下:

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

您可以使用以下方法解开记录 Gift 的实例。 您可以使用 varWristWatch 作为模式变量 watch 的类型:

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

但是,以下代码将不起作用:

static void cannotUnwap(Gift<object> obj) {
   if (obj instanceof Gift(var s)) {   // won’t compile              
   	//..
   }
}

下一部分使用记录模式和 switch 表达式创建强大的递归方法。

记录模式、switch 表达式和密封类

结合记录模式、switch 表达式和密封类,您可以创建功能强大、简洁且富有表现力的代码来处理数据。 这是密封接口 TwoDimensional 的示例,它由记录 PointLineTriangleSquare 实现:

sealed interface TwoDimensional {}
record Point (int x, int y) implements TwoDimensional { }
record Line    ( Point start, 
                 Point end) implements TwoDimensional { }
record Triangle( Point pointA, 
                 Point pointB, 
                 Point PointC) implements TwoDimensional { }
record Square  ( Point pointA, 
                 Point pointB, 
                 Point PointC, 
                 Point pointD) implements TwoDimensional { }

下面的方法定义了一个递归方法进程,它使用 switch 构造返回二维图形(如 LineTriangleSquare)中所有点的 xy 坐标之和:

static int process(TwoDimensional twoDim) {
   return switch (twoDim) {
       case Point(int x, int y) -> x + y;
       case Line(Point a, Point b) -> process(a) + process(b);
       case Triangle(Point a, Point b, Point c) -> 
                                 process(a) + process(b) + process(c);
       case Square(Point a, Point b, Point c, Point d) -> 
                                 process(a) + process(b) + process(c) + process(d);
   };
}

IntelliJ IDEA 还会在此方法的间距中显示递归调用图标:

Java 20 中的记录模式

由于当前状态为“Proposed to Target”,针对 Java 20 的记录模式的第二版预览计划对其进行增强。 除了支持泛型记录模式实参的类型推断外,它还提到在增强的 for 语句中支持记录模式。 模式匹配必然会改变您的日常编码。

switch 模式匹配 – 第三版预览

在 Java 17 中作为预览语言功能引入的 switch 模式匹配在 Java 19 中处于第三版预览。 如果您对此主题还不熟悉,或者想进一步了解它是什么或 IntelliJ IDEA 如何支持它,请参阅这篇博文,我在其中做出了详细介绍 – 从什么是模式匹配开始,到 instanceof 的模式匹配,然后是 switch 的模式匹配。

在这篇文章中,我将介绍模式匹配从 Java 18 中的第二版预览以来的变化。 我们从在 switch 块中使用 when 子句的第一个变化开始。

在 switch 块中使用 when 替换受保护的模式

想象一组类 – Pollution、AirPollution 和 Deforestation,定义如下:

class Pollution { }
class AirPollution extends Pollution {
   public int getAQI() {
       return 100;
   }
}
class Deforestation {
   public int getTreeDamage() {
       return 300;
   }
}

在之前的 switch 模式匹配预览中,您可以使用受保护的模式向 case 标签添加条件,即使用 && 定义条件。 以下代码使用 switch 的模式匹配和受保护的模式 && airPol.getAQI() > 200,以进一步细化 AirPollution 的实例,此 switch 将为其返回值 500:

public class MyEarth {
   int getDamage(Object obj) {
       return switch (obj) {
           case AirPollution airPol && airPol.getAQI() > 200 -> 500;
           case Deforestation def -> def.getTreeDamage();
           case null, default -> -1;
       };
   }
}

在 switch 模式匹配的第三版预览中,受保护的模式已被替换为 when(如果您使用过 SQL 查询,可以很容易地将它们联系起来)。 我们重写前面的示例:

public class MyEarth {
   int getDamage(Object obj) {
       return switch (obj) {
           case AirPollution airPol when airPol.getAQI() > 200 -> 500;
           case Deforestation def -> def.getTreeDamage();
           case null, default -> -1;
       };
   }
}

 

新检查 – Push down for ‘switch’ expressions

IntelliJ IDEA 添加了一个新检查 – Push down for ‘switch’ expressions,这可以进一步分离计算及其副作用,帮助您修改 switch 表达式。 例如,考虑以下代码:

void printObject(Object obj) {
   if (obj instanceof String s) {
       System.out.println("String: "" + s + """);
   } else if (obj instanceof Collection<?> c) {
       System.out.println("Collection (size = " + c.size() + ")");
   } else {
       System.out.println("Other object: " + obj);
   }
}

第一步,应用 IntelliJ IDEA 的检查 Replace ‘if’ with ‘switch’(将 ‘if’ 替换为 ‘switch’),得到以下代码:

void printObject(Object obj) {
   switch (obj) {
       case String s -> System.out.println("String: "" + s + """);
       case Collection<?> c -> 
            System.out.println("Collection (size = " + c.size() + ")");
       case null, default -> System.out.println("Other object: " + obj);
   }
}

注意,每个 switch 标签都包含对 System.out.println() 的调用。 现在,您可以应用 IntelliJ IDEA 的检查“Push down for ‘switch’ expression”,得到以下代码:

void printObject(Object obj) {
   System.out.println(switch (obj) {
       case String s -> "String: "" + s + """;
       case Collection<?> c -> "Collection (size = " + c.size() + ")";
       case null, default -> "Other object: " + obj;
   });
}

最后,您可能想提取一个变量来分离计算和副作用,生成以下代码:

void printObject(Object obj) {
   final var representation = switch (obj) {
       case String s -> "String: "" + s + """;
       case Collection<?> c -> "Collection (size = " + c.size() + ")";
       case null, default -> "Other object: " + obj;
   };
   System.out.println(representation);
}

下面的 gif 涵盖以上所有步骤:

Java 20 中 switch 的模式匹配

由于当前状态为“Proposed to Target”,针对 Java 20 的switch 模式匹配的第四版预览计划添加更多更改。 除了简化 switch 标签的语法外,它还包括多项其他更改。

预览功能

Java 的新发布周期为六个月,新的语言功能作为预览功能发布。 它们可能会在后续 Java 版本的第二版或第三版预览中重新引入,可能有也可能没有更改。 足够稳定后,它们就会作为标准语言功能添加到 Java 中。

预览语言功能是完整的但不是永久的,这实际上意味着功能已经准备好供开发者使用,只是更细致的细节可能会在未来的 Java 版本中根据开发者反馈发生变化。 与 API 不同,语言功能在未来不能被弃用。 因此,如果您对预览语言功能有任何反馈,请随时在 JDK 邮寄名单中分享(需要免费注册)。

由于这些功能的运作方式,IntelliJ IDEA 仅支持当前 JDK 的预览功能。 预览语言功能可以在不同 Java 版本间变化,直到其被移除或添加为标准语言功能。 使用旧版本 Java SE Platform 中的预览语言功能的代码可能无法在新版本上编译或运行。 例如,Java 12 中的 switch 表达式发布时使用 break 从其分支返回一个值,后来改为 yield。 IntelliJ IDEA 已经不再支持使用 break 从 switch 表达式返回值。

总结

IntelliJ IDEA 不仅致力于支持新的 Java 功能,还将确保现有意图和检查能够与它们一起工作。

IntelliJ IDEA 2022.3 支持记录模式并增强了对 switch 的模式匹配的支持。 更多支持即将推出。 IntelliJ IDEA 完全支持 Java 最近添加的功能,例如密封类和接口、记录、instanceof 的模式匹配,以及文本块。

我们期待用户的反馈。 记得对 IntelliJ IDEA 中这些功能的支持提交反馈。

祝您开发愉快!

 

本博文英文原作者:

image description

Discover more