IntelliJ IDEA
IntelliJ IDEA – the Leading Java and Kotlin IDE, by JetBrains
Java 22 和 IntelliJ IDEA
Java 22 现已正式发布,IntelliJ IDEA 2024.1 全面支持该版本,您可以使用其中的新功能!
从新手开发者到 Java 专家,从寻求性能和安全功能的大型组织到尖端技术爱好者,从 Java 语言的新特性到 JVM 平台的改进,Java 22 可以满足各种群体的各类需求。
这些 Java 功能在一个又一个版本发布后良好配合,创造出更多可能,深度助力开发者创建解决现有痛点、更强劲且更安全的应用程序。
本博文并未覆盖所有 Java 22 功能。 如果您有兴趣,我建议查看此链接,详细了解 Java 22 中的新增内容和变化,包括 bug。
在这篇博文中,我将介绍 IntelliJ IDEA 如何帮助您上手、启动和运行 Java 22 功能,例如字符串模板、隐式声明的类与实例 main 方法、super() 之前的语句,以及未命名变量和模式。
过去一个月,我发布了多篇博文分别详细介绍了每个主题。 如果您不熟悉这些主题,我强烈建议阅读这些详细的博文(我在这篇博文的相应小节中附上了链接)。 在这篇博文中,我将提及先前博文中的一些部分,特别是 IntelliJ IDEA 如何支持这些功能。 首先,我们将 IntelliJ IDEA 配置为使用 Java 22 功能。
IntelliJ IDEA 配置
配置完成,我们从我最喜欢的新功能之一“字符串模板”开始。
字符串模板(预览语言功能)
IntelliJ IDEA 能够高亮显示可被替换为字符串模板的代码
假设您定义了以下代码,记录一条使用串联运算符组合字符串文字和变量值的消息:
public void processOrder(int orderId, String product, int qty, LocalDate orderDate) { if (quantity <= 0) { String errorMessage = "Invalid order quantity: " + qty + " for product " + product + ", order ID " + orderId; logger.error(errorMessage); return; } //.. Remaining code }
"
) 和 +
运算符,代码不太容易阅读或理解,如果向其中添加更多文字或变量值,情况会变得更糟。StringBuilder.append()
、String.format()
或 String.formatted()
方法,或使用 MessageFormat
类替换上面的代码(如我关于此主题的详细博文所示),但这些方法都有各自的问题。字符串模板和 IntelliJ IDEA 中的嵌入式表达式
将字符串模板与文本块结合使用
\{
时,IntelliJ IDEA 将添加 }
。当您开始输入变量名称 countryName
时,它会显示该上下文中的可用变量:语言注入和字符串模板
@Language
注解来临时或永久利用此功能,如下所示:您可以查看此链接,详细了解 IntelliJ IDEA 中注入语言或引用的好处和用法。
预定义模板处理器
自定义模板处理器
让我们使用先前博文中未涉及的自定义字符串模板。假设您想要创建一个记录实例,例如 WeatherData,存储上一节中使用的 JSON 的详细信息。
假设您定义以下记录来存储由上一节中的 JSON 表示的天气数据:
public record WeatherData (String cod, City city) { } public record City (int id, String name, String country, Coord coord) {} public record Coord (double lat, double lon) { }
您可以创建一个方法返回自定义字符串模板来处理内插字符串、接受类名(本例中为 WeatherData)并返回其实例:
public <T> StringTemplate.Processor<T, RuntimeException> getJSONProcessorFor(Class<T> classType) { return StringTemplate.Processor.of( (StringTemplate st) -> { List<Object> sanitizedLst = new ArrayList<>(); for (Object templateExpression : st.values()) { switch (templateExpression) { case String str -> sanitizeStr(str, sanitizedLst); case Number _, Boolean _ -> sanitizedLst.add(templateExpression); case null -> sanitizedLst.add(""); default -> throw new IllegalArgumentException("Invalid value"); } } String jsonSource = StringTemplate.interpolate(st.fragments(), sanitizedLst); System.out.println(jsonSource); try { ObjectMapper objectMapper = new ObjectMapper(); return objectMapper.readValue(jsonSource, classType); } catch (JsonProcessingException e) { throw new RuntimeException(e); } }); }
根据应用程序的逻辑,您可能希望对在通过模板表达式内插的 JSON 值中遇到的特殊字符进行转义、将其删除或抛出错误,如下所示(以下方法选择转义特殊字符并将其作为 JSON 值的一部分包含在内):
private void sanitizeStr(String str, List<Object> sanitizedLst) { String sanitizedStr = str.replace("\\", "\\\\") .replace("\"", "\\\"") .replace("/", "\\/") .replace("\b", "\\b") .replace("\f", "\\f") .replace("\n", "\\n") .replace("\r", "\\r") .replace("\t", "\\t"); sanitizedLst.add("\"" + sanitizedStr + "\""); }
您可以初始化并使用此自定义 JSON 模板处理器,如下所示。 可以看到,文本块和字符串模板的组合使解决方案相当优雅简洁。 JSON 易于阅读、编写和理解(得益于文本块)。 模板表达式使非常量且将由变量注入的部分清晰明了。 最后,自定义模板处理器 WEATHER_JSON 将确保生成的 JSON 根据您定义的逻辑得到验证,并返回 WeatherData 的实例(听起来是不是很神奇?) :
StringTemplate.Processor<WeatherData, RuntimeException> WEATHER_JSON = getJSONProcessorFor(WeatherData.class); String cod = null; String name = "Amazing City"; Double lat = 55.7522; WeatherData weatherData = WEATHER_JSON.""" { "cod": \{cod}, "city": { "id": 524901,,, "name": \{name}, "country": "XYZ", "coord": { "lat": \{lat}, "lon": 37.6156 } } }""";
FMT
等预定义字符串模板为附近的文具店生成格式正确的收据,或者将 :) 或 :( 等组合编码和解码为 🙂 或 ☹️ 表情符号。隐式声明的类与实例 main 方法(预览语言功能)
public static void main(String [])
编写其 main()
方法。借助此功能,可以隐式声明类,以及使用更短的关键字列表创建 main()
方法。Java 21 之前和之后的 ‘HelloWorld’ 类
HelloWorld
,使用特定关键字列表定义 main()
方法,以将任何文本(例如 ‘Hello World’)打印到控制台,如下所示:public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World"); } }
main()
的较短签名):void main() { System.out.println("Hello World"); }
编译和执行代码
编写完代码后,下一步就是执行。
javac
和 java
命令编译和执行代码。C:\code\MyHelloWorldProject\javac HelloWorld.java C:\code\MyHelloWorldProject\java HelloWorld
C:\code\MyHelloWorldProject\java HelloWorld.java
但是,由于实例 main 方法和隐式类为预览语言功能,您应该使用这些命令添加标记 --enable-preview
和 --source 22
,如下所示:
C:\code\MyHelloWorldProject\java --enable-preview --source 22 HelloWorld.java
public class HelloWorld { }
。由于我们一开始就试图避免不必要的关键字,我建议创建一个不包含任何代码的新文件。除了将消息打印到控制台之外,main() 还能做什么?
main()
方法就能得到的结果:main()
方法中,以解决实际问题的技能奠定良好的编程基础。使用运行命令或图标在 IntelliJ IDEA 中运行和执行代码,新程序员在入门时又省去了一个步骤。将隐式类改为常规类
使用 main() 方法创建源代码文件但没有类声明时会发生什么?
隐式类中 main 方法的变体
众所周知,方法可以被重载。 这是否意味着隐式类可以定义多个 main 方法? 如果答案是肯定的,那么哪一个有资格作为主要的 main 方法?这是一个有趣的问题。 首先,请注意,您不能定义具有相同签名(即具有相同方法形参)的 static 和非 static main 方法。 以下方法被视为隐式类中有效的 main() 方法:
public static void main(String args[]) {} public void main(String args[]) {} public static void main() {} static void main() {} public void main() {} void main() {}
如果没有检测到有效的 main 方法,IntelliJ IDEA 可以为您添加一个,如以下 gif 所示:
教育工作者可以使用此功能向学生逐步介绍其他概念
super() 之前的语句 – 预览语言功能
super()
之前执行语句,虽然这对于验证传递给基类构造函数的值很重要。一种流行变通方法是创建 static 方法验证值,然后对 super()
的实参调用这些方法。尽管这种方式效果很好,但它可能会使代码看起来很复杂。Java 22 中的预览语言功能,super() 之前的语句,将改变这种情况。super()
之前执行验证实参的代码条款与条件仍然适用,例如,在 super()
执行完成之前不访问派生类的实例成员。示例 – 在派生类构造函数中验证传递给 super() 的值
IndustryElement
来扩展类 Element
,其定义如下:public class Element { int atomicNumber; Color color; public Element(int atomicNumber, Color color) { if (color == null) throw new IllegalArgumentException("color is null"); this.atomicNumber = atomicNumber; this.color = color; } // rest of the code }
Element
的构造函数未检查 atomicNumber
是否在 1-118 范围内(所有已知元素的原子序数在 1 到 118 之间)。通常,基类的源代码不可访问或开放修改。您将如何在 IndustryElement
类的构造函数中验证传递给 atomicNumber
的值?super()
之前执行任何语句。开发者通过定义和调用 static 方法找出一个变通方法(static 方法属于类而不属于实例,并且可以在类的任何实例存在之前执行):public class IndustryElement extends Element{ private static final int MIN_ATOMIC_NUMBER = 1; private static final int MAX_ATOMIC_NUMBER = 118; public IndustryElement(int atomicNumber, Color color) { super(checkRange(atomicNumber, MIN_ATOMIC_NUMBER , MAX_ATOMIC_NUMBER), color); } private static int checkRange(int value, int lowerBound, int upperBound) { if (value < lowerBound || value > upperBound) throw new IllegalArgumentException("Atomic number out of range"); return value; } }
从 Java 22 开始,您可以在派生类的构造函数中内联 static 方法的内容,如以下 gif 所示:
这是生成的代码:
public class IndustryElement extends Element{ private static final int MIN_ATOMIC_NUMBER = 1; private static final int MAX_ATOMIC_NUMBER = 118; public IndustryElement(int atomicNumber, Color color) { if (atomicNumber < MIN_ATOMIC_NUMBER || atomicNumber > MAX_ATOMIC_NUMBER) throw new IllegalArgumentException("Atomic number out of range"); super(atomicNumber, color); } }
您还会在哪里使用此功能?
它在后台是如何运作的?
super()
执行之前,此功能仍然不允许使用派生类实例的成员。IndustryElement
类的构造函数在修改前后的指令集 – 一个可以在 super()
之前执行语句,另一个则不可以。javap -c IndustryElement.class
super()
之前不明确执行语句,但调用 static 方法验证原子序数的范围:Element
)都会在执行所有其他语句之后被调用。本质上讲,这意味着您仍然在执行同样的操作,只是形式上更方便。https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-6.html#jvms-6.5.aload_n
在调用 super() 之前可以执行“任何”语句吗?
super()
之前的语句尝试访问派生类的实例变量或执行方法,代码将无法编译。checkRange()
方法更改为实例方法,代码将无法编译,如下所示:未命名变量和模式
_
替换其名称(或其类型和名称)。由于此类变量和模式不再有名称,它们被称为未命名变量和模式。忽略未使用的变量将减少理解代码段所需的时间和精力。未来,这可以防止错误 :-)。此语言功能不适用于实例或类变量。_
替换未使用的变量是否总是一个好主意,或者它们是否意味着代码异味,您又是否应该考虑重构代码库来将其移除?这些都是很好的问题。如果您不熟悉这个主题,我建议阅读我的详细博文 Drop the Baggage: Use ‘_’ for Unnamed Local Variables and Patterns in Java 22 获取问题的答案。IntelliJ IDEA 配置
一个快速示例
_
替换。switch 构造中的未使用模式和模式变量
GeometricShape
和记录来表示形状,例如 Point
、Line
、Triangle
、Square
,如以下代码所示:sealed interface GeometricShape {} record Point ( int x, int y) implements GeometricShape { } record Line ( Point start, Point end) implements GeometricShape { } record Triangle( Point pointA, Point pointB, Point PointC) implements GeometricShape { } record Square ( Point pointA, Point pointB, Point PointC, Point pointD) implements GeometricShape { }
GeometricShape
实例并返回其面积的方法。由于 Point
和 Line
被视为一维形状,没有面积。以下是定义此类计算并返回面积的方法的方式之一:int calcArea(GeometricShape figure) { return switch (figure) { case Point (int x, int y) -> 0; case Line (Point a, Point b) -> 0; case Triangle (Point a, Point b, Point c) -> areaTriangle(a, b, c); case Square (Point a, Point b, Point c, Point d) -> areaSquare (a, b, c, d); }; }
int x
、int y
、Point a
和 Point B
(用于 case 标签 Line)保持未使用状态。这些可被替换为 _
。另外,由于 case Point 的所有记录组件都未使用,可以将其替换为 Point _
。这也可以让我们合并第一个和第二个 case 标签。这些步骤都显示在以下 gif 中:int calcArea(GeometricShape figure) { return switch (figure) { case Point _, Line _ -> 0; case Triangle (Point a, Point b, Point c) -> areaTriangle(a, b, c); case Square (Point a, Point b, Point c, Point d) -> areaSquare (a, b, c, d); }; }
calcArea()
的实例的类型为 Point
和 Line
的情况,从而为它们返回 0。未使用的模式或变量和嵌套记录
对于具有多个未使用模式或模式变量的嵌套记录,此功能也非常方便,如以下示例代码所示:
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) { } public class GeoMaps { boolean checkFirstNameAndCountryCodeAgain (Object obj) { if (obj instanceof Passenger(Name (String fName, _), _, _, Country (String countryCode, _) )) { if (fName != null && countryCode != null) { return fName.startsWith("Simo") && countryCode.equals("PRG"); } } return false; } }
checkFirstNameAndCountryCodeAgain
中的 if 条件仅使用了两个模式变量,其他可被替换为 _
。这也减少了代码中的噪音。您还可以在哪里使用此功能?
_
替换。预览功能
总结
_
表示。它们不能传递到方法,也不能在表达式中使用。将代码库中未使用的局部变量替换为 _
后,可以非常清楚地传达其意图。它清晰地向任何阅读代码段的人表明变量未在其他位置使用。目前为止,这种意图只能通过注释传达,然而,并不是所有开发者都会写注释。