Java编程常见陷阱(一)

Last Modified: 2022/10/26

Object#equals 方法空指针异常

在调用 equals 方法时应该确保调用对象不为空,否则很容易抛出 NullPointerException

myObject.equals(xxObject)

这里如果 myObject 为空,那么此时会抛出空指针异常。改进方法有两种:

  • 将确定不为空的对象作为调用对象,可能为空的作为参数。以上面为例,如果确信 xxObject 不为空,可以改为 xxObject.equals(myObject)
  • 使用 Objects.equals 方法的地方,可改为 Objects.equals(myObject, xxObject)。这种不用考虑对象是否为空的问题,推荐使用。

整形包装类的比较问题

结论:整形包装类的比较不应该使用 == 而应该使用 equals 方法比较。

例如:下面的例子,使用了等号比较,你会发现当比较的两个整形相等且值在 [-128 ~ 127] 之间,会输出 true,否则输出 false。

Integer a = 127;
Integer b = 127;
System.out.println(a == b); // out: true
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // out: false

因为值在 [-128 ~ 127] 区间的整数在赋值给 Integer 类型的变量时,Integer 对象是在 IntegerCache.cache 中产生的,也就是这个区间的相同值都会返回同一个 Integer 对象,而同一个对象的内存地址相同,所以 == 比较返回 true。

而那些不在 [-128 ~ 127] 区间的整数则不会用到 IntegerCache.cache,因此即便两个整形值相同,他们产生的对象的内存地址也不同,使用 == 比较自然也不会相等。

不仅是赋值,Integer#valueOf 方法也存在同样的问题,根本原因还是实现中使用了 IntegerCache.cache。 但是如果直接使用 new Integer(xx) 则不会有这个问题。

System.out.println(new Integer(1) == new Integer(1)); // out: false

记住那么多情况是没有必要的,在比较对象的时候,我们应该用 equals 而不是 ==,除非你确实就是想比较内存地址相同或基本数据类型的值是否相等。

浮点数比较问题

先看一段很简单的浮点计算,以你长达20几年的生活经验,觉得一定会输出 true,但当你看到实际运行结果的时候,还是会感到 amazing。

float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a == b); // out: false

根本原因是计算机使用二进制,二进制无法精确表示大部分的十进制小数。由于丢失进度,导致比较结果不相等。正确的做法是,永远不要使用浮点数比较。那如果一定比较呢?推荐两个方法:

方法一:指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的。

float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
float diff = 1e-6f;
if (Math.abs(a - b) < diff) {
    System.out.println("相等");
}

方法二:使用 BigDecimal 来定义值,再进行浮点数的运算操作。

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
if (x.equals(y)) {
    System.out.println("相等");
}

POJO 对象默认值问题

POJO 对象不应该设置默认值。假如一个 User 类包含 createTime 字段,如果默认值是 new Date(),那么当我们需要更新 User 的时候,可能会不小心将用户的创建时间也一起更新掉。

stream list 转 map 相同键值问题

Collectors.toMap有几个重载方法,我们这里关注下面两个方法:

  • m1: Collector.toMap(keyMapper, valueMapper)
  • m2: Collector.toMap(keyMapper, valueMapper, mergeFunction)

转 map 就会需要将 list 中的对象转化为键值对,keyMapper 用来确定 key,valueMapper 用来确定 value。 当使用 m1 方法,如果遇到两个对象转化后的键值相同,就会抛出 IllegalStateException 异常。

List<String> list = Arrays.asList("abc", "b", "abc");
list.stream().collect(Collectors.toMap(a -> a, b -> b));

运行以上代码会抛出异常,异常信息提示 abc 键值重复。原因是 list 中有两个 abc,这个两个值经过 keyMapper 方法处理后生成的键值都是 abc,导致异常的发生。异常堆栈如下:

java.lang.IllegalStateException: Duplicate key abc

	at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)
	at java.util.HashMap.merge(HashMap.java:1245)
	at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
	...

m1 方法的实现会出现以上异常,我们改用 m2 就可以避免该问题。

List<String> list = Arrays.asList("abc", "b", "abc");
list.stream().collect(Collectors.toMap(a -> a, b -> b, (a, b) -> b));

m2 方法的 mergeFunction 就是用来处理键值重复的:当重复 key 发生时,mergeFunction 第一个参数是之前 key 对应的 value 值或者上次调用 mergeFunction 返回的值,第二个参数是当前的 value 值。

mergeFunction 的函数体则是处理重复逻辑的地方,我们可以选择前一次值,也可以选择本次的值,甚至可以做任何逻辑,返回一个新值。

结论:在转 map 的时候,应该使用 m2 方法,以避免出现 key 值重复导致的异常。

然而,解决了 key 重复问题并不能高枕无忧。如果 valueMapper 返回值为 null,会抛出空指针异常。原因是 merge 实现在遇到 value 为空是,直接抛出空指针异常:

public V merge(K key, V value,
               BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    if (value == null)
        throw new NullPointerException();
    ...

List#subList 转 ArrayList 问题

ArrayList 的 subList 是不是也是 ArrayList 呢?听起来似乎没啥不合理,不如试一试?

List<String> list = new ArrayList<>();
list.add("1");
ArrayList<String> subList = (ArrayList<String>) list.subList(0, 1);

运行的结果却是:

java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList

结论:ArrayList 的 subList() 返回的是 java.util.ArrayList$SubList 类的实例,该类是 ArrayList 的内部类且和 ArrayList 类之间并无继承关系,所以强制转换会报错。

Map 的 keySet/values/entrySet 方法返回的集合追加元素问题

结论:Map 的这个三个方法返回的集合追加元素都会报错,会抛出 UnsupportedOperationException。

Map 的 keySet()entrySet() 返回的对象的 add 方法实现在 AbstractSet 中,实现是直接抛出 UnsupportedOperationException。
Map 的 values() 返回的对象的 add 方法实现在 AbstractCollection 中,实现也是直接抛出 UnsupportedOperationException。

Collections 的 emptyList/singletonList 追加和删除元素问题

Collections 的 emptyList 和 singletonList 方法返回的 list 是不可修改的,对返回的 list 追加元素和删除元素均会抛出 UnsupportedOperationException。

Collection#addAll 空指针问题

任何实现了 Collection 接口的实现类,在调用 addAll 方法时,都需要对方法参数判空,否则一旦参数为空,就会抛出空指针异常。

List<String> list = new ArrayList<>();
List<String> list2 = null;
if (list2 != null) { // // 确保addAll方法的参数不为空
    list.addAll(list2);
}

结论:在调用 addAll 方法之前,先判空,避免空指针问题。

Arrays#asList 方法

Arrays#asList 方法返回的 list 也是不可变的,即不可以增加元素,删除元素和清空元素,调用这些方法会抛出 UnsupportedOperationException。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。也就是说修改数组,会反映到 list 上,修改 list(不包括 add/remove/clear 等会改变数组长度和结构的方法) 也会反映到数组中。

String[] str = new String[] { "chen", "yang", "hao" };
List<String> list = Arrays.asList(str);
str[0] = "li";
System.out.println(list.get(0)); // 输出:li。我们通过改变数组第1个元素,从而反映到了list上。
list.set(0, "chen");
System.out.println(str[0]); // 输出:chen。同样我们改变list也反映到了数组上。
有问题吗?点此反馈!

温馨提示:反馈需要登录