Java编程常见陷阱(一)
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也反映到了数组上。
温馨提示:反馈需要登录