JAVA编码中存在一些容易被人忽视的陷阱,稍不留神可能就会跌落其中,给项目的稳定运行埋下隐患。此外,这些陷阱也是面试的时候面试官比较喜欢问的问题。

本文对这些陷阱进行了统一的整理,让你知道应该如何避免落入陷阱中,下面就一起来了解下吧。

循环中操作目标list

遍历List然后对list中符合条件的元素进行删除操作,这是项目里面非常常见的一个场景。

先看下两种典型的错误写法:

错误写法1:


for (User user : userList) {
if ("男".equals(user.getSex())) {
userList.remove(user);
}
}

错误原因:

在循环或迭代时,会首先创建一个迭代实例,这个迭代实例的expectedModCount 赋值为集合的modCount。而每当迭代器使⽤ hashNext() / next() 遍历下⼀个元素之前,都会检测 modCount 变量与expectedModCount 值是否相等,相等的话就返回遍历;否则就抛出异常ConcurrentModificationException,终⽌遍历。

如果在循环中添加或删除元素,是直接调用集合的add()remove()方法,导致了modCount增加或减少,但这些方法不会修改迭代实例中的expectedModCount,导致在迭代实例中expectedModCountmodCount的值不相等,抛出ConcurrentModificationException异常。

错误写法2:


for (int i = 0; i < userList.size(); i++>) {
if ("男".equals(user.getSex())) {
userList.remove(i);
}
}

错误原因:

删除元素之后,元素下标发生前移,但是指针是不变的,再处理下一个的时候,就可能会有部分元素被漏掉没有处理。

那正确的方式应该如何处理呢?接着往下看。

正确写法1:


// 使用迭代器来实现
Iterator iterator = userList.iterator();
while (iterator.hasNext()) {
if ("男".equals(user.getSex())) {
iterator.remove();
}
}

补充说明:

与前面的错误写法1相对比,同样都是基于迭代器的逻辑,为什么这种写法就对了呢?

这是因为迭代器中的remove()add()方法,会在调用集合的remove()add()方法后,将expectedModCount重新赋值为modCount,所以在迭代器中增加、删除元素是可以正常运行的。

正确写法2:


// 使用Lambda表达式实现
userList.removeIf(user -> "男".equals(user.getSex());

正确写法3:


// 使用removeAll实现
List<User> maleUsers = new ArrayList<>();
for (User user : userList) {
if ("男".equals(user.getSex())) {
maleUsers.add(user);
}
}
userList.removeAll(maleUsers);

将对象作为参数传递并重新赋值

看个例子:

这里涉及到JAVA中一个值传递和引用传递的概念。

对于一个引用类型而言,参数传递的时候,本质上是将一个引用对象对应内存地址传递过去,参数对象与实际对象指向同一个内存块。对于示例代码中的changeUser()方法,将入参重新赋值了一个新的对象,本质上其实是将user1对应指向的内存地址信息更改了,对于原始的user而言,并没有被改变。

而对于chageAge()方法而言,对user2的操作,本质上是对user2所指向的具体内存对象进行操作,也即user对应的内存对象数据,所以这种情况下,变更是会生效的。

所以呢,编码的时候,要注意不能在方法里面对入参进行重新赋值,可以采用返回值的方式返回个新的结果对象,然后进行赋值操作。

字母L的使用

先看一个例子:

同样相同的三个数字的相乘,L标识的位置不同,得到的结果也不一样,那到底哪个是对的呢?

很明显这个一个JAVA隐式类型转换的问题。

  • 第一个结果显然是不对的,因为三个int值相乘之后结果明显超出int长度范围,所以截断了。
  • 第二个结果,前面两个int相乘,在与第三个long型运算,结果会自动转换为long型,但是根据运算顺序,前面2个int值运算的中间结果也是int类型,且长度超出范围被截断了,截断后的结果与最后一位long进行的运算,所以第二个结果也是错的。
  • 第三个结果是对的,因为long型放在前面,所以前两位运算的时候就先转为long型,再与第三个int运算后,结果依旧是long型,不会出现溢出的情况。

TIPS:

int运算转long的时候,最好将第一个运算的数字标识为L(long)型,避免中途数据溢出。

再看个例子:

    public static void main(String[] args) {
System.out.println(2 * 2l);
}

第一眼看完,想一下答案应该是几?是42吗?

其实结果是4,为什么?因为第二个2后面的是个字母l。虽然这种写法对于程序而言没有问题,但是很容易让开发人员混淆,造成认知上的错误。

TIPS:

long数字标识的时候,使用大写字母L来表示。

流/资源的释放

打开的流或者连接,在用完之后需要可靠的退出。但是有一种循环中打开流的场景,需要特别注意,笔者在多年的代码review经历中发现,基本每个项目都会存在循环中打开的流没有全部可靠释放的问题。

上面的示例代码中,虽然最后finally里面也有执行流的关闭操作,但是try分支中,inputStream是在一个for循环里面被多次创建了,而最终finally分支中仅关闭了最后一个,之前的流都处于未关闭状态,造成资源的泄漏。

日期格式转换的并发场景

很多的项目中都使用SimpleDateFormat来做日期的格式化操作,但是要注意SimpleDataFormat是非线程安全的,所以使用的时候需要注意。

但是实际使用的时候,如果每次需要格式化的时候,都去new SimpleDateFormat()对象,这个成本开销有点大,会对整体的性能造成一定的影响。

所以使用的时候可以采取一些措施,保证线程安全的同时也兼顾其处理性能:

事务失效场景

JAVA开发中,经常会使用Spring的@Transactional注解来指定事务回滚的相关策略,但是有时候会发现@Transactional并没有生效,下面介绍下可能的几种情况。

finally分支的数据处理

finally分支一般伴随着try...catch语句一起使用,用来当所有操作退出前执行一些收尾处理逻辑,比如资源释放、连接关闭等等。但是如果使用不当,也会造成我们的业务逻辑不按预期执行,所以使用的时候要注意。

finally分支中对返回值重新修改

先看下如下代码写法,在try...catch分支中都有return操作,然后再finally中进行返回值修改,最终返回结果并不会被finally中的逻辑修改:

因为如果存在 finally 代码块,try...catch中的return语句不会立马返回调用者,而是记录下返回值的副本,待 finally代码块执行完毕之后再向调用者返回其值,然后即使在finally中修改了返回值,也不会返回修改后的值。

再看另一种常见的写法:

与上面的差异点在于,try...catch分支里面并没有return语句,而是finally外面统一执行返回操作,这种情况下就可以生效。其实也很好理解,try...catch...finally这个语句块里面没有return操作,所以也就不会有暂存return副本的逻辑了。

finally分支中直接return

在finally分支里面存在return语句是一个很不好的实践。一般的IDEA中也会智能提示finally里面存return分支。

finally里面如果存在return分支,则finally里面的返回值会覆盖掉try...catch逻辑中处理后计划返回的结果,也即导致try...catch部分的逻辑失效,容易引起业务逻辑上的问题。

finally分支中抛出异常

一般的编码规范中,都会要求finally分支里面的处理逻辑要增加catch保护,防止其抛出异常。

原因说明:

相对而言,finally里面执行的都是一些资源释放类的操作,而try...catch部分则是业务维度的核心逻辑,人们更关心的是catch部分发生的业务层面的异常,如果finally里面抛出异常,会导致catch中原本应该要往外抛的异常被丢弃,可能会影响上层逻辑的后续处理。

全局变量中的集合类

全局类型的集合类,使用的时候需要注意两个关键点:

  1. 注意下并发场景的线程安全性;
  2. 注意下数据的最大范围、是否有数据淘汰机制,避免内存无限制增加,导致内存溢出。

参考下redis之类的依赖内存的缓存中间件,都有一个绕不开的兜底策略,即数据淘汰机制。对于业务类编码实现的时候,如果使用Map等容器类来实现全局缓存的时候,应该要结合实际部署情况,确定内存中允许的最大数据条数,并提供超出指定容量时的处理策略。比如我们可以基于LinkedHashMap来定制一个基于LRU策略的缓存Map,来保证内存数据量不会无限制增长。

public class FixedLengthLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private static final long serialVersionUID = 1287190405215174569L;
private int maxEntries; public FixedLengthLinkedHashMap(int maxEntries, boolean accessOrder) {
super(16, 0.75f, accessOrder);
this.maxEntries = maxEntries;
} /**
* 自定义数据淘汰触发条件,在每次put操作的时候会调用此方法来判断下
*/
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxEntries;
}
}

浮点数据计算

看一段很简单的代码:


public static void main(String[] args) {
System.out.println(0.1 + 0.2);
}

上面的执行后,输出结果应该是多少?很明显应该是0.3呀!然而实际上,运行之后,输出结果为:

0.30000000000000004

这是因为浮点数是不精确的,因为浮点数是存在小数点位的,而十进制的0.1换算为二进制是一个无限循环小数,所以实际上存储的其实是一个近似0.1的值、而不是精确的0.1

也正是这个原因,一般实现中,判断两个float是否相等时,往往不用==,而是判断两个浮点数之差绝对值是否小于一个很小的数。

对于一些需要精确计算的场景,显然是不能使用浮点数来运算的,比如一些银行金融领域涉及金钱数额相关的场景,是绝对不允许使用float或者double进行运算,而是推荐使用BigDecimal来替代。

使用Object作为HashMap的key

大家都知道在JAVA中,HashMap的key是不可以重复的,相同的key对应值会进行覆盖。但是,如果使用自定义对象作为HashMap的key,就要小心了,因为如果操作不当,很容易造成内存泄漏的问题。

如果一定要使用,确保此Object一定是覆写了hashCode()equals()方法,并且保证覆写的equalshashCode方法中一定不能有频繁易变更的字段参与计算。

结语

好啦,关于JAVA中常见的十大陷阱,这里就给大家分享到这里。希望大家在实际编码的时候可以注意提防,避免踩坑。


我是悟道,聊技术、又不仅仅聊技术~

如果觉得有用,请点个关注,也可以关注下我的公众号【架构悟道】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的自己。

避坑手册 | JAVA编码中容易踩坑的十大陷阱的更多相关文章

  1. Java面试中遇到的坑【填坑篇】

    看到大家对上篇<Java面试中遇到的坑>一文表现出强力的关注度,说明大家确实在面试中遇到了类似的难题.大家在文章留言处积极留言探讨面试中遇到的问题,其中几位同学还提出了自己的见解,我感到非 ...

  2. Java 面试中遇到的坑

    Java开发中很多人都不愿意修改自己以前的代码,看别人的代码更是无法忍受,当看到别人代码里面一些匪夷所思的写法实现时,恨不得找到负责人好好跟他谈谈心,那么你在开发中是不是也使用到以下几种实现呢. 1. ...

  3. 对称加密算法在C#中的踩坑日常

    前言 有幸接触了一下传说中的对称加密算法3DES 感觉这些加密算法与我的工作是想去甚远的,一般没什么机会接触这些东西 今次了解了一下3DES这个对称算法 原理算不上明白,算是踩了C#中的一些坑吧 C# ...

  4. 在Java编码中,如何减少bug数量

    众所周知,Java编程语言在IT行业是企业中不可缺少的.不管,从Web应用到Android应用,这款语言已经被广泛用于开发各类应用及代码中的复杂功能.但在编写代码时,bug永远是困扰每一位从业者的头号 ...

  5. ASP.NET Core中的OWASP Top 10 十大风险-SQL注入

    不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址 本博文翻译自: https://dotnetcoretutorials.com/201 ...

  6. Java表达式中的那些坑

    [1]您确定真正了解后缀表达式与前缀表达式的区别吗? public class IncrementDemo{ public static void main(String[] args) { int ...

  7. Java面试中遇到的坑【篇二面试干货】

    俗话说早起的鸟儿有虫吃,现在临年关越来越近,有跳槽的想法的同事也都打算年前做好功课年后入职,所谓年终奖拿了,工作换的也是水到渠成. 说到这里想必有同学要说了,年底了放着年终奖不拿为何要跳槽呢?这个就要 ...

  8. 可运行的Java RMI示例和踩坑总结

    简述 资料参考: https://docs.oracle.com/javase/tutorial/rmi/overview.html https://blog.csdn.net/bigtree_372 ...

  9. Next.js 在 Serverless 中从踩坑到破茧重生

    作者 杨苏博,偏后端的全栈开发,目前负责腾云扣钉的 Cloud Studio 产品.在团队中负责接技术架构设计与 Review.Cloud Studio 编辑器内核设计与开发.部分核心插件设计与开发: ...

随机推荐

  1. Intellij IDEA 2022 正式发布,这些功能真不错

    Intellij IDEA 2022 正式发布了,作为正版用户,胖哥赶紧更新了一波,好家伙!这几个功能确实很香啊.新版更新的东西真不少,不愧是一个大版本更新. 依赖分析 IDEA的依赖检查.依赖冲突解 ...

  2. optimoptions requires Optimization Toolbox(optimoptions 需要 Optimization Toolbox)解决方法

    问题:在下载版的matlab中做coursera的machine learning里的ex2,做到 1.2.3 Learning parameters using fminunc 时出现optimop ...

  3. JAVA语言学习day16--7月22日

    ###15用户选择功能 * A: 用户选择功能 * a: 案例代码 import java.util.ArrayList; import java.util.Scanner; /* * 超市管理系统主 ...

  4. Apache Doris ODBC Mysql外表在Ubuntu下使用方法及配置

    Apache Doris 社区 2022 年的总体规划,包括待开展或已开展.以及已完成但需要持续优化的功能.文档.社区建设等多方面,我们期待有更多的小伙伴参与进来讨论.同时也希望多多关注Doris,给 ...

  5. Java 18 新特性:使用Java代码启动jwebserver

    前几天分享了Java 18 新特性:简单Web服务器的jwebserver命令行功能. 今天换一种方式,使用Java代码来实现一个静态资源服务器. 详细步骤我录了个视频放到B站了,感兴趣的小伙伴可以点 ...

  6. [AcWing 29] 删除链表中重复的节点

    点击查看代码 /** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * L ...

  7. Kubernetes 从入门到进阶实战教程 (2021 最新万字干货版)

    作者:oonamao 毛江云,腾讯 CSIG 应用开发工程师原文:来源腾讯技术工程,https://tinyurl.com/ya3ennxf 写在前面 笔者今年 9 月从端侧开发转到后台开发,第一个系 ...

  8. 按照 Promise/A+ 规范逐行注释并实现 Promise

    0. 前言 面试官:「你写个 Promise 吧.」 我:「对不起,打扰了,再见!」 现在前端越来越卷,不会手写 Promise 都不好意思面试了(手动狗头.jpg).虽然没多少人会在业务中用自己实现 ...

  9. BottomNavigationView点击刷新问题

    BottomNavigationView点击刷新问题 问题: 项目中使用了BottomNavigationView作为底部导航栏. 点击item切换的时候发现会有闪烁现象出现. 原因: BottomN ...

  10. KMP算法学习以及小结(好马不吃回头草系列)

    首先请允许我对KMP算法的三位创始人Knuth,Morris,Pratt致敬,这三位优秀的算法科学家发明的这种匹配模式可以大大避免重复遍历的情况,从而使得字符串的匹配的速度更快,效率更高. 首先引入对 ...