大家好,我是码牛,又跟大家见面啦!
身为 Java 程序员的您,不知是否曾踏入过某些基础知识的误区。时而,某个漏洞,您耗费大量时间查找,最终惊觉竟是一个极为低级的差错。时而,某些代码,对于这一批数据其功能正常,然而更换一批数据便出现了异常状况。时而,您或许会望着某行代码瞠目结舌,心中思忖:这行代码缘何出错?今日与诸位一同探讨 99%的 Java 程序员已然踏入,亦或即将面临的 5 个误区。
1. 用“==”号比较的误区
不知您在项目中是否目睹过,有些同仁针对 Integer 类型的两个参数运用“==”号来比较是否相等?反正于我而言是见过的,那么此种用法正确与否?我的答复是视具体情形而定,不可断言一定正确,亦或错误。
某些状态字段,例如:orderStatus 存有:-1(未下单)、0(已下单)、1(已支付)、2(已完成)、3(取消),这 5 种状态。此时倘若使用“==”判断是否相等:
返回结果会是 true 吗?答案:为 false 。
部分同学或许会予以反驳,在 Integer 当中不是存在范围为:-128 至 127 的缓存吗?缘何会是 false ?首先来审视一下 Integer 的构造方法:
它实际上并未运用到缓存。那么缓存是在何处被使用的?答案存在于 valueOf 方法之中:
如果上面的判断改成这样:
返回结果会是 true 吗?答案:确为 true 。
我们应当养成优良的编码习惯,尽可能减少使用“==”来判断两个 Integer 类型数据是否相等,唯有在上述极为特殊的场景下二者才相等。而应当变更为使用 equals 方法进行判断:
运行结果为true。
2. Objects.equals的坑
假设当下存在这样一个需求:对当前登录的用户予以判定,如果其为我们所指定的系统管理员,那么发送一封邮件。
系统管理员不存在特殊的字段标识,其用户 id 等于 888,在开发、测试以及生产环境中该数值均是相同的。此需求着实极易实现:
从当前登录用户的上下文当中获取用户信息,并进行判断。倘若用户信息为空,那么直接返回。若获取到的用户信息不为空,接下来则判断用户 id 是否等同于 888 。若等于 888 ,则发送邮件;若不等于 888 ,则不进行任何操作。
当我们使用 id 为 888 的系统管理员账号登录之后,进行了相关操作,满心期待地准备接收邮件之时,却发现一无所获。其后,发现 UserInfo 类是如此定义的:
此时,部分伙伴或许会言:未察觉出有何问题呀。然而我所要陈述的是,此段代码的确存在问题。
究竟是何问题呢?接下来我们着重审视它的 equals 方法:
equals 方法的判断逻辑如下:该方法首先判断对象 a 和 b 的引用是否相等,若相等则直接返回 true 。倘若引用不相等,则判别 a 是否为空,若 a 为空则返回 false 。
若 a 不为空,则调用对象的 equals 方法进一步判定值是否相等。这便要从 Integer 的 equals 方法谈起了。其 equals 方法的具体代码如下:
首先判断参数 obj 是否为 Integer 类型,若否,则直接返回 false 。若为 Integer 类型,再进一步判断 int 值是否相等。
而在上述这个例子当中,b 是 long 类型,所以 Integer 的 equals 方法直接返回了 false 。也就是说,倘若调用了 Integer 的 equals 方法,必然要求传入的参数也是 Integer 类型,否则该方法将会直接返回 false 。
除此之外,还有 Byte 、Short 、Double 、Float 、Boolean 和 Character 也具备类似的 equals 方法判断逻辑。
常见的陷阱有:Long 类型与 Integer 类型的比较,例如:用户 id 的场景。Byte 类型与 Integer 类型的比较,例如:状态判断的场景。Double 类型与 Integer 类型的比较,例如:金额为 0 的判断场景。
3. BigDecimal的坑
通常我们会把一些小数类型的字段(比如:金额),定义成BigDecimal,而不是Double,避免丢失精度问题。
使用Double时可能会有这种场景:
正常情况下预计amount2 - amount1应该等于0.01
但是执行结果,却为:
实际结果小于预计结果。Double 类型的两个参数相减会被转换为二进制,由于 Double 的有效位数为 16 位,这便会产生存储小数位数不足的状况,在此种情形下就会出现误差。
常识昭示我们,运用 BigDecimal 能够规避精度的丢失。然而,使用 BigDecimal 真能避免丢失精度吗?答案是否定的。缘何如此?
这个例子中定义了两个BigDecimal类型参数,使用构造函数初始化数据,然后打印两个参数相减后的值。
结果:
不科学呀,为啥还是丢失精度了?
Jdk中BigDecimal的构造方法上有这样一段描述:
大致的含义为,此构造函数的结果或许难以预测,可能会呈现出创建时为 0.1 ,但实际却是 0.1000000000000000055511151231257827021181583404541015625 的情形。
由此观之,运用 BigDecimal 构造函数来初始化对象,同样会丧失精度。那么,究竟怎样才能不丢失精度呢?
我们可以使用Double.toString方法,对double类型的小数进行转换,这样能保证精度不丢失。
其实,还有更好的办法:
使用BigDecimal.valueOf方法初始化BigDecimal类型参数,也能保证精度不丢失。在新版的阿里巴巴开发手册中,也推荐使用这种方式创建BigDecimal参数。
4. Java8 filter的坑
对于Java8中的Stream用法,大家肯定再熟悉不过了。
我们通过对集合的Stream操作,可以实现:遍历集合、过滤数据、排序、判断、转换集合等等,N多功能。
这里重点说说数据的过滤。
在没有Java8之前,我们过滤数据一般是这样做的:
通常需要另一个集合辅助完成这个功能。
但如果使用Java8的filter功能,代码会变得简洁很多,例如:
代码简化了很多,完美。
但如果你对过滤后的数据,做修改了:
你当时可能只是想修改过滤后的数据,但实际上,你会把元素数据一同修改了。
意不意外,惊不惊喜?
其根本原因是:过滤后的集合中,保存的是对象的引用,该引用只有一份数据。
也就是说,只要有一个地方,把该引用对象的成员变量的值,做修改了,其他地方也会同步修改。
如下图所示:
5. 自动拆箱的坑
Java5之后,提供了自动装箱和自动拆箱的功能。
自动装箱是指:JDK会把基本类型,自动变成包装类型。
比如:
等价于:
而自动拆箱是指:JDK会把包装类型,自动转换成基本类型。
例如:
等价于:
但实际工作中,我们在使用自动拆箱时,往往忘记了判空,导致出现NullPointerException异常。
5.1 运算
很多时候,我们需要对传入的数据进行计算,例如:
如果传入了null值:
则会直接报错。
5.2 传参
有时候,我们定义的某个方法是基本类型,但实际上传入了包装类,比如:
如果出现add方法报NullPointerException异常,你可能会懵逼,int类型怎么会出现空指针异常呢?
其实,这个问题出在:Integer类型的参数,其实际传入值为null,JDK字段拆箱,调用了它的intValue方法导致的问题。
说实话,不得不承认这里所涵盖的内容本质上皆属于相当基础的范畴。然而,往往越是基础的事物,越容易使人麻痹大意,从而犯下疏忽性的错误,导致如同“大意失荆州”般的后果,并且陷入各种意想不到的陷阱之中。
从过往的经验以及众多案例分析来看,基础的知识和操作常常被人们轻视,认为其简单易懂,无需过多关注。但正是这种轻视的态度,使得人们在处理基础内容时,缺乏应有的谨慎和细致,进而增加了踩坑的可能性。
最后,让我们进行一番统计。在此,想询问一下,在面对这些看似简单却暗藏玄机的基础坑时,一个都未曾踩过的同学,可否麻烦您举个手,让大家能够以您为榜样,汲取经验和力量!