Alibaba开发规范_编程规约之集合框架:最佳实践与常见陷阱
文章目录
- 引言
- 1. hashCode与equals方法的覆写
- 1.1 规则
- 1.2 解释
- 1.3 代码示例
- 正例
- 反例
- 2. ArrayList的subList方法
- 2.1 规则
- 2.2 解释
- 2.3 代码示例
- 正例
- 反例
- 3. Map的keySet、values和entrySet方法
- 3.1 规则
- 3.2 解释
- 3.3 代码示例
- 正例
- 反例
- 4. Collections类返回的不可变集合
- 4.1 规则
- 4.2 解释
- 4.3 代码示例
- 正例
- 反例
- 5. subList场景中的并发修改异常
- 5.1 规则
- 5.2 解释
- 5.3 代码示例
- 正例
- 反例
- 6. 集合转数组的方法
- 6.1 规则
- 6.2 解释
- 6.3 代码示例
- 正例
- 反例
- 7. addAll方法的NPE判断
- 7.1 规则
- 7.2 解释
- 7.3 代码示例
- 正例
- 反例
- 8. Arrays.asList方法的限制
- 8.1 规则
- 8.2 解释
- 8.3 代码示例
- 正例
- 反例
- 9. 泛型通配符的使用
- 9.1 规则
- 9.2 解释
- 9.3 代码示例
- 正例
- 反例
- 10. 泛型集合的赋值与类型检查
- 10.1 规则
- 10.2 解释
- 10.3 代码示例
- 正例
- 反例
- 11. foreach循环中的remove/add操作
- 11.1 规则
- 11.2 解释
- 11.3 代码示例
- 正例
- 反例
- 12. Comparator实现类的条件
- 12.1 规则
- 12.2 解释
- 12.3 代码示例
- 正例
- 反例
- 13. 泛型定义的diamond语法
- 13.1 规则
- 13.2 解释
- 13.3 代码示例
- 正例
- 反例
- 14. 集合初始化时指定初始值大小
- 14.1 规则
- 14.2 解释
- 14.3 代码示例
- 正例
- 反例
- 15. 使用entrySet遍历Map
- 15.1 规则
- 15.2 解释
- 15.3 代码示例
- 正例
- 反例
- 16. Map类集合的K/V存储null值
- 16.1 规则
- 16.2 解释
- 16.3 代码示例
- 正例
- 反例
- 17. 集合的有序性与稳定性
- 17.1 规则
- 17.2 解释
- 17.3 代码示例
- 正例
- 反例
- 18. 使用Set进行去重操作
- 18.1 规则
- 18.2 解释
- 18.3 代码示例
- 正例
- 反例
- 小结
引言
Java集合框架是Java编程中最常用的工具之一,它提供了丰富的接口和类来存储、操作和处理数据集合。然而,由于集合框架的复杂性和灵活性,在实际使用中常常会遇到一些陷阱和问题。
1. hashCode与equals方法的覆写
1.1 规则
- 覆写equals方法时,必须覆写hashCode方法。
- Set存储的对象必须覆写hashCode和equals方法,因为Set依赖于这两个方法来判断对象的唯一性。
- 自定义对象作为Map的键时,必须覆写hashCode和equals方法。
1.2 解释
hashCode
和equals
方法是Java中用于对象比较的两个重要方法。equals
方法用于判断两个对象是否相等,而hashCode
方法则用于生成对象的哈希码。在集合框架中,hashCode
方法主要用于散列表(如HashMap
、HashSet
)中快速定位对象。
哈希碰撞是不可避免的,因为输入空间远远大于输出空间。例如,256位的哈希值,输出空间只有2的256次方。根据鸽笼原理,哈希碰撞是必然存在的。
鸽笼原理(抽屉原理)就是"如果有五个鸽子笼,养鸽人养了6只鸽子,那么当鸽子飞回笼中后,至少有一个笼子中装有2只鸽子。"
1.3 代码示例
正例
public class User {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
反例
public class User {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
// 未覆写hashCode方法
}
在反例中,User
类只覆写了equals
方法,但没有覆写hashCode
方法。这将导致在使用HashSet
或HashMap
时,可能会出现无法正确识别对象唯一性的问题。
2. ArrayList的subList方法
2.1 规则
- ArrayList的subList结果不可强转成ArrayList,否则会抛出
ClassCastException
异常。 - subList返回的是ArrayList的内部类SubList,它是原列表的一个视图,对子列表的所有操作会反映到原列表上。
2.2 解释
subList
方法返回的是一个视图,而不是一个新的ArrayList
。这个视图是基于原列表的,因此对子列表的修改会直接影响原列表。
2.3 代码示例
正例
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List<Integer> subList = list.subList(1, 3);
subList.set(0, 99);
System.out.println(list); // 输出: [1, 99, 3, 4, 5]
反例
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
ArrayList<Integer> subList = (ArrayList<Integer>) list.subList(1, 3); // 抛出ClassCastException
在反例中,尝试将subList
的结果强制转换为ArrayList
,这将导致ClassCastException
异常。
3. Map的keySet、values和entrySet方法
3.1 规则
- 使用Map的keySet()、values()、entrySet()返回的集合对象时,不可对其进行添加元素操作,否则会抛出
UnsupportedOperationException
异常。
3.2 解释
keySet()
、values()
和entrySet()
返回的集合是Map的视图,它们不允许直接添加元素。因为这些视图是基于Map的,添加元素会导致Map的结构发生变化,从而引发异常。
3.3 代码示例
正例
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
Set<String> keys = map.keySet();
for (String key : keys) {
System.out.println(key);
}
反例
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
Set<String> keys = map.keySet();
keys.add("c"); // 抛出UnsupportedOperationException
在反例中,尝试向keySet
返回的集合中添加元素,这将导致UnsupportedOperationException
异常。
4. Collections类返回的不可变集合
4.1 规则
- Collections类返回的对象,如emptyList()、singletonList()等是不可变的,不可对其进行添加或删除元素的操作。
4.2 解释
Collections
类提供了一些静态方法来返回不可变的集合对象。这些集合对象在创建后不允许修改,任何尝试修改的操作都会抛出UnsupportedOperationException
异常。
4.3 代码示例
正例
List<String> emptyList = Collections.emptyList();
System.out.println(emptyList.size()); // 输出: 0
反例
List<String> emptyList = Collections.emptyList();
emptyList.add("a"); // 抛出UnsupportedOperationException
在反例中,尝试向emptyList
中添加元素,这将导致UnsupportedOperationException
异常。
5. subList场景中的并发修改异常
5.1 规则
- 在subList场景中,高度注意对原集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生
ConcurrentModificationException
异常。
5.2 解释
subList
返回的视图是基于原列表的,如果原列表在子列表操作期间被修改(如增加或删除元素),子列表的遍历或修改操作可能会抛出ConcurrentModificationException
异常。
5.3 代码示例
正例
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List<Integer> subList = list.subList(1, 3);
for (Integer i : subList) {
System.out.println(i);
}
反例
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List<Integer> subList = list.subList(1, 3);
list.remove(0); // 修改原列表
for (Integer i : subList) {
System.out.println(i); // 抛出ConcurrentModificationException
}
在反例中,原列表在子列表遍历期间被修改,导致ConcurrentModificationException
异常。
6. 集合转数组的方法
6.1 规则
- 使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全一致、长度为0的空数组。
6.2 解释
toArray(T[] array)
方法允许将集合转换为指定类型的数组。传入长度为0的空数组可以确保动态创建与集合大小相同的数组,性能最好。
6.3 代码示例
正例
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
String[] array = list.toArray(new String[0]);
System.out.println(Arrays.toString(array)); // 输出: [a, b, c]
反例
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
String[] array = (String[]) list.toArray(); // 抛出ClassCastException
在反例中,直接使用无参toArray
方法返回的是Object[]
类型,强制转换为String[]
类型会导致ClassCastException
异常。
7. addAll方法的NPE判断
7.1 规则
- 在使用Collection接口任何实现类的addAll()方法时,都要对输入的集合参数进行NPE判断。
7.2 解释
addAll
方法会将输入集合中的所有元素添加到当前集合中。如果输入集合为null
,则会抛出NullPointerException
异常。
7.3 代码示例
正例
List<String> list1 = new ArrayList<>(Arrays.asList("a", "b"));
List<String> list2 = new ArrayList<>(Arrays.asList("c", "d"));
list1.addAll(list2);
System.out.println(list1); // 输出: [a, b, c, d]
反例
List<String> list1 = new ArrayList<>(Arrays.asList("a", "b"));
List<String> list2 = null;
list1.addAll(list2); // 抛出NullPointerException
在反例中,list2
为null
,调用addAll
方法时会抛出NullPointerException
异常。
8. Arrays.asList方法的限制
8.1 规则
- 使用Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,如
add
、remove
、clear
,否则会抛出UnsupportedOperationException
异常。
8.2 解释
Arrays.asList
返回的集合是一个固定大小的列表,它不支持添加或删除元素的操作。这是因为Arrays.asList
返回的列表是基于原始数组的,数组的大小是固定的。
8.3 代码示例
正例
String[] array = {"a", "b", "c"};
List<String> list = Arrays.asList(array);
System.out.println(list); // 输出: [a, b, c]
反例
String[] array = {"a", "b", "c"};
List<String> list = Arrays.asList(array);
list.add("d"); // 抛出UnsupportedOperationException
在反例中,尝试向Arrays.asList
返回的列表中添加元素,这将导致UnsupportedOperationException
异常。
9. 泛型通配符的使用
9.1 规则
- 泛型通配符
<? extends T>
来接收返回的数据时,此写法的泛型集合不能使用add方法。 - 泛型通配符
<? super T>
不能使用get方法。
9.2 解释
<? extends T>
表示泛型类型是T
或T
的子类,这种类型的集合只能读取数据,不能写入数据。<? super T>
表示泛型类型是T
或T
的父类,这种类型的集合只能写入数据,不能读取数据。
9.3 代码示例
正例
List<? extends Number> numbers = new ArrayList<>(Arrays.asList(1, 2, 3));
Number num = numbers.get(0); // 可以读取
// numbers.add(4); // 编译错误,不能写入
反例
List<? super Number> numbers = new ArrayList<>();
numbers.add(1); // 可以写入
// Number num = numbers.get(0); // 编译错误,不能读取
在反例中,尝试从<? super Number>
类型的集合中读取数据,这将导致编译错误。
10. 泛型集合的赋值与类型检查
10.1 规则
- 在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行instanceof判断,避免抛出
ClassCastException
异常。
10.2 解释
Java的泛型是编译时的类型检查,运行时泛型信息会被擦除。因此,在将无泛型限制的集合赋值给泛型限制的集合时,可能会导致类型不匹配的问题。
10.3 代码示例
正例
List<String> generics = new ArrayList<>();
List notGenerics = new ArrayList();
notGenerics.add("a");
notGenerics.add(1);
for (Object obj : notGenerics) {
if (obj instanceof String) {
generics.add((String) obj);
}
}
System.out.println(generics); // 输出: [a]
反例
List<String> generics = new ArrayList<>();
List notGenerics = new ArrayList();
notGenerics.add("a");
notGenerics.add(1);
generics = notGenerics; // 编译通过,但运行时抛出ClassCastException
String str = generics.get(1); // 抛出ClassCastException
在反例中,将无泛型限制的集合赋值给泛型限制的集合,导致在运行时抛出ClassCastException
异常。
11. foreach循环中的remove/add操作
11.1 规则
- 不要在foreach循环里进行元素的remove/add操作。remove元素请使用
Iterator
方式,如果并发操作,需要对Iterator
对象加锁。
11.2 解释
foreach
循环底层使用的是Iterator
,如果在遍历过程中修改集合(如删除或添加元素),会导致Iterator
的游标错乱,从而抛出ConcurrentModificationException
异常。
11.3 代码示例
正例
List<String> list = new ArrayList<>(Arrays.asList("1", "2"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("1".equals(item)) {
iterator.remove();
}
}
System.out.println(list); // 输出: [2]
反例
List<String> list = new ArrayList<>(Arrays.asList("1", "2"));
for (String item : list) {
if ("1".equals(item)) {
list.remove(item); // 抛出ConcurrentModificationException
}
}
在反例中,尝试在foreach
循环中删除元素,这将导致ConcurrentModificationException
异常。
12. Comparator实现类的条件
12.1 规则
- 在JDK7及以上版本,Comparator实现类要满足自反性、传递性和对称性,否则
Arrays.sort
、Collections.sort
会抛出IllegalArgumentException
异常。
12.2 解释
Comparator
接口用于定义对象的排序规则。如果Comparator
实现类不满足自反性、传递性和对称性,排序操作可能会导致不可预期的结果。
12.3 代码示例
正例
Comparator<Integer> comparator = (o1, o2) -> o1.compareTo(o2);
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 2));
Collections.sort(list, comparator);
System.out.println(list); // 输出: [1, 2, 3]
反例
Comparator<Integer> comparator = (o1, o2) -> o1 > o2 ? 1 : -1; // 不满足自反性
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 2));
Collections.sort(list, comparator); // 抛出IllegalArgumentException
在反例中,Comparator
实现类不满足自反性,导致Collections.sort
抛出IllegalArgumentException
异常。
13. 泛型定义的diamond语法
13.1 规则
- 在JDK7及以上,使用diamond语法或全省略来定义集合泛型。
13.2 解释
diamond语法(即<>
)允许在创建泛型对象时省略类型参数,编译器会自动推断类型。这可以减少代码冗余,提高代码的可读性。
13.3 代码示例
正例
Map<String, String> map = new HashMap<>(16);
List<String> list = new ArrayList<>(10);
反例
Map<String, String> map = new HashMap<String, String>(16); // 冗余的类型参数
List<String> list = new ArrayList<String>(10); // 冗余的类型参数
在反例中,类型参数是冗余的,使用diamond语法可以简化代码。
14. 集合初始化时指定初始值大小
14.1 规则
- 集合初始化时,指定集合初始值大小。
14.2 解释
指定集合的初始容量可以减少集合在扩容时的性能开销。特别是在处理大量数据时,频繁的扩容操作会严重影响性能。
14.3 代码示例
正例
Map<String, String> map = new HashMap<>(16);
List<String> list = new ArrayList<>(10);
反例
Map<String, String> map = new HashMap<>(); // 未指定初始容量
List<String> list = new ArrayList<>(); // 未指定初始容量
在反例中,未指定集合的初始容量,可能导致集合在添加元素时频繁扩容,影响性能。
15. 使用entrySet遍历Map
15.1 规则
- 使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历。
15.2 解释
entrySet
方法返回的是Map.Entry
对象的集合,它包含了键和值。使用entrySet
遍历Map
时,只需要遍历一次即可获取键和值,而使用keySet
方式则需要遍历两次。
15.3 代码示例
正例
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
反例
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key)); // 遍历两次
}
在反例中,使用keySet
方式遍历Map
时,需要遍历两次,效率较低。
16. Map类集合的K/V存储null值
16.1 规则
- 高度注意Map类集合K/V能不能存储null值的情况。
集合类 | Key | Value | Super | 说明 |
---|---|---|---|---|
HashTable | 不允许为null | 不允许为null | Dictionary | 线程安全 |
ConcurrentHashMap | 不允许为null | 不允许为null | AbstractMap | 锁分段技术(JDK8:CAS) |
TreeMap | 不允许为null | 允许为null | AbstractMap | 线程不安全 |
HashMap | 允许为null | 允许为null | AbstractMap | 线程不安全 |
16.2 解释
不同的Map
实现类对null
值的支持不同。例如,HashMap
允许key
和value
为null
,而ConcurrentHashMap
不允许key
和value
为null
。
16.3 代码示例
正例
Map<String, Integer> map = new HashMap<>();
map.put(null, 1);
map.put("a", null);
System.out.println(map); // 输出: {null=1, a=null}
反例
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put(null, 1); // 抛出NullPointerException
map.put("a", null); // 抛出NullPointerException
在反例中,尝试向ConcurrentHashMap
中插入null
值,这将导致NullPointerException
异常。
17. 集合的有序性与稳定性
17.1 规则
- 合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。
17.2 解释
有序性是指集合中的元素按某种规则排列,稳定性是指集合每次遍历的元素次序是一定的。例如,ArrayList
是有序但不稳定的,TreeSet
是有序且稳定的。 ArrayList 是 order/unsort
;HashMap 是 unorder/unsort
;TreeSet 是 order/sort
17.3 代码示例
正例
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 2));
Collections.sort(list);
System.out.println(list); // 输出: [1, 2, 3]
反例
Set<Integer> set = new HashSet<>(Arrays.asList(3, 1, 2));
System.out.println(set); // 输出顺序不确定
在反例中,HashSet
是无序的,输出的元素顺序不确定。
18. 使用Set进行去重操作
18.1 规则
- 利用Set元素唯一的特性,可以快速对一个集合进行去重操作,避免使用List的
contains
方法进行遍历、对比、去重操作。
18.2 解释
Set
集合中的元素是唯一的,因此可以利用Set
来快速去重。而List
的contains
方法需要遍历整个集合,效率较低。 list contains
方法是迭代比较去 去重。从0开始比,依次比,集合多,会很慢。而set
底层是依据hashCode
和equals
进行判断
18.3 代码示例
正例
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "a", "c"));
Set<String> set = new HashSet<>(list);
System.out.println(set); // 输出: [a, b, c]
反例
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "a", "c"));
List<String> uniqueList = new ArrayList<>();
for (String item : list) {
if (!uniqueList.contains(item)) {
uniqueList.add(item);
}
}
System.out.println(uniqueList); // 输出: [a, b, c]
在反例中,使用List
的contains
方法进行去重操作,效率较低。
小结
Java集合框架提供了强大的功能,但也伴随着一些潜在的陷阱。通过遵循最佳实践,开发者可以避免常见的错误,并编写出高效、健壮的代码。