通常情况下,我们都希望我们的代码是高效和兼容的,但是实际情况下代码中常常含有一些隐藏的坑,只有等出现异常时我们才会去解决它。本文是一篇比较简短的文章,列出了开发人员在编写 Java 程序时常犯的错误,避免线上问题。

1、大量使用 Enum.values

Enum.Values() 的问题在于,按照规范它的返回必须是一个不可变的列表。为了实现这一点,它在每次调用时返回一个带有枚举值的新数组实例。

public enum Fruits {
APPLE, PEAR, ORANGE, BANANA; public static void main(String[] args) {
System.out.println(Fruits.values());
System.out.println(Fruits.values());
}
}
// output
[Lcom.test.Fruits;@7ad041f3
[Lcom.test.Fruits;@251a69d7

它们是内存中的两个独立对象,这好像也没啥事,但是如果在处理大量请求时使用 Fruit.values() 并且机器负载很高,这可能会导致内存升高等问题。

public class Main {
public static final Fruits[] values = Fruits.values(); public static void main(String[] args) {
System.out.println(values);
System.out.println(values);
}
}
// output
[Lcom.wayn.data.elastic.config.Fruits;@4534b60d
[Lcom.wayn.data.elastic.config.Fruits;@4534b60d

如上我们可以通过引入私有静态最终变量 values 来缓存它们来轻松解决此问题。

2、将 Optional 作为方法参数传递

如下代码

LocalDateTime getCurrentTime(Optional<ZoneId> zoneId) {
return zoneId.stream()
.map(LocalDateTime::now)
.findFirst()
.orElse(LocalDateTime.now(ZoneId.systemDefault()));
}

我们传递可选的 zoneId 参数,并根据它的存在来决定是在系统时区中给出时间还是使用指定的时区。但是,这不是正确使用 Optional 的方式。我们应该避免将它们用作参数,而是使用方法重载。

LocalDateTime getCurrentTime(ZoneId zoneId) {
return LocalDateTime.now(zoneId);
} LocalDateTime getCurrentTime() {
return getCurrentTime(ZoneId.systemDefault());
}

如上代码明显更易于阅读和调试。

3、使用字符拼接

Java 中的字符串是不可变的。这意味着一旦创建它们就不再可编辑。 JVM 维护一个字符串池,在创建一个新字符串之前,它调用 String.intern() 方法,该方法从字符串池中返回一个与值匹配的实例(如果存在)。

假设我们想通过连接东西来创建一个长字符串

String longString = "";
longString +="start";
longString +="middle";
longString +="middle";
longString +="middle";
longString +="end";

不久前,我们被告知这是一个非常糟糕的主意,因为Java的旧版本执行以下操作

  • 在第 1 行中,字符串 "start" 被插入到字符串池中,longString 指向它
  • 在第 2 行中,字符串 "startmiddle" 被添加到池中,longString 指向它
  • 在第 3 行,我们有 "startmiddlemiddle"
  • 在第 4 行 "startmiddlemiddlemiddle"
  • 最后,在第 5 行,我们将 "startmiddlemiddlemiddleend" 添加到池中并将 longString 指向它

所有这些字符串都保留在池中并且从不使用,这会浪费大量 RAM。

为了避免这种情况,我们可以使用 StringBuilder

String longString = new StringBuilder()
.append("start")
.append("middle")
.append("middle")
.append("middle")
.append("end")
.toString();

调用 toString 方法时,StringBuilder 仅创建一个字符串,从而为我们保存了最初添加到池中的所有中间字符串。但是,在 Java 5 之后,编译器会自动为我们完成此操作,并且可以安全地使用带有 "+" 的字符串连接。

此规则有一个例外,那就是在循环中进行字符串连接时

String message = "";
for (int i = 0; i < 10; i++) {
message += "msg" + i;
} System.out.println(message);

这段代码不会被 JIT 优化,每次迭代都会将新的字符串插入到字符串池中,这里我们必须使用 StringBuilder

StringBuilder msgB = new StringBuilder();
for (int i = 0; i < 10; i++) {
msgB.append("msg").append(i);
} System.out.println(msgB);

这里还有几件事要注意

即时编译器有时会重新组织代码。

String s = "1" + "2" + "3";

转换成

String s = "123";

从 Java 15 开始,可以使用文本块处理多行字符串:

String sql = """
SELECT * FROM users as u
WHERE u.name = 'John'
AND u.age > 34
""";

4、过度使用原始包装器

考虑以下两个片段

int sum = 0;
for (int i = 0; i < 1000 * 1000; i++) {
sum += i;
}
System.out.println(sum); // ---------------------- Integer sum = 0;
for (int i = 0; i < 1000 * 1000; i++) {
sum += i;
}
System.out.println(sum);

在我的机器上,第一个比第二个快 6 倍。唯一的区别是我们使用包装器 Integer 类。这样做的原因是,在第 3 行中,运行时必须将 sum 变量转换为原始 int(自动拆箱),并且在执行添加后,结果将包装在一个新的 Integer 类中(自动装箱)。这意味着我们创建了 100 万个 Integer 类并执行了 200 万个装箱操作,这解释了速度急剧下降的原因。

仅当需要将包装类存储在集合中时才应使用包装类。但是,未来的 Java 版本将支持原始类型的集合,这将使包装器过时。

5、自己编写哈希函数

当我们想将对象存储在 HashMap 中时,通常会实现对象的哈希函数。该 HashMap 由带有数字的 "桶" 组成,每个哈希码都分配给一个特定的桶。如果存入 "桶" 对象的哈希函数没有正确编写,HashMap 的性能将显着降低。一个写得很好的散列函数将确保所有键的平均分配。

在一般情况下我们需要自己编写哈希函数,但在大多数情况下,使用内置的 Objects.hash(...) 方法就行,该方法为一系列输入值生成哈希代码,生成散列代码的方式就像将所有输入值都放入一个数组中一样,并且通过调用 Arrays.hashCode(Object[]) 对该数组进行散列。

public class Car {
private final String model;
private final Integer year;
private final Instant manufactureDate; public Car(String model, Integer year, Instant manufactureDate) {
this.model = model;
this.year = year;
this.manufactureDate = manufactureDate;
} @Override
public int hashCode() {
return Objects.hash(model, year, manufactureDate);
} @Override
public boolean equals(Object obj) {
// 在实现 hashCode 时,不要忘记实现 equals
}
}

6、使用 java.util.Date

我们甚至应该避免 java.util 中的所有时间类改用 java.time 包。

Date 类已被弃用,原因有很多,它有很多设计缺陷。

  • 它不是无法被修改的
  • 它无法处理时区
  • 充满已弃用但仍在使用的遗留代码

当程序中出现对日期支持的需求时,util 包中的 Date、Calendar 和 rest time 类就出现了。鉴于如上缺陷,程序界有几次修复它们的尝试,但最后他们决定引入一个新的包 java.time。 java.time 包与第三方的 joda.time 非常相似,这意味着我们不需要在使用 joda.time,Jdk8 已经有了内置支持。

我们列出 java.time 中使用的三个最重要的类

LocalDate

表示特定时区的日期(不包括一天中的时间)。

LocalDate.of(2022, 6, 12);
LocalDate.parse("2022-06-12"); // The Date/Time API in Java works with the ISO 8601 format by default, which is (yyyy-MM-dd)
// We can overwrite it like this
LocalDate.parse("2022.06.12", DateTimeFormatter.ofPattern("yyyy.MM.dd"));

LocalDateTime

与 LocalDate 相同,但它有一天中的时间。

LocalDateTime.of(2022, 6, 12, 10, 34, 18);
var dateTime = LocalDateTime.parse("2022-06-23T10:34:18"); // it's easy to get the time in a different zone
dateTime.atZone(ZoneId.of("GMT+2"));

Instant

我最喜欢的。它本质上是 LocalDateTime,但强制使用 UTC 时区。在应用程序中需要处理时区时,最好在所有服务和数据库中使用同一个时区。当使用 Instant 时,一切都变成了 UTC,然后读者可以根据需要将其转换为不同的时区。

// Current time in UTC
Instant.now(); // Note the 'Z' at the end it means UTC
Instant.parse("2022-06-21T12:12:12Z"); // Convert instant to a different time zone
Instant.now().atZone(ZoneId.of("GMT+3"));

简单来说

  • 不要使用日期和日历(或任何与 java.util 相关的日期)
  • 不要使用 joda.time(因为它与 java.time 非常相似)
  • 如果只对某个区域的日期感兴趣,请使用 LocalDate
  • 如果对某个区域的日期和时间感兴趣,请使用 LocalDateTime
  • 如果需要日期时间并且不想处理时区,请使用 Instant

本文翻译自国外论坛 medium,原文地址:https://medium.com/@b.stoilov/things-to-avoid-while-writing-java-cd078e5aa61c

关注公众号【waynblog】每周分享技术干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!

编写Java代码时应该避免的6个坑的更多相关文章

  1. 【eclipse jar包】在编写java代码时,为方便编程,常常会引用别人已经实现的方法,通常会封装成jar包,我们在编写时,只需引入到Eclipse中即可。

    Eclipse中导入外部jar包 在编写java代码时,为方便编程,常常会引用别人已经实现的方法,通常会封装成jar包,我们在编写时,只需引入到Eclipse中即可. 工具/原料 Eclipse 需要 ...

  2. 像写C#一样编写java代码

    JDK8提供了非常多的便捷用法和语法糖,其编码效率几乎接近于C#开发,maven则是java目前为止最赞的jar包管理和build工具,这两部分内容都不算多,就合并到一起了. 愿编写java代码的过程 ...

  3. 如何更规范化编写Java 代码

    如何更规范化编写Java 代码 Many of the happiest people are those who own the least. But are we really so happy ...

  4. 如何更规范化的编写JAVA 代码

    如何更规范的编写JAVA代码 一.MyBatis 不要为了多个查询条件而写 1 = 1 当遇到多个查询条件,使用where 1=1 可以很方便的解决我们的问题,但是这样很可能会造成非常大的性能损失, ...

  5. myeclipse 编写java代码提示 dead code 原因

    经常使用MyEclipse或Eclipse编辑器编写java代码的程序员,可能经常遇到一个黄线警告提示:dead code:一般程序员遇到这些问题都会置之不理,反正也不影响程序的编译执行.对,这不是b ...

  6. Jmeter自定义编写Java代码调用socket通信

    一.前言 最近需要测试一款手机游戏的性能,找不到啥录制脚本的工具,然后,另外想办法.性能测试实际上就是对服务器的承载能力的测试,和各种类型的手机客户端没有啥多大关系,手机再好,服务器负载不了,也不能够 ...

  7. 通过编写Java代码让Jvm崩溃

    在书上看到一个作者提出一个问题"怎样通过编写Java代码让Jvm崩溃",我看了之后也不懂.带着问题查了一下,百度知道里面有这样一个答案: 1 package jvm; 2 3 pu ...

  8. 在执行java代码时,设置了断点,然后莫名的没执行完方法内的代码就结束了,此刻一般在出错处代码用try,catch包括起来

    在执行java代码时,设置了断点,然后莫名的没执行完方法内的代码就结束了,此刻一般在出错处代码用try,catch包括起来就能看到是什么异常了,记住try,catch语句的作用

  9. MyEclipse 编写 JSP 代码时很卡的解决办法

    在网上看到很多方法,都是尝试过,个人感觉都没有说到重点,所以收效甚微. 后来自己总结了一下: 我们都是习惯在MyEclipse 工具,双击jsp 文件打开进行编辑.这时,工具会打开窗口的 Previe ...

  10. Java mac 上编写Java代码

    看视频学JAVA,不想下载 notepad++之类的,虽然知道mac有内嵌的JAVA sdk ,但是还是不知道怎么编写,今天终于编写了我的第一个JAVA程序,还是以 Hello World 开始吧 1 ...

随机推荐

  1. 项目实战接口开发SpringBoot

    目录 一.springboot官方demo开发 二.使用SpringBoot开发get方法接口 三.一个要求携带cookie信息访问的get接口开发 四.需要携带参数的get请求两种开发方式 4.1 ...

  2. Python:单元测试框架unittest

    1.什么是单元测试 测试函数/方法或者一段代码,用于检验被测代码的一个很小的.很明确的功能是否正确,通常是开发做. 在Python中的单元测试框架有Unittest和Pytest,现在总结Unitte ...

  3. 神经网络优化篇:详解正则化(Regularization)

    正则化 深度学习可能存在过拟合问题--高方差,有两个解决方法,一个是正则化,另一个是准备更多的数据,这是非常可靠的方法,但可能无法时时刻刻准备足够多的训练数据或者获取更多数据的成本很高,但正则化通常有 ...

  4. 【C++】【图像处理】灰度直方图实现算法解析(以.raw格式的图像为基础进行图像处理、gray levels:256)

    前情提要:本记录需要一定的C++和图像处理基础进行阅读. 图像处理算法学习记录: Code: 1 void histCompute(BYTE*image, int width, int height) ...

  5. auto{x}与auto(x)---一位中国小伙为cppreference作出的贡献

      C++作为一门静态类型语言,是需要程序员声明变量类型的.然而来到了C++11,auto的诞生使得变量声明变得及为方便,尤其是对于比较长的模板类型,auto一定程度上为代码编写者减轻了负担.到了C+ ...

  6. 谷粒学院通用工具类R类

    import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.HashMap; import ...

  7. 在macOS中搭建.NET MAUI开发环境

    @ 目录 准备 安装扩展 安装 .NET 安装工作负载 安装 Xcode 命令行工具 调试安卓应用 安装 JDK 安装 Android SDK 安装 Android 模拟器 安装模拟器 安装镜像 创建 ...

  8. 【推荐】后现代风格文本编辑器-Helix

    目录 Helix简介 Helix 安装 Linux平台 Ubuntu Fedora/RHEL Arch Linux extra NixOS Flatpak Snap AppImage macOS系统 ...

  9. 浅谈树形DP

    树形DP是动态规划中最难也最常考的内容.具有DP和图论相结合的特点. 但从本质上来说,树形DP只不过是一种线性DP,只是将它与搜索结合起来了而已. 树形DP的基本步骤 读图 树形DP的题目中,通常会给 ...

  10. 文心一言 VS 讯飞星火 VS chatgpt (22)-- 算法导论4.2 2题

    二.为Strassen算法编写伪代码. 文心一言: Strassen算法是一种用于矩阵乘法的算法,它的核心思想是通过不断将原始矩阵分割成较小的子矩阵来实现高效的乘法运算.以下是Strassen算法的伪 ...