超越常见陷阱:Java开发人员五大致命错误

9,962次阅读
没有评论

共计 4267 个字符,预计需要花费 11 分钟才能阅读完成。

Java 是一种广泛使用的编程语言,它具有跨平台、面向对象、高性能等特点。但即使对于经验丰富的开发人员,也常常会犯一些致命的错误。这些错误可能导致代码质量下降、性能问题或安全漏洞。本文将揭示 Java 开发人员常犯的五大致命错误,并提供了宝贵的建议,助您避免陷入这些错误,提升代码质量和开发效率。

1. 使用 Objects.equals 比较对象

Objects.equals​是 Java 7 提供的一个静态方法,它可以用来比较两个对象是否相等,同时避免空指针异常。这个方法看起来很方便,但是如果使用不当,可能会导致意想不到的结果。例如,下面的代码中,使用​Objects.equals​比较一个 Long 类型的变量和一个 int 类型的常量,结果却是 false。

Long longValue = 123L;
System.out.println(longValue == 123); // true
System.out.println(Objects.equals(longValue, 123)); // false

Objects.equals​方法内部会先判断两个参数是否为同一对象,如果不是,再调用第一个参数的​equals​方法比较第二个参数。而 Long 类的​equals​方法会先判断参数是否为 Long 类型,如果不是,直接返回​false​。所以,当我们使用​Objects.equals​比较一个 Long 类型的变量和一个 int 类型的常量时,实际上是调用了 Long 类的​equals​方法,而这个方法会认为两个参数类型不同,所以返回 false。要避免这个错误,我们需要注意以下几点:

  • 当比较基本类型和包装类型时,尽量使用​==​运算符,因为它会自动进行拆箱和类型转换,而不会出现类型不匹配的问题。
  • 当比较两个包装类型时,尽量保证它们的类型一致,或者使用相应的​parse​方法将它们转换为基本类型再比较。
  • 当比较自定义类型时,尽量重写​equals​方法和​hashCode​方法,以实现合理的相等判断逻辑。

2. 日期格式错误

在 Java 中,我们经常需要对日期进行格式化,以便在不同的场景中显示或存储。为了实现日期格式化,我们通常会使用​DateTimeFormatter​类,它可以根据指定的模式将日期转换为字符串,或者将字符串转换为日期。然而,如果我们使用错误的模式,可能会导致日期格式化出现错误。例如,下面的代码中,使用 YYYY-MM-dd 模式格式化一个 Instant 对象,结果却得到了错误的年份。

Instant instant = Instant.parse("2021-12-31T00:00:00.00Z");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss")
                                               .withZone(ZoneId.systemDefault());
System.out.println(formatter.format(instant)); // 2022-12-31 08:00:00

DateTimeFormatter​类中的模式字母 YYYY 和 yyyy 有细微的差别。它们都表示年份,但是 yyyy 表示日历年,而 YYYY 表示周年。日历年是按照公历的规则划分的,而周年是按照 ISO 8601 标准划分的,它的第一周是包含 1 月 4 日的那一周,而最后一周是包含 12 月 28 日的那一周。所以,当我们使用 YYYY-MM-dd 模式格式化一个 Instant 对象时,实际上是使用了周年的年份,而不是日历年的年份。而 12 月 31 日属于下一年的第一周,所以得到了错误的年份。要避免这个错误,我们需要注意以下几点:

  • 当使用​DateTimeFormatter​类格式化日期时,尽量使用 yyyy 表示年份,而不是 YYYY,除非我们确实需要使用周年的概念。
  • 当使用​DateTimeFormatter​类解析字符串时,尽量保证字符串的格式和模式的格式一致,否则可能会出现解析异常或错误的日期。
  • 当使用​DateTimeFormatter​类进行日期转换时,尽量指定时区,否则可能会出现时差的问题。

3. 在 ThreadPool 中使用 ThreadLocal

ThreadLocal​是一种特殊的变量,它可以为每个线程提供一个独立的副本,从而实现线程间的隔离。使用​ThreadLocal​可以避免一些线程安全的问题,也可以提高一些性能。然而,如果我们在使用线程池的情况下使用​ThreadLocal​,就要小心了,因为这可能会导致一些意想不到的结果。例如,下面的代码中,使用​ThreadLocal​保存用户信息,然后在线程池中执行一个任务,发送邮件给用户。

private ThreadLocal currentUser = ThreadLocal.withInitial(() -> null);
private ExecutorService executorService = Executors.newFixedThreadPool(4);

public void executor() {executorService.submit(() -> {User user = currentUser.get();
        Integer userId = user.getId();
        sendEmail(userId);
    });
}

这段代码看起来没有什么问题,但是实际上有一个隐藏的 bug。因为我们使用了线程池,线程是可以复用的,所以在使用​ThreadLocal​获取用户信息的时候,很可能会误获取到别人的信息。这是因为​ThreadLocal​的副本是绑定在线程上的,而不是绑定在任务上的,所以当一个线程执行完一个任务后,它的​ThreadLocal​的副本并不会被清除,而是会被下一个任务使用。这样就可能导致数据混乱或安全漏洞。要避免这个错误,我们需要注意以下几点:

  • 当使用​ThreadLocal​时,尽量在每次使用完后调用​remove​方法,以清除线程的副本,避免对下一个任务造成影响。
  • 当使用线程池时,尽量不要使用​ThreadLocal​,而是使用其他的方式来传递数据,例如使用参数或返回值。
  • 当使用线程池时,尽量使用自定义的线程工厂,以便在创建线程时设置一些初始化的操作,或者在回收线程时设置一些清理的操作。

4. 使用 HashSet 去除重复数据

在编程的时候,我们经常会遇到去重的需求,即从一个集合中去除重复的元素,只保留唯一的元素。为了实现去重,我们通常会使用​HashSet​,它是一种基于哈希表的集合,它可以保证元素的唯一性,同时具有较高的查询效率。然而,如果我们使用​HashSet​去重时不注意一些细节,可能会导致去重失败。例如,下面的代码中,使用​HashSet​去重一个​User​对象的列表,结果却没有去除重复的对象。

User user1 = new User();
user1.setUsername("test");
User user2 = new User();
user2.setUsername("test");
List users = Arrays.asList(user1, user2);
HashSet sets = new HashSet(users);
System.out.println(sets.size()); // the size is 2

HashSet​的去重机制是基于对象的​hashCode​方法和​equals​方法的。当我们向​HashSet​中添加一个对象时,它会先计算对象的哈希码,然后根据哈希码找到对应的桶,再在桶中遍历元素,使用​equals​方法判断是否有相同

5. 在 List 中循环删除元素

这是一个很常见的错误,很多开发人员都会在使用​List​的​foreach​循环时,试图在循环体中删除元素,这样做会导致​ConcurrentModificationException​异常,因为在迭代过程中修改了集合的结构。如果要在循环中删除元素,应该使用迭代器的 r​emove​方法,或者使用 Java 8 提供的​removeIf​方法,或者使用一个临时的集合来存储要删除的元素,然后在循环结束后再进行删除。例如:

List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");

// 错误的做法,使用 foreach 循环删除元素
for (String s : list) {if (s.equals("b")) {list.remove(s); // 抛出 ConcurrentModificationException 异常
}
}

// 正确的做法,使用迭代器删除元素
Iterator it = list.iterator();
while (it.hasNext()) {String s = it.next();
    if (s.equals("b")) {it.remove(); // 安全的删除元素
    }
}

// 正确的做法,使用 Java 8 的 removeIf 方法删除元素
list.removeIf(s -> s.equals("b")); // 使用 lambda 表达式删除元素

// 正确的做法,使用临时集合删除元素
List temp = new ArrayList();
for (String s : list) {if (s.equals("b")) {temp.add(s); // 将要删除的元素添加到临时集合中
    }
}

// 一次性删除所有元素
list.removeAll(temp); 

总结

在 Java 开发中,避免常见错误是提高代码质量和开发效率的关键。本文揭示了 Java 开发人员常犯的五大致命错误,并提供了宝贵的建议。遵循良好的命名和代码风格,您将能够更好地避免这些错误,提升代码质量并取得更高的开发效率。

1698630578111788

如果你对编程知识和相关职业感兴趣,欢迎访问编程狮官网(https://www.w3cschool.cn/)。在编程狮,我们提供广泛的技术教程、文章和资源,帮助你在技术领域不断成长。无论你是刚刚起步还是已经拥有多年经验,我们都有适合你的内容,助你取得成功。

原文地址: 超越常见陷阱:Java 开发人员五大致命错误

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于2024-09-19发表,共计4267字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)