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 2024.1 Beta 起提供 Java 22 支持。最终版本已于 2024 年 3 月发布。
在 Project Settings(项目设置)中,将 SDK 设为 Java 22。对于语言级别,在 Project(项目)和 Modules(模块)标签页上选择 22 (Preview) – Statements before super(), string templates (2nd preview etc.),如以下设置屏幕截图所示:
如果您还没有将 Java 22 下载到系统中,没关系,IntelliJ IDEA 帮您解决!
您可以使用相同的 Project(项目)设置窗口,点击 SDK 旁边的下拉菜单,然后选择 Download JDK(下载 JDK)。
您将看到以下弹出窗口,其中包含供应商列表(例如 Oracle OpenJDK、GraalVM、Azul Zulu 等):
配置完成,我们从我最喜欢的新功能之一“字符串模板”开始。
字符串模板(预览语言功能)
现有的字符串串联选项难以使用并且容易出错;字符串模板提供了更好的替代方案,即字符串内插,额外优势包括验证、安全和通过模板处理器转换。
如果您不熟悉这个主题,请阅读我的详细博文:String Templates in Java – why should you care?这篇文章涵盖了所有基础知识,包括需要字符串模板的理由,以及内置和用户定义字符串处理器的多个实操示例。
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 可以帮您搞定 🙂
字符串模板和 IntelliJ IDEA 中
的嵌入式表达式
嵌入模板表达式(变量、表达式或方法调用)的语法可能不太符合 Java 开发者的习惯,在没有帮助的情况下可能也不易上手。别担心,IntelliJ IDEA 帮您解决!
每个嵌入式表达式都必须包含在 \{} 内。当您输入 \{ 时,IntelliJ IDEA 会为您添加右 }。它还提供代码补全,帮助您选择作用域内的变量或它上面的任何方法。如果插入的代码无法编译,IntelliJ IDEA 也会高亮显示(作为编译错误),如以下片段所示:
将字符串模板与文本块结合使用
处理跨多行的字符串值(例如 JSON、XML、HTML、SQL)或通常由外部环境处理的其他值时,文本块非常有用。Java 开发者经常结合使用字符串文字和变量值(变量、表达式或方法调用)创建此类字符串值。
以下示例展示了 IntelliJ IDEA 如何使用字符串模板为串联字符串文字与变量值的多行字符串值检测和创建文本块。它还展示了 IntelliJ IDEA 如何为此类块中的变量名称提供代码补全。当您输入 \{ 时,IntelliJ IDEA 将添加 }。当您开始输入变量名称 countryName 时,它会显示该上下文中的可用变量:
语言注入和字符串模板
您还可以在跨越单行或多行的字符串值(例如文本块)中注入语言或引用。由此,您将获得全面的编码辅助来编辑文字值。您可以使用 @Language 注解来临时或永久利用此功能,如下所示:
您可以查看此链接,详细了解 IntelliJ IDEA 中注入语言或引用的好处和用法。
预定义模板处理器
借助字符串模板,您可以使用预定义处理器,例如 STR、FMT 和 RAW。我强烈建议阅读关于字符串模板的详细博文,其中包含多个实操示例。
自定义模板处理器
让我们使用先前博文中未涉及的自定义字符串模板。假设您想要创建一个记录实例,例如 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
}
}
}""";
不要错过我关于此主题的详细博文:String Templates in Java – why should you care? 了解如何使用 FMT 等预定义字符串模板为附近的文具店生成格式正确的收据,或者将 :) 或 :( 等组合编码和解码为 🙂 或 ☹️ 表情符号。是不是还挺有趣的?
隐式声明的类与实例 main 方法
(预览语言功能)
此功能在 Java 21 中作为预览语言功能引入,在 Java 22 中为第二个预览版。
它将彻底改变新 Java 开发者学习 Java 的方式。它简化了学生学习基础知识(例如变量赋值、序列、条件和迭代)时的初始步骤。学生不再需要声明显式类来开发代码,或使用签名 public static void main(String []) 编写其 main() 方法。借助此功能,可以隐式声明类,以及使用更短的关键字列表创建 main() 方法。
如果您不熟悉此功能,我强烈建议阅读详细介绍此功能的文章 ‘HelloWorld’ and ‘main()’ meet minimalistic在这篇博文中,我将包含其中的一些部分。
Java 21 之前和之后的
‘HelloWorld’ 类
在 Java 21 之前,您需要定义一个类,例如 HelloWorld,使用特定关键字列表定义 main() 方法,以将任何文本(例如 ‘Hello World’)打印到控制台,如下所示:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
在 Java 21 中,这个初始步骤已被缩短。您可以使用以下代码定义源代码文件(例如 HelloWorld.java),将消息打印到控制台(不需要定义类,它具有方法 main() 的较短签名):
void main() {
System.out.println("Hello World");
}
前面的代码比之前所需的更简单。让我们看看这一变化如何帮助您专注于真正需要的东西。
编译和执行代码
编写完代码后,下一步就是执行。
在命令提示符上,您可以使用 javac 和 java 命令编译和执行代码。
假设您已在源代码文件 HelloWorld.java 中定义了代码,则可以使用以下命令来运行和执行:
C:\code\MyHelloWorldProject\javac HelloWorld.java
C:\code\MyHelloWorldProject\java HelloWorld
从 Java 11 开始,可以跳过单个源代码文件中定义的代码的编译过程,因此,您可以仅使用第二个命令(指定源代码文件的名称,如下所示):
C:\code\MyHelloWorldProject\java HelloWorld.java
但是,由于实例 main 方法和隐式类为预览语言功能,您应该使用这些命令添加标记 --enable-preview 和 --source 22,如下所示:
C:\code\MyHelloWorldProject\java --enable-preview --source 22 HelloWorld.java
最终,您都可能会转而使用 IDE 编写代码。如果您想使用 IntelliJ IDEA 创建实例 main 方法,请见以下快速步骤。创建一个新的 Java 项目,选择构建系统 IntelliJ(这样您就可以使用 Java 编译器和运行时工具),在运行代码之前,使用实例 main 方法创建一个新文件(例如 HelloWorld.java),并将属性设置为使用 Java 22,如以下 gif 所示(这可以让您免于每次执行代码时都在命令提示符上输入编译/执行命令):
您是否想知道在 src 文件夹中创建 Java 类而不是文件会不会更好?选择 Java 类的选项将生成最小类的主体,例如 public class HelloWorld { }。由于我们一开始就试图避免不必要的关键字,我建议创建一个不包含任何代码的新文件。
除了将消息打印到控制台之外,
main() 还能做什么?
在详细介绍此主题的博文中,我提供了多个实操示例来展示仅通过 main() 方法就能得到的结果:
以上示例是为了展示序列、条件和迭代的强大功能,这些都可以包含在 main() 方法中,以解决实际问题的技能奠定良好的编程基础。使用运行命令或图标在 IntelliJ IDEA 中运行和执行代码,新程序员在入门时又省去了一个步骤。
将隐式类改为常规类
当您准备好升级并使用其他概念(例如用户定义的类)时,您还可以将我们在前面的示例中使用的隐式类和代码转换为常规类,如下所示:
使用 main() 方法创建源代码文件
但没有类声明时会发生什么?
在后台,Java 编译器会创建一个隐式顶层类,带有无实参构造函数,因此这些类不需要以与常规类不同的方式处理。
以下片段展示了源代码文件 AnimateText.java 的反编译类:
隐式类中 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 可以为您添加一个,如以下片段所示:
教育工作者可以使用此功能
向学生逐步介绍其他概念
如果您是教育工作者,您可以向学生介绍其他常用编程做法,例如创建方法,即将部分代码委托给另一个方法并从 main 方法调用您还可以介绍向这些方法传递值与变量。
下面的片段显示了此过程:
super() 之前的语句
– 预览语言功能
通常,我们会为必要但未经官方允许的任务创建替代解决方案。例如,官方未允许在派生类构造函数中于 super() 之前执行语句,虽然这对于验证传递给基类构造函数的值很重要。一种流行变通方法是创建 static 方法验证值,然后对 super() 的实参调用这些方法。尽管这种方式效果很好,但它可能会使代码看起来很复杂。Java 22 中的预览语言功能,super() 之前的语句,将改变这种情况。
使用此功能,您可以摆脱创建 static 方法的变通方法,在调用 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 的值?
在 Java 21 之前,不允许在 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 方法的内容,如以下动图所示:
这是生成的代码:
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);
}
}
您还会在哪里使用此功能?
如果您不熟悉此功能,我建议阅读我的详细博文 Constructor Makeover in Java 22,其中我使用以下示例详细介绍了此功能:
它在后台是如何运作的?
语言语法已经放宽,但不会改变或影响内部 JVM 指令。此新功能的 JVM 指令没有变化,因为构造函数的执行顺序保持不变:从基类到派生类。此外,在 super() 执行之前,此功能仍然不允许使用派生类实例的成员。
我们来访问和比较 IndustryElement 类的构造函数在修改前后的指令集 – 一个可以在 super() 之前执行语句,另一个则不可以。
为此,使用以下命令:
javap -c IndustryElement.class
以下指令集适用的构造函数在 super() 之前不明确执行语句,但调用 static 方法验证原子序数的范围:
以下指令集适用的构造函数在 super() 之前明确执行语句来验证原子序数的范围:
这里最重要的一点是,在这两种情况下,基类的构造函数(即 Element)都会在执行所有其他语句之后被调用。本质上讲,这意味着您仍然在执行同样的操作,只是形式上更方便。
我知道,要记住每个指令代码的含义并不容易。访问以下链接搜索指令代码,就可以更轻松地按照上述指令集操作:
https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-6.html#jvms-6.5.aload_n
在调用 super() 之前
可以执行“任何”语句吗?
不可以。如果 super() 之前的语句尝试访问派生类的实例变量或执行方法,代码将无法编译。例如,如果将 static checkRange() 方法更改为实例方法,代码将无法编译,如下所示:
未命名变量和模式
从 Java 22 开始,使用未命名变量和模式,您可以将未使用的局部变量、模式和模式变量标记为忽略,使用下划线 _ 替换其名称(或其类型和名称)。由于此类变量和模式不再有名称,它们被称为未命名变量和模式。忽略未使用的变量将减少理解代码段所需的时间和精力。未来,这可以防止错误 :-)。此语言功能不适用于实例或类变量。
您是否想知道使用 _ 替换未使用的变量是否总是一个好主意,或者它们是否意味着代码异味,您又是否应该考虑重构代码库来将其移除?这些都是很好的问题。如果您不熟悉这个主题,我建议阅读我的详细博文 Drop the Baggage: Use ‘_’ for Unnamed Local Variables and Patterns in Java 22 获取问题的答案。
IntelliJ IDEA 配置
由于这不是预览语言功能,需要在 Project Settings(项目设置)中的 Project(项目)和 Modules(模块)标签页上将 Language Level(语言级别)设为 22 – Unnamed variables and patterns,如以下设置屏幕截图所示:
一个快速示例
以下 gif 展示了未使用的局部变量 connection 如何被 IntelliJ IDEA 检测并用下划线 _ 替换。
前面 gif 中显示的修改后的代码清楚表明,在 try-with-resources 语句中定义的局部变量没有被使用。
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);
};
}
在前面的示例中,IntelliJ IDEA 检测到模式 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 条件仅使用了两个模式变量,其他可被替换为 _。这也减少了代码中的噪音。
您还可以在哪里使用此功能?
阅读我的详细博文 Drop the Baggage: Use ‘_’ for Unnamed Local Variables and Patterns in Java 22 详细了解可以使用此功能的其他用例:
如果没有意识到未使用的变量或模式是否有代码异味,则不建议使用此功能。我使用这些示例表明,有时重构代码摆脱未使用的变量可能优于仅仅用下划线 _ 替换。
预览功能
“字符串模板”、“隐式声明的类与实例 main 方法”和“super() 之前的语句”在 Java 22 中是预览语言功能。Java 的新发布周期为六个月,新语言功能作为预览功能发布。它们可能会在后续 Java 版本的第二个或其他预览版中重新引入,可能有也可能没有更改。足够稳定后,它们就会作为标准语言功能添加到 Java 中。
预览语言功能是完整的但不是永久的,这实际上意味着功能已经准备好供开发者使用,只是更细致的细节可能会在未来的 Java 版本中根据开发者反馈发生变化。与 API 不同,语言功能在未来不能被弃用。因此,如果您对预览语言功能有任何反馈,请随时在 JDK 邮件名单(需要免费注册)中分享。
由于这些功能的运作方式,IntelliJ IDEA 仅支持当前 JDK 的预览功能。预览语言功能在不同 Java 版本间可能发生变化,直到其被移除或添加为标准语言功能。使用旧版本 Java SE Platform 中的预览语言功能的代码可能无法在新版本上编译或运行。
总结
在这篇博文中,我介绍了四个 Java 22 功能:字符串模板、隐式声明的类与实例 main 方法、super() 之前的语句,以及未命名变量和模式。
字符串模板很好地扩充了 Java。除了帮助开发者使用组合字符串常量和变量的字符串之外,它们还提供了额外一层安全保障。可以轻松创建自定义字符串模板来完成多项任务,例如破译字母组合,将其忽略或替换以增加安全性。
Java 语言设计师引入隐式声明的类与实例 main 方法,减少了 Java 学生编写第一个 HelloWorld 代码所需的步骤。新学生可以从最基本的 main() 方法(例如 void main())开始,并通过序列、选择和迭代等基础知识来提升技能,建立强大的编程基础。
在 Java 22 中,“super() 之前的语句”功能允许在派生类构造函数中调用 super() 之前或者在记录或枚举中调用 this() 之前执行代码,以便您可以根据需要验证方法形参或转换值。这可以避免创建 static 方法之类的变通方法,并使代码更易阅读和理解。此功能不会改变构造函数的操作方式,JVM 指令保持不变。
未命名变量是代码构造的局部变量,它们没有名称,并使用下划线 _ 表示。它们不能传递到方法,也不能在表达式中使用。将代码库中未使用的局部变量替换为 _ 后,可以非常清楚地传达其意图。它清晰地向任何阅读代码段的人表明变量未在其他位置使用。目前为止,这种意图只能通过注释传达,然而,并不是所有开发者都会写注释。
快乐编程!
本博文英文原作者:Mala Gupta
IntelliJ IDEA 相关阅读
关于 IntelliJ IDEA
JetBrains 的旗舰 IDE IntelliJ IDEA 专为高效的 JVM 开发而设计。 凭借对语言和技术的深入了解以及符合人体工程学的用户界面,IntelliJ IDEA 使开发成为愉悦的体验!
免费的开源 IntelliJ IDEA 社区版和 IntelliJIDEA Edu 也可以用于学习和教学编程。
进一步了解 IntelliJ IDEA
⏬ 戳「阅读原文」了解更多