当前位置: 首页 > 图灵资讯 > 技术篇> JAVA语言错误集锦

JAVA语言错误集锦

来源:图灵教育
时间:2023-06-05 09:24:59

通常,我们都希望我们的代码是高效和兼容的,但实际上,代码中经常含有一些隐藏的坑,只有在出现异常时才能解决。本文是一篇相对简短的文章,列出了开发人员在编写 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 thisLocalDate.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 zonedateTime.atZone(ZoneId.of("GMT+2"));

复制

Instant

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

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

复制

简单来说

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