IntelliJ IDEA
IntelliJ IDEA – the Leading Java and Kotlin IDE, by JetBrains
Java 23 和 IntelliJ IDEA
Java 23 包含全新和更新的 Java 语言功能、核心 API 以及 JVM,同时适合新的 Java 开发者和高级开发者。 IntelliJ IDEA 2024.2 现已支持 Java 23 功能。
跟上 Java 新版本的发布节奏可能很难 – 更改是什么、为什么要更改以及如何使用全新和更新的功能。 在这篇博文中,我将介绍 Java 23 的一些全新和更新功能 – 它们为您解决的痛点、语法和语义,以及 IntelliJ IDEA 如何帮助您使用它们。
我将重点介绍 Java 23 功能,例如在模式匹配中包含基元数据类型、在代码库中导入模块的能力、在文档注释中使用 Markdown 的可能性、隐式声明的类与实例 main 方法,以及灵活的构造函数体。 如果您感兴趣,请访问此链接来查看 Java 23 其他功能的列表。
在深入了解 Java 23 功能的详细信息之前,我们先快速配置 IntelliJ IDEA。
IntelliJ IDEA 配置
最近发布的 IntelliJ IDEA 2024.2 已经支持 Java 23。
在您的 Project Settings(项目设置)中,将 SDK 设置为 Java 23。 您可以将 IntelliJ IDEA 配置为使用下载版本的 JDK 23,也可以选择从供应商列表中下载而无需退出 IDE。 对于语言级别,选择“23(Preview) – Primitive types in patterns, implicitly declared classes, etc.”(23(预览) – 模式中的基元类型,隐式声明的类等),如下面的屏幕截图中所示:
要使用 Markdown 文档注释等正式功能,请将语言级别更改为“23 – Markdown documentation comments”(23 – Markdown 文档注释),如以下设置屏幕截图中所示:
在掌握 IntelliJ IDEA 的配置后,我们来深入学习新功能。
模式、instanceof 和 switch 中的基元类型(预览功能)
想象一下,您需要编写一个条件构造,它基于 long
变量的值是否匹配几个字面量值或落在某个值范围内来执行代码。
您会怎么做? 在此之前,您只能使用 if/else 构造执行此操作。 但是,借助 Java 23 中的模式、instanceof 和 switch 中的基元类型(一种预览语言功能),您可以使用更具表达性且易于阅读的 switch 构造来编写此功能,同时在 case 标签中使用 long 值。
将基元类型添加到模式匹配中意味着什么?
在 Java 23 之前,switch 构造(语句和表达式)仅能处理引用变量和一些基元数据类型,例如 int
、byte
、short
(有约束)。 此外,instanceof
运算符不能用于任何基元数据类型。
借助 Java 23,您可以将所有基元数据类型(包括 boolean
、long
、float
和 double
)与 Switch 构造中的模式匹配和
instanceof
运算符一起使用。 这适用于在嵌套和顶层上下文中使用。
为什么您应该关注此功能? 一项功能的价值取决于它影响的代码库有多大以及使用频率。 由于条件语句是编程的基础之一,您可以预期在代码库中大量使用此功能。 即使您可能不会编写代码,您也会阅读由其他人编写的代码。
我们通过一个示例来理解此功能。
一个示例(将较长的 if-else 语句替换为 switch 表达式)
想象一个方法,例如 getHTTPCodeDesc(int)
,它接受 HTTP 服务器代码作为 int 值,并返回相应的字符串表示,同时将其与某个字面量值或值范围进行比较。
这段代码似乎没有明显的问题 – 我们都编写过或阅读过类似的代码。 不过,处理 if-else 构造的流程可能需要更长的时间,因为它们可能定义不仅限于一个变量的复杂条件。 我们简单地假设方法 getHTTPCodeDesc()
的定义如下:
public String getHTTPCodeDesc(int code) { if (code == 100) { return "Continue"; } else if (code == 200) { return "OK"; } else if (code == 301) { return "Moved permanently"; } else if (code == 302) { return "Found"; } else if (code == 400) { return "Bad request"; } else if (code == 500) { return "Internal server error"; } else if (code == 502) { return "Bad gateway"; } else if (code > 100 && code 200 && code 302 && code 400 && code 502 && code < 600) { return "Server error"; } else { return "Unknown error"; } }
在 Java 23 中,可以使用 switch 表达式(使用基元作为模式)替换上述代码,具体如下所示:
public String getHTTPCodeDesc(int code) { return switch(code) { case 100 -> "Continue"; case 200 -> "OK"; case 301 -> "Moved Permanently"; case 302 -> "Found"; case 400 -> "Bad Request"; case 500 -> "Internal Server Error"; case 502 -> "Bad Gateway"; case int i when i > 100 && i "Informational"; case int i when i > 200 && i "Successful"; case int i when i > 302 && i "Redirection"; case int i when i > 400 && i "Client Error"; case int i when i > 502 && i "Server Error"; default -> "Unknown error"; }; }
上述代码的第一个明显好处是,相比于使用 if-else 语句的版本,它更易于阅读和理解。 您可以一目了然地理解代码逻辑。
另一个不那么明显的好处(但也很重要)是上述代码如何减少您的认知负担。 认知负担指的是您在工作记忆中拥有的信息量(工作记忆空间有限)。 如果您试图用与您的最终目标无直接关联的指令或信息来使您的工作记忆超载,那么您的工作效率会降低。 易于阅读的代码段可以帮助您将注意力集中到代码的其他领域。 当我们探讨如何经常从中受益时,这些小的胜利会起到很大作用。
现在,我们来聊聊简单的部分,我是指语法。 正如您所注意到的,利用带保护的 (when i > 100 && i < 200
) 类型模式 (int i
),带保护的 case 标签既可以有常量值(例如 100、200 等),也可以有使用模式匹配指定的值范围。 您也可以定义一个 default
子句。
在上述代码中,方法 getHTTPCodeDesc()
使用 switch 表达式返回一个值。 无论您传递给方法形参(即 code
)的值如何,该方法都必须返回一个值。 换句话说,switch 表达式必须为详尽。 如果不是,IntelliJ IDEA 可以检测到并提供添加 default
子句的选项,如以下 GIF 中所示:
上述代码在 int 类型的变量上进行切换。 类似地,您可以在所有其他基元类型(例如 long、double、float 等)的变量上切换。
您是第一次使用模式匹配还是首次了解 switch 构造的最近更改?
如果您对模式匹配完全不熟悉,请查看我的博文 Java 17 和 IntelliJ IDEA 中关于基础知识的部分。 如果您对如何将模式匹配与 switch 构造配合使用感兴趣,我有关于此主题的另一篇详细博文:Evolution of the Switch Construct in Java—Why Should you Care?。这篇文章介绍了 switch 构造如何使用模式匹配来检查引用值是否符合不同模式,并根据变量的类型及其特性有条件地执行代码。
将模式匹配与布尔值配合使用
我们经常阅读和编写根据 boolean
变量的值是 true
还是 false
来返回值的代码。 例如,在以下代码中,方法 calculateDiscount
根据您传递给方法形参 isPremiumMember
的值是 true
还是 false
来计算并返回折扣值:
public class DiscountCalculator { private static final int PREMIUM_DISCOUNT_PERCENTAGE = 20; private static final int REGULAR_DISCOUNT_PERCENTAGE = 5; public int calculateDiscount(boolean isPremiumMember, int totalAmount) { int discount; if (isPremiumMember) { // Calculate discount for premium members discount = (totalAmount * PREMIUM_DISCOUNT_PERCENTAGE) / 100; } else { // Calculate discount for regular members discount = (totalAmount * REGULAR_DISCOUNT_PERCENTAGE) / 100; } return discount; } }
除了 if-else 构造外,您还可以切换布尔方法形参 isPremiumMember
的值,而无需定义局部变量 discount
,具体如下所示:
public int calculateDiscount(boolean isPremiumMember, int totalAmount) { return switch (isPremiumMember) { case true -> (totalAmount * PREMIUM_DISCOUNT_PERCENTAGE) / 100; case false -> (totalAmount * REGULAR_DISCOUNT_PERCENTAGE) / 100; }; }
由于方法 calculateDiscount()
中的 switch 表达式为详尽,如果您缺少 true 或 false 值中的任何一个,IntelliJ IDEA 都可以检测到并建议您插入默认或缺少的 true/false case,如以下 gif 中所示:
将基元类型与 instanceof 运算符配合使用
在 Java 23 之前,任何基元类型都不能与 instanceof 运算符结合使用。
instanceof 运算符可用于检查变量的类型,并有条件地执行代码。 利用 instanceof 的模式匹配,如果待比较变量的类型与类型模式匹配,您还可以声明和初始化模式变量,而无需显式转换。 instanceof 变量还可以使用保护模式。
随着将基元类型添加到 instanceof 运算符,您可以定义如下代码:
import static java.io.IO.println; void main() { var weight = 68; if (weight instanceof byte byteWeight && byteWeight <= 70) { println("Weight less than 70"); } }
请注意,Java 23 中的“隐式声明的类与实例 main 方法”功能定义了具有 static 方法的 java.io.IO
,允许您导入并使用 println()
将值输出到控制台,而不是使用 System.out.println()
执行此操作。
如果您计划检查更多的类型和条件,可以使用带有保护的 switch 构造,具体如下所示:
var weight = 68; switch (weight) { case byte b when b println("byte: less than 70"); case int i when i println("int: less than 7000"); case long l when l >= 7_000 && l println("long range: 7_000 - 70_000"); case double d -> println("double"); }
数据类型之间的安全转换
当您将模式匹配与基元数据类型配合使用时,Java 编译器确保没有信息丢失。 在以下示例中,instanceof 运算符可以在检测到安全时在 double 和 byte 数据类型之间转换:
double height = 67; if (height instanceof byte byteHeight) System.out.println(byteHeight);
如果没有 instanceof 运算符,类似的代码将无法执行。 以下代码不会编译:
double height = 67; final byte convertToByte = height;
IntelliJ IDEA 中的稳健数据流分析
IntelliJ IDEA 为 switch 语句中的基元类型提供了有力支持,还集成了复杂的数据流分析,可以帮助开发者避免常见问题。 如以下示例中所示,IntelliJ IDEA 中的数据流分析可以确定第二个 case 标签,即 case int _ 和其余的 case 标签不可达(因为如果变量 weight 的值大于 70,代码会退出方法)。 IntelliJ IDEA 会就不可到达的代码发出警告并提供适当的建议:
记录和基元数据类型组件
想象一下,您定义了如下记录 Person:
record Person(String name, double weight) {}
在此之前,您可以将其分解为精确的数据类型。 不过,借助模式匹配中的基元类型,您可以使用其他兼容的数据类型,例如 int、long 等。 示例如下:
Person person = new Person("Java", 672); switch (person) { case Person(String name, byte weight) -> println("byte:" + weight); case Person(String name, int weight) -> println("int:" + weight); case Person(String name, double weight) -> println("double:" + weight); case Person(String name, long weight) -> println("long:" + weight); default -> throw new IllegalStateException("Unexpected value: " + person); }
您也可以将它与 instanceof 运算符配合使用,具体如下所示:
if (person instanceof Person(String name, byte weight)) { System.out.println("Instanceof : " + weight); }
IntelliJ IDEA 支持此功能的后续计划
对模式匹配中基元数据类型的更多支持正在开发,包括能够在所有基元数据类型上使用后缀运算符 .switch 来开始编写 switch 构造。 此外,也在开发的支持是将现有的 if-else 语句转换为使用基元数据类型的 switch 构造 – 这可让您更轻松地采用此新功能。
Markdown 文档注释(正式功能)
在此之前,Java 文档注释需要使用 HTML 和 JavaDoc 标记进行编写。 借助此新功能,即文档注释,您还可以使用 Markdown 编写 JavaDoc 注释。 这是 Java 23 中的一项正式功能。
您是否想知道此更改的原因是什么?
原因之一是 HTML 不再是新开发者的流行选择(尽管在 Java 于 20 世纪 90 年代后期推出时,它是一个不错的选择)。 手动编写 HTML 并不简单。 此外,Markdown 更易于编写、阅读,并且可以轻松转换为 HTML。 许多开发者都在使用 Markdown 来记录他们的代码、撰写书籍、文章、博文,以及生成网站页面等。
让我们看看如何在您的源代码文件中使用 Markdown 编写 Javadoc 注释,以及 IntelliJ IDEA 如何提供帮助。
一个示例
Markdown 文档注释以 /// 开头。
使用三个斜杠的选择非常有趣。 Oracle 中此功能的负责人 Jonathan Gibbons 分享说,更改 Java 语言中功能的语法并非易事。 多行注释以 /* 开头,以 */ 结尾。 这样便很难在文档中包括可能包含 */ 的任何代码。 因此,Markdown 文档注释以 /// 开头。
此外,也支持编写文档注释的旧方法,即 HTML 和 JavaDoc 标记。 Jonathan 还提到,无法将 JavaDoc 标记转换为 Markdown 等效标记。 因此,开发者可以使用 Markdown 注释和 JavaDoc 标记的组合,以获得两全其美的结果。
下面是一个使用 Markdown 和 Javadoc 标记记录方法的示例:
/// /// **Getting started and having fun learning Java :)** /// /// Prints a pattern of the letter 'P' using the specified character. /// /// @param size the size of the pattern /// @param charToPrint the character to use for the pattern /// private void printP(int size, char charToPrint) { for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { if (j == 0 || (i == 0 || i == size / 2) && j < size - 1 || (j == size - 1 && i <= size / 2)) { System.out.print(charToPrint + " "); } else { System.out.print(" "); } } System.out.println(); } }
IntelliJ IDEA 可以帮助您在编辑器中切换视图,查看文档注释对任何阅读 Java 文档注释的人如何显示。
在 IntelliJ IDEA 中查看 Java 文档注释
JEP“Markdown 文档注释”的负责人 Jonathan Gibbons 强调,所有开发者都需要检查他们添加到代码库中的 Java 文档注释是否正确。
IntelliJ IDEA 提供了“阅读器模式”,让您能够在源代码中查看 Java 文档注释。 您还可以使用 Toggle Rendered View(切换渲染视图)功能在 Java 文档注释代码和查看方式之间切换,如以下 gif 中所示:
在 IntelliJ IDEA 中编写 Markdown 文档注释
IntelliJ IDEA 可以检测到您正在使用 Markdown 记录方法。 当您以 /// 开始并按 Enter 时,它也会在下一行添加 ///,如以下 GIF 中所示:
您是否应该将现有的文档注释转换为使用 Markdown?
下面的 gif 显示了在 IntelliJ IDEA 中使用 Markdown 编写的 hashCode 方法的文档。 利用 Toggle Rendered View(切换渲染视图),您可以轻松在阅读器视图中查看文档,这更容易阅读和理解。
理想情况下,除非您的开发者或 API 用户在使用不提供替代视图的工具(如 IntelliJ IDEA)查看您的(API、库、框架)代码库时遇到重大可读性问题,否则我不鼓励您将现有文档转换为使用 Markdown。
模块导入声明(预览功能)
借助此功能,您可以在类或接口中使用单条语句导入模块库(如 Java API),例如 import module java.base,这条语句将导入由模块 java.base 导出的所有软件包。
您不需要单独的 import 语句来导入类中的软件包,如 java.util,或者说 java.io,因为它们由 java.base 导出。
一个示例
以下代码示例使用了来自 java.io 和 java.util 软件包的类。 通过包含语句 ‘import java.base’,您不需要单独导入 java.io 和 java.util 软件包,因为它们由模块 java.base 导出:
import module java.base; public class ImportModuleHelloWorld { public static void main(String[] args) { try { InputStream inputStream = new BufferedInputStream( new FileInputStream( new File("abc.txt"))); } catch (FileNotFoundException e) { throw new RuntimeException(e); } Map list = new HashMap(); } }
不过,如果您从顶部删除 import module 语句,则 IntelliJ IDEA 将从 java.io 和 java.util 软件包中导入单个类和接口。 此操作在以下 GIF 中显示:
哪些软件包是由模块 java.base(或其他模块)导出的?
当您使用 IntelliJ IDEA 时,回答这个问题很简单。 点击编辑器中的模块名称或使用相关快捷键(Go to Declaration(转到声明) 或 Go to Usages(转到用法)),您可以查看此模块的定义以找出由此模块导出的所有模块。 此操作在以下 gif 中显示:
隐式声明的类与实例 main 方法(第三预览版)
此功能在 Java 21 中作为预览语言功能引入,在 Java 23 中为第三个预览版。
它将改变新 Java 开发者学习 Java 的方式。 它简化了学生学习基础知识(例如变量赋值、序列、条件和迭代)时的初始步骤。 学生不再需要声明显式类来开发代码,或使用此签名编写其 main() 方法 public static void main(String [])。 借助此功能,可以隐式声明类,以及使用更短的关键字列表创建 main() 方法。
如果您不熟悉此功能,我强烈建议阅读详细介绍此功能的文章 ‘HelloWorld’ and ‘main()’ meet minimalistic。 在本节中,我将包括 Java 23 中对此功能的补充。
使用模块 java.base 导出的软件包而不显式导入它们
只需在隐式类的顶部包含一条 import 语句,即 import module java.base,您就能自动导入 java.base 导出的软件包。 这意味着您的隐式类将不再需要这些类或软件包的单独 import 语句,如以下 gif 中所示:
简化与控制台交互的代码的编写
与控制台交互 – 打印消息或使用消息是新 Java 开发者经常使用的操作之一。 在此功能中,这一点得到了进一步简化。
您的隐式声明的类可以使用方法 println() 和 print() 将消息输出到控制台,并使用方法 readln() 读取字符串消息,而无需在您的类中显式导入它们。 所有这些方法都在一个由隐式类隐式导入的新顶层类 java.io.IO 中声明。
花点时间,看看您需要如何在常规类中显式导入它们,如以下 GIF 中所示:
以下 gif 显示了当您在隐式类中使用前述相同方法时不需要显式导入:
灵活的构造函数体(第二预览版)
这是 JDK 22 中一项功能的第二预览版,之前称为“super() 之前的语句”。 除了功能名称的更改外,这项功能还有一个重要变化。 现在,可以在调用 super() 或 this() 之前初始化字段。
当超类从其构造函数中调用方法,而您想要在子类中重写此方法并在此方法内部访问子类中的字段时,这非常有用。 此前,从超类构造函数中调用方法时,子类字段不会初始化。 现在,可以初始化字段并防止意外。 下面是展示此功能的示例代码:
abstract class Action { public Action() { System.out.println("performing " + getText()); } public abstract String getText(); } class DoubleAction extends Action { private final String text; private DoubleAction(String text) { this.text = text; // this did not compile before Java 23 with preview features enabled. super(); } @Override public String getText() { return text + text; } }
如果您不熟悉此功能,敬请阅读详细介绍此功能的博文 https://blog.jetbrains.com/idea/2024/02/constructor-makeover-in-java-22/,其中探讨了此功能的使用原因和使用方式。
预览功能
在这篇博文中介绍的功能中,“模式、instanceof 和 switch 中的基元类型”、“模块导入声明”、“隐式声明的类和实例 main 方法”和“灵活的构造函数体”在 Java 23 中是预览语言功能。 Java 的新发布周期为六个月,新语言功能作为预览功能发布。 它们可能会在后续 Java 版本的第二个或其他预览版中重新引入,可能有也可能没有更改。 足够稳定后,它们就会作为标准语言功能添加到 Java 中。
预览语言功能是完整的但不是永久的,这实际上意味着功能已经准备好供开发者使用,只是更细致的细节可能会在未来的 Java 版本中根据开发者反馈发生变化。 与 API 不同,语言功能在未来不能被弃用。 因此,如果您对预览语言功能有任何反馈,请随时在 JDK 邮件名单(需要免费注册)中分享。
由于这些功能的运作方式,IntelliJ IDEA 仅支持当前 JDK 的预览功能。 预览语言功能在不同 Java 版本间可能发生变化,直到它们被移除或添加为标准语言功能。 使用旧版本 Java SE Platform 中的预览语言功能的代码可能无法在新版本上编译或运行。
总结
在这篇博文中,我介绍了五个 Java 23 功能 – 模式、instanceof 和 switch 中的基元类型、Markdown 文档注释、模块导入声明、隐式声明的类与实例 main 方法,以及灵活的构造函数体。
查看这些新功能,了解它们如何帮助您改进应用程序。
快乐编程!
本博文英文原作者: