JetBrains

编写卓越 Java 代码的艺术:JetBrains 与你分享最佳实践 | 技术解析

好的代码都会遵循一定规则,了解这些规则将增大您成功的几率。我们将在本文中分享一些 Java 最佳做法,为您提供帮助。我们将介绍必知提示和技巧,涵盖软件开发的总体建议以及 Java 和项目特定的专业知识。我们开始吧!

常规

首先,牢记以下有关现代编码的一般规则。

干净好过聪明

代码的主要目的是被理解和维护,而不是炫耀技术能力。干净的代码会让软件更易于调试、维护和扩展,使参与项目的所有人受益。复杂并不是荣誉奖章,简单和可读才是。

考虑以下示例。

  • 聪明的:

Image

这是交换变量 `a` 和 `b` 值的非常规方式。虽然聪明,但乍一看可能会令人困惑。

  • 干净的:

Image

这是更常见的方式。虽然额外需要一行代码,但其直观性使大多数程序员都能更轻松地理解。

保持简短

确保方法和类不会太长。虽然对于类的确切行数或单词数没有严格规定,但建议的做法是保持重点突出且连贯的结构。对于方法,通常每个方法的推荐代码量为 10 到 20 行左右。如果方法变得更长,最好将其分成更小、更易管理的部分。

如果您想练习识别方法是否过长,可以观看技术指导 Emily Bache 制作的精彩视频。

IntelliJ IDEA 还可以提供多种重构选项来帮助您摆脱长方法和类。例如,它允许您提取方法以将长方法分解为更短的方法。

命名很难,所以要多加留意

方法和变量的正确名称可以作为理解代码目的和功能的直观指南,对于有效沟通至关重要。以下是您需要了解的最重要的命名约定。

我们建议避免单字母变量,确保方法名称反映其操作,并将对象和字段名称与业务域对齐,增强清晰度和沟通。例如,名为 calculateTotalPrice() 的方法可以立即传达其目的,而像 calculate() 这样的模糊名称则使功能不够明确。同样,名为 customerEmailAddress 的变量清楚直观,而 cea 这样的缩写可能指代任何内容,导致困惑。

另一个例子是,指定相关单位而不是命名变量超时。使用 timeoutInMs 或 timeoutInMilliseconds 可以避免单位混淆。

测试、测试、测试

要确保应用按预期工作并在发生变化时继续运行,必须测试代码。测试有助于及早发现问题,使修正更便宜、更容易。它们还会指导您代码应该如何工作,并最大限度地减小后续更新时破坏代码的几率。

好的测试名称很重要,因为它们显示了每个测试的作用和查找的内容。例如,名为 AlertWhenEmailIsMissing()checks 的测试是用于在缺失电子邮件时发出警报,您无需深入了解细节。

您可以阅读 Marit van Dijk 的这篇博文,了解有关测试的更多信息。

特定于语言

以下提示和技巧将帮助您在使用 Java 编写时避免一些常见错误,提高您的代码质量。

使用 switch 表达式代替

过多的 If 语句

使用 switch 表达式可以将多个条件合并到一个结构中,让代码更易读、更清晰。这种方式简化了代码,使其更易理解和维护。

下面以不同类型的冰淇淋及其主要成分举例。

  • 过多 else-if

在本例中,代码使用了一系列 else–if 语句将冰淇淋口味与其主要成分相匹配。随着口味数量的增加,if 语句的数量可能会变得难以管理,使代码更难阅读。

Image

  • 切换

在这个不同版本的示例中,我们使用 switch 表达式而不是多个 if-else 条件获得了相同的结果。在将单个变量与多个常量值进行比较时,switch 表达式更紧凑、更简洁且更容易理解。

IntelliJ IDEA 提供了一种特殊的检查,可以在几秒钟内将 if 语句转换为 switch 表达式。

Image

您可以在 Java 技术布道师 Mala Gupta 最近发布的博文中找到更多精彩的 switch 用法示例。

避免空 catch 块

Java 中的空 catch 块是内部没有任何用于处理异常的代码的 catch 子句。当这种块捕获异常时,什么也不会发生,程序会继续运行,就好像没有出现错误一样。这可能会使问题难以发现和调试。

  • 空 catch 块

在此示例中,我们捕获异常但不采取任何措施。

Image

IntelliJ IDEA 通过检查高亮显示此类情况并提供解决方案:

Image

  • 记录异常

一种异常处理方式是借助 e.printStackTrace() 进行记录,它将堆栈跟踪打印到控制台,帮助我们识别和调试潜在问题。

Image

  • 记录异常并重新抛出

理想情况下,catch 块将识别 IOException,然后将错误消息打印到控制台并重新抛出异常以供后续处理。

这样,您就可以全面了解问题所在。

Image

  • 记录异常并返回替代值

解决空 catch 块问题的另一种方式是记录异常但返回有意义的值。

Image

选择集合而不是数组

以获得更大的灵活性

虽然 Java 中的数组高效易用,但其大小固定,并且提供的运算有限,不太适合多种数据操作。

Java 中的集合提供了更多的灵活性和实用性,例如 ArrayList 或 HashSet。ArrayList 提供了动态大小调整和许多实用方法,并且更易用,特别是与泛型搭配。我们来看一些代码示例:

  • 数组

我们创建了一个 String 数组。由于 Java 中的数组大小固定,如果我们想添加第十一个元素,我们必须创建一个新数组并复制所有元素。

Image

  • 集合

作为先前代码的替代方案,我们可以使用名为 ArrayList 的集合类。像 ArrayList 这样的集合可以在运行时增长和收缩,从而提供更大的灵活性。它们还提供了强大的数据操作方法,例如 .add()、.remove()、.contains()、.size() 等。

Image

拥抱不可变性

不可变对象是指创建后状态无法更改的对象。它们通过移除与跟踪可变状态更改相关的复杂性来帮助编写更安全、更简洁的代码。这可以最大限度地减少错误和意外副作用的风险,确保行为一致,并简化调试和维护应用程序的过程。在 Java 中,我们使用 final 实现不可变性。

  • 无 final

在此示例中,我们将创建一个 Car 类并打印其品牌和型号。然后,我们将更改汽车型号并再次打印。控制台输出将显示 Car 类的状态已经更改,表明可变行为。

Image

  • 有 final

在下方的改进代码中,您可以看到我们做了同样的事,但是无法更改汽车型号或品牌,因为我们没有 setter 方法,并且 Car 类为 final。控制台输出将显示 Car 类的状态保持不变,表明不可变行为。

Image

复合先于继承

在 Java 中,通常最好使用复合(对另一个类的对象有引用或依赖项)而不是继承(从超类创建子类)。复合使代码更加灵活并且更易测试。

  • 继承

在我们的示例中,GamingComputer 继承自 BasicComputer。这可能会导致问题,因为如果 BasicComputer 发生变化,可能会破坏 GamingComputer。此外,GamingComputer 被锁定为一种 BasicComputer,限制了其灵活性。

Image

  • 复合

在第二个示例中,Computer 类由 Memory 和 Processor 组成,它们是充当字段的独立类实例。每个类都独立定义其方法和行为。这种方式更加灵活,因为您可以交换不同的 Memory 或 Processor 类型,或者在运行时更改其行为,而无需更改 Computer 类。

Image

如需更多示例,请参阅《简单攻略:在 Java 中创建继承》。

使用 lambda 简化函数式接口

Java 中的函数式接口是一种只有一个抽象方法的接口。lambda 提供了一种简洁、富有表现力的实现方式,无需匿名类的样板代码。

  • 无 lambda

下面我们将使用一个匿名内部类实现 Comparator 接口进行排序。它体积庞大,并且对于更复杂的接口可能不可读。

Image

  • 有 lambda

这段代码的作用与上面相同,但我们使用 lambda 函数而不是匿名类。这使代码更加紧凑直观。

Image

使用增强的 for 循环或流

与传统 for 循环相比,Java 中的增强 for 循环(for-each 循环)和流提供了更可读和更紧凑的方式来迭代集合或数组。

  • 经典 for 循环

在这段代码中,我们使用 for 循环遍历列表。它需要一个计数器,处理元素索引并定义停止条件 – 这都增加了复杂性。

Image

  • 增强的 for 循环

此循环 (forEach) 消除了对计数器的需要,并直接提供了列表中的每个条目,简化了代码并减小了出错的几率。您可以借助 IntelliJ IDEA 中的检查来应用。

Image

  • 流

流为我们提供了每个条目,类似于增强的 for 循环,但也允许我们执行复杂的操作,例如筛选和映射。

Image

使用 try-with-resources 语句

保护资源

try-with-resources 语句可以帮助您确保每个资源在使用后正确关闭。不关闭 try 块中的资源可能会导致内存问题和应用程序错误,影响性能和可靠性。

  • 手动关闭资源

在下面的示例中,我们手动处理系统资源 FileInputStream,如果关闭时抛出异常,可能会出现资源泄漏等问题。

Image

  • 使用 try-with-resources 语句

在改进版本中,在 try 块内声明 FileInputStream 意味着 Java 会将其自动关闭,无论我们是正常离开还是出现异常。

Image

理清深度嵌套的代码

深度嵌套的代码突出了最初很难注意到的逻辑问题。这通常是因为使用了大量条件句,但这些条件句是编码必需的,不能直接除去。不过,我们仍然需要找到简化代码的方式。

  • 大量条件句

您可以看到冗余嵌套条件句看起来有多奇怪。

Image

  • 重构的代码

我们使用“保护子句”技术来消除嵌套条件句, 在满足某些条件时快速退出函数,以看起来更清晰的代码保持相同的逻辑。

Image


项目特定

对于处理包含多个依赖项的项目,我们还提供了一些实用注意事项。

确保依赖项处于最新状态

请确保项目的依赖项处于最新状态,以增强安全性、引入新功能并修正 bug。定期更新可以确保项目顺利运行并与其他工具保持兼容。

IntelliJ IDEA 可以帮助您使依赖项保持最新状态。首先,通过 Preferences/Settings | Plugins(偏好设置/设置 | 插件)从 JetBrains Marketplace 安装 Package Search 插件。然后,导航到 Dependencies(依赖项)工具窗口查看项目中的所有现有依赖项,点击它们旁边的 Upgrade(升级)链接。

Image


检查存在漏洞的依赖项和 API

定期扫描项目查找依赖项和 API 中的薄弱环节,有助于您最大限度地降低安全风险、遵守预定义的规则并保持一切顺利运行。快速解决这些漏洞有助于保护项目及其用户免受潜在威胁。

要在 IntelliJ IDEA 中查找存在漏洞的依赖项,首先导航到 Code | Analyze Code(代码 | 分析代码),然后选择 Show Vulnerable Dependencies(显示存在漏洞的依赖项)。结果将在 Problems(问题)工具窗口的 Vulnerable Dependencies(存在漏洞的依赖项)标签页中显示。

Image

您还可以在 Project(项目)工具窗口中右键点击文件夹或文件,例如 pom.xml 或 build.gradle,然后从上下文菜单中选择 Analyze Code | Show Vulnerable Dependencies(分析代码 | 显示存在漏洞的依赖项)。

即使没有明确检查漏洞,IntelliJ IDEA 也会高亮显示 pom.xml 或 build.gradle 中的漏洞。

Image


避免循环依赖关系

当项目的各个部分在循环中相互依赖时,就会出现循环依赖关系。例如,A 部分需要 B 部分的某些内容,但 B 部分也需要 A 部分的某些内容。这可能会使项目变得混乱和难以开展,因为很难弄清楚一个部分从哪里开始,另一个部分在哪里结束。最好避免这些循环,保持内容清晰且易于管理。

要避免循环依赖项,您可以使用依赖关系矩阵。这有助于直观呈现项目中组件之间的依赖关系。

结论

希望我们的建议能够简化您的日常任务,我们期待您编写出更简单、清晰和专业的代码,成为更高效的开发者。

IntelliJ IDEA 将帮助您找到这篇博文中讨论的条目并提供自动修正。试用一下,然后告诉我们您的想法!

本博文英文原作者:Irina Mariasova

Image

IntelliJ IDEA 相关阅读

Image
Image
Image
Image

关于 IntelliJ IDEA

JetBrains 的旗舰 IDE IntelliJ IDEA 专为高效的 JVM 开发而设计。凭借对语言和技术的深入了解以及符合人体工程学的用户界面,IntelliJ IDEA 使开发成为愉悦的体验!

免费的开源 IntelliJ IDEA 社区版和 IntelliJIDEA Edu 也可以用于学习和教学编程。

进一步了解 IntelliJ IDEA

Image

⏬ 戳「阅读原文」了解更多