引言
在写代码时,我们通常喜欢使用org.springframework.util.ObjectUtils#nullSafeEquals
来比较两个对象是否相等,从而避免使用equals
方法在对象为空时导致空指针异常。
最近在写代码时,我试图使用stream流的filter,配合使用ObjectUtils.nullSafeEquals
过滤出租户id不为0的数据。于是写了类似如下的代码。在具体讲述问题之前,我们先看下代码,大家判断下代码会输出什么?
public static void main(String[] args) {
List<Long> tenantIds = new ArrayList<>();
tenantIds.add(0L);
tenantIds.add(1L);
tenantIds.add(2L);
List<Long> list = tenantIds.stream()
.filter(id -> !ObjectUtils.nullSafeEquals(id, 0))
.collect(Collectors.toList());
System.out.println(list);
}
OK,我们运行下代码揭晓下答案,竟然并没有过滤掉为0的id。
排查
那为啥没有过滤掉0呢?我们来看下ObjectUtils.nullSafeEquals
的相关源码。
方法其实很直观,我们一行一行看下。
public static boolean nullSafeEquals(@Nullable Object o1, @Nullable Object o2) {
// 比较的两个对象引用地址相同,说明对象为同一个。返回true
if (o1 == o2) {
return true;
}
// 如果两个对象存在为null的,则返回false
if (o1 == null || o2 == null) {
return false;
}
// 由于上面的if校验,说明o1不为null,这里调用equals方法比较两个对象是否相等
if (o1.equals(o2)) {
return true;
}
// 这里是比较两个数组对象,调用了arrayEquals方法比较
if (o1.getClass().isArray() && o2.getClass().isArray()) {
return arrayEquals(o1, o2);
}
return false;
}
看完上述方法,再根据我们传入的参数。我们可以定位到问题发生在o1.equals(o2)
,那这里为啥返回了false?
我们知道equals方法为Object类的方法,所有类的父类。我们在编写自定义类时,通常会重写equals方法来实现当前类的比较。
由于咱们上面定义的tenantId为Long类型,那我们找到Long类中的equals方法,看看它做了什么就能知道原因了?源码如下
我们可以看到这里判断了传入的obj如果是Long类型或其子类,会把传入的对象强转为Long类型,调用其中的longValue方法与当前的Long对象维护的value进行比较。
ObjectUtils.nullSafeEquals(id, 0)
这里的两个0为啥不相等呢?我们简化下代码 ,使用IDEA中jclasslib插件看下字节码文件(这个插件推荐下)。
简化后的代码:
public static void main(String[] args) {
Long tenantId = 0L;
System.out.println(ObjectUtils.nullSafeEquals(tenantId, 0));
}
字节码
0 lconst_0
1 invokestatic #2 <java/lang/Long.valueOf : (J)Ljava/lang/Long;>
4 astore_1
5 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
8 aload_1
9 iconst_0
10 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
13 invokestatic #5 <org/springframework/util/ObjectUtils.nullSafeEquals : (Ljava/lang/Object;Ljava/lang/Object;)Z>
16 invokevirtual #6 <java/io/PrintStream.println : (Z)V>
19 return
我们可以看到ObjectUtils.nullSafeEquals(id, 0)
中的0,在编译时,会被编译器转换为 Integer的包装类。从而导致了运行结果与预期不符合的场景。
那我们怎么解决呢?
我们可以把ObjectUtils.nullSafeEquals(id, 0)
修改为ObjectUtils.nullSafeEquals(id, 0L)
,这样编译器就会把0L转换为Long类型的包装类,从而就能正确调用Long类的equals方法了。
思考
- 我们平时开发时,通常会犯一些很低级的错误,从而导致系统的bug出现。只是一个租户id为0的一条数据未能正确过滤,也许就会导致生产事故的发生。在去年,我所在的项目就遇到了一个空串导致的p0级别的生产事故,导致了很多数据异常删除,最后处理了一两天才完全把数据恢复好。这个后面有时间会作为负面教材给大家分享出来。
- 我们在使用一些轮子(方法)时,不要想当然的去使用,一定要有阅读源码的习惯,并做一些单元测试,来确保方法结果符合预期。
- 在编写代码时,一定要尽量考虑周全,提高代码的健壮性,该判空的时候不要觉得数据不可能为空就不去写兜底判断,对自己代码负责一些。
- 这个代码其实排查起来很快的,但是考虑到也许有刚入行的同学不明白,就给大家分享的细致了些,希望大家都能有所收获。