- 【强制】关于 hashCode 和 equals 的处理,遵循如下规则:
1) 只要重写 equals,就必须重写 hashCode。
2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法。
3) 如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals。
说明:String 因为重写了 hashCode 和 equals 方法,所以我们可以愉快地使用 String 对象作为 key 来使用。
User user = new User();
Map<User, String> userMap = new HashMap<>();
- 【强制】判断所有集合内部的元素是否为空,使用 isEmpty()方法,而不是 size()==0 的方式。
说明:前者的时间复杂度为 O(1),而且可读性更好。
正例:
Map<String, Object> map = null;
// NullPointerException
System.out.println(map.isEmpty());
Map<String, Object> map = new HashMap<>();
if(map.isEmpty()) {
System.out.println("no element in this map.");
}
- 【强制】在使用 java.util.stream.Collectors 类的 toMap()方法转为 Map 集合时,一定要使用含有参数类型为 BinaryOperator,参数名为 mergeFunction 的方法,否则当出现相同 key值时会抛出 IllegalStateException 异常。
说明:参数 mergeFunction 的作用是当出现 key 重复时,自定义对 value 的处理策略。
正例:
List<Pair<String, Double>> pairArrayList = new ArrayList<>(3);
pairArrayList.add(new Pair<>("version", 6.19));
pairArrayList.add(new Pair<>("version", 10.24));
pairArrayList.add(new Pair<>("version", 13.14));
Map<String, Double> map = pairArrayList.stream().collect(
// 生成的 map 集合中只有一个键值对:{version=13.14}
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));
反例:
String[] departments = new String[] {"iERP", "iERP", "EIBU"};
// 抛出 IllegalStateException 异常
Map<Integer, String> map = Arrays.stream(departments)
.collect(Collectors.toMap(String::hashCode, str -> str));
// 改进
String[] departments = new String[]{"iERP", "iERP", "EIBU"};
Map<Integer, String> map = Arrays.stream(departments)
.collect(Collectors.toMap(String::hashCode, String::valueOf, (v1, v2) -> v2));
- 【强制】在使用 java.util.stream.Collectors 类的 toMap()方法转为 Map 集合时,一定要注意当 value 为 null 时会抛 NPE 异常。
说明:在 java.util.HashMap 的 merge 方法里会进行如下的判断:
if (value == null || remappingFunction == null)
throw new NullPointerException();
反例:
List<Pair<String, Double>> pairArrayList = new ArrayList<>(2);
pairArrayList.add(new Pair<>("version1", 4.22));
pairArrayList.add(new Pair<>("version2", null));
Map<String, Double> map = pairArrayList.stream().collect(
// 抛出 NullPointerException 异常
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));
- 【强制】ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出 ClassCastException 异常:java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。
说明:subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList 而是 ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。
List<String> list = new ArrayList<>();
list.add("d");
list.add("33");
list.add("44");
list.add("55");
list.add("66");
// ClassCastException
ArrayList<String> list2 = (ArrayList) list.subList(0, 2);
总结
使用sublist()返回的只是原list对象的一个视图,因此Sublist内部类和ArrayList的内部保存数据的地址是一样得;
即它们在内存中是同一个List(集合),只是parentOffset ,size等参数不同
对SubList子列表的所有操作都会最终反映到原列表上。
ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常。
如果达到的效果要对子集进行操作,原始list不改变。建议以下方式:
List<Object> tempList = new ArrayList<Object>(list.subList(2, lists.size()));
-
【强制】使用 Map 的方法 keySet()/values()/entrySet()返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常。
-
【强制】Collections 类返回的对象,如:emptyList()/singletonList()等都是 immutable list,不可对其进行添加或者删除元素的操作。
反例:如果查询无结果,返回 Collections.emptyList()空集合对象,调用方一旦进行了添加元素的操作,就会触发 UnsupportedOperationException 异常。
通过java.util.Collections.emptyList()方法的相关源码可以得知它实际上就是返回了一个空的List,
但是这个List和我们平时常用的那个List是不一样的。这个方法返回的List是Collections类的一个静态内部类,
它继承AbstractList后并没有实现add()、remove()等方法,因此这个返回值List并不能增加删除元素。
既然这个List不能进行增删操作,那么它有何意义呢?
这个方法主要目的就是返回一个不可变的列表,使用这个方法作为返回值就不需要再创建一个新对象,可以减少内存开销。
并且返回一个size为0的List,调用者不需要校验返回值是否为null,所以建议使用这个方法返回可能为空的List。
项目中的使用
比如列表查询 返回集合的时候 如果没有符合条件的数据 可返回 EmptyList<>()
//obj为查询出来的集合
Object datas = obj == null ? Collections.EMPTY_LIST : obj;
emptySet()、emptyMap()方法同理。
Collections.singletonList()返回的是不可变的集合,但是这个长度的集合只有1,可以减少内存空间。
但是返回的值依然是Collections的内部实现类,同样没有add的方法,调用add,set方法会报错。
List<String> list = Collections.singletonList("1");
- 【强制】在 subList 场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生 ConcurrentModificationException 异常。
ArrayList中有个protected transient int modCount = 0;用来记录当前ArrayList被修改的次数。
比如add(),remove()等都会导致modeCount增加:
ArrayList.subList()会生成一个SubList的对象,SubList中有个对应modCount同步ArrayList中的modeCount:
SubList对象每次再遍历时,会将自己的modeCount与ArrayList的modeCount进行对比,如果两个值不一样就会报异常:
ConcurrentModificationException
- 【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。
反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现ClassCastException 错误。
正例:
List<String> list = new ArrayList<>(2);
list.add("guan");
list.add("bao");
String[] array = list.toArray(new String[0]);
说明:使用 toArray 带参方法,数组空间大小的 length,
1) 等于 0,动态创建与 size 相同的数组,性能最好。
2) 大于 0 但小于 size,重新创建大小等于 size 的数组,增加 GC 负担。
3) 等于 size,在高并发情况下,数组创建完成之后,size 正在变大的情况下,负面影响与 2 相同。
4) 大于 size,空间浪费,且在 size 处插入 null 值,存在 NPE 隐患。
反例:
List<String> list = new ArrayList<>(2);
list.add("guan");
list.add("bao");
// ClassCastException
String[] objects = (String[]) list.toArray();
- 【强制】在使用 Collection 接口任何实现类的 addAll()方法时,都要对输入的集合参数进行NPE 判断。
说明:在 ArrayList.addAll 方法的第一行代码即 Object[] a = c.toArray(); 其中 c 为输入集合参数,如果为 null,则直接抛出异常。
反例:
List<String> list = new ArrayList<>(4);
list.add("guan");
list.add("bao");
List<String> listadd = null;
// NullPointerException
list.addAll(listadd);
- 【强制】使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。
说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。
String[] str = new String[] { "yang", "hao" };
List list = Arrays.asList(str);
第一种情况:list.add(“yangguanbao”); 运行时异常。
第二种情况:str[0] = “changed”; 也会随之修改,反之亦然
反例:
String[] str = new String[] { "yang", "hao" };
List<String> list = Arrays.asList(str);
str[0] = "qjw";
// qjw hao
for (String s : list) {
System.out.println(s);
}
// UnsupportedOperationException
list.add("ccc");
- 【强制】泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用 add 方法,而<? super T>不能使用 get 方法,两者在接口调用赋值的场景中容易出错。
说明:扩展说一下 PECS(Producer Extends Consumer Super)原则:
第一、频繁往外读取内容的,适合用<? extends T>。
第二、经常往里插入的,适合用<? super T>
·通配符也会进行类型的擦除,即向上擦除到Object类。
·通配符的上界:<? extends T>,表示向上擦除的边界
·通配符的下界:<? super T>,表示寻找是否有T类型的基类实现了Comparable接口。
即:
< ? extends T> 表示该通配符所代表的类型是T类型的子类。
< ? super T> 表示该通配符所代表的类型是T类型的父类。
- 【强制】在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行instanceof 判断,避免抛出 ClassCastException 异常。
说明:毕竟泛型是在 JDK5 后才出现,考虑到向前兼容,编译器是允许非泛型集合与泛型集合互相赋值。
反例:
List<String> generics = null;
List notGenerics = new ArrayList(10);
notGenerics.add(new Object());
notGenerics.add(new Integer(1));
generics = notGenerics;
// 此处抛出 ClassCastException 异常
String string = generics.get(0);
// 改进
String string = generics.get(0) instanceof String ? generics.get(0) : null;
- 【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
正例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
反例:
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?
解读:
将字节码反编译
上图发现,我们简简单单的一句for (String item : list) { 在真实的字节码中却变成了红框中的部分。
在创建该对象时有这么一句赋值语句:
该字段在ArrayList中标识当前对象的修改次数(包括remove和add方法),跟踪代码不难发现都有这么一行代码modCount++;
查看while条件中的源码,并不会发生异常,暂且不管。
public boolean hasNext() {
return cursor != size;
}
仔细查看Itr代码中的next实现:
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
再看checkForComodification 方法,
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
结合上面的判断,在创建Itr对象时就将ArrayList的修改次数modCount赋值给Itr迭代器对象,如果在迭代期间ArrayList对象被操作(remove和add)了导致modCount 值修改,就会报异常ConcurrentModificationException 。
再分析
那么为什么当判断条件为“1”时
if ("1".equals(item)) 就不会报错呢?
查看while条件中的源码涉及到两个字段cursor和size,做个表格来一步一步记录下值的变化
public boolean hasNext() {
return cursor != size;
}
判断条件为if ("1".equals(item)) {
注:因为是两次add,所以导致了expectedModCount和modCount初始值为2
迭代次数 | cursor | size | expectedModCount | modCount |
---|---|---|---|---|
新创建 | 0 | 2 | 2 | 2 |
第一次next | 1 | 2 | 2 | 2 |
执行remove操作后 | 1 | 1 | 2 | 3 |
以上就能看到,当remove操作后就导致了cursor和size一致,也就退出了while循环,避免了异常抛出。
判断条件为if ("2".equals(item))
迭代次数 | cursor | size | expectedModCount | modCount |
---|---|---|---|---|
新创建 | 0 | 2 | 2 | 2 |
第一次next | 1 | 2 | 2 | 2 |
第二次next | 2 | 2 | 2 | 2 |
执行remove操作后 | 2 | 1 | 2 | 3 |
while判断条件仍为true,继续执行 | 2 | 1 | 2 | 3 |
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
所以在next代码块中的checkForComodification就抛出了异常,以上也就完美了解释了为什么当判断条件为2时抛异常了。
- 【强制】在 JDK7 版本及以上,Comparator 实现类要满足如下三个条件,不然 Arrays.sort,Collections.sort 会抛 IllegalArgumentException 异常。
说明:三个条件如下
1) x,y 的比较结果和 y,x 的比较结果相反。
2) x>y,y>z,则 x>z。
3) x=y,则 x,z 比较结果和 y,z 比较结果相同。
正例:
new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getId() == o2.getId() ?0 :(o1.getId() > o2.getId() ? 1 : -1);
}
};
反例:下例中没有处理相等的情况,交换两个对象判断结果并不互反,不符合第一个条件,在实际使用中可能会出现异常。
new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getId() > o2.getId() ? 1 : -1;
}
};
原因:摘自程序员大本营
使用的主要是TimSort.sort()方法。这个方法是一个优化版的归并排序。
获取两个有序数组A和B
传统的归并排序中,A和B都是从“一个元素”开始的;“一个元素”天然有序。
TimSort会通过插入排序等方法,先构建一些小的有序数组,以提高一点性能。
另外,在TimSort中,A和B都是入参a[]中的一个片段。这样可以节省一些内存空间。
找到待归并区间
从数组A中,找到A[n-1]<B[0] && A[n]>B[0],以及A[m]<B[length-1] && A[m+1]>B[length-1]。
此时,待归并区间就是A[n,m]和B。
准备操作
在正式开始归并之前,会做一些准备操作。包括将非待归并区间的数据移动到合适的位置上;准备一个临时数组、初始化一些指针数据等。如下图。
数组B被复制到了临时数组temp中。因为数组B的空间会被其他元素覆盖。
原数组A中最后一个元素“12”被放到了原数组B的最后一个位置上。因为这个元素比待归并区间所有元素都更大。
指针B1指向数组A'的第一个元素;C1指向A'的最后一个元素;B2指向B'的第一个元素;C2指向B'的最后一个元素。这四个指针是用来确定两个数组中待比较和待移动的数据范围的
指针D指向的位置,是下一个“已排序”元素的位置。也就是从A'和B'中找到的最大的元素,将会放到这个位置上。
归并操作
归并操作相对比较简单。依次比较A'[C1]和B'[C2],将较大的数值移动到D的位置上,并将D和对应的C1/C2向左移动一位。重复执行此操作,直到C1<B1或者C2<B2,然后将另一数组中剩下的数据直接写入到数组中即可。
下图是示例数据从准备操作(左上角标记0)到四次排序(左上角标记依次从1到4)的归并步骤。
TimSort的优化归并操作
TimSort在某些情况(触发条件待考)下,会对上述归并操作做一个优化。主要的优化点在于:不是一次一个元素的移动,而是尝试着一次移动多个元素。
下图是按优化后的逻辑,同样的示例数据从准备操作(左上角标记0)到完成排序的归并步骤。注意第一步和第二步每次都移动了两个元素。这里只用了5步就完成了归并;而优化前需要7步。
问题原因
问题原因是,对某些数据来说,上述代码会导致compare(a,b)<0并且compare(b,a)<0,也就是a<b && b<a。当这类数据遇到某些特殊情况时,就会发生这个异常。
例如,我们假定:
a<b && b<a,这是代码中出现的bug
假定输入数组a[] = {5,a,7,12,4,b,8,8},其中待归并的两个有序数组分别是{5,a,7,12}和{4,b,8,8}
假定b<7&&7>b。这样可以触发“特殊情况”,即:a和b在某一次归并操作后,会同时成为“是否移动元素”的临界条件。
这样,在“特殊情况”下,优化后的归并操作可能陷入死循环。用画图来表示是这样的。
获取两个有序数组A和B
首先,我们有两个有序数组A和B,如下图所示。
找到待归并区间、做好准备操作
这样,在划分完待归并区间后,得到的结果是这样的:
第一次归并操作:C2落在了元素b上
然后,开始第一次归并操作。由于B'[C2]>A'[C1],我们需要从C2开始,在数组B'中找到一个下标n,使得B'[n]<A'[C1]。找到之后,将B'(n,C2]复制到D的位置上。复制完成后,将C2和D都向左移动若干个位置。
这里需要注意两点:首先,临界点的比较条件是B'[n]<A'[C1],这是有顺序的;其次,复制的条件是B'(n,C2],这是个半包区间。
这样,第一轮归并完成后的结果是这样的:
第二次归并操作:C1落在了元素a上
接下来做第二次归并操作。由于A'[C1]>B'[C2](这是先决条件里的第三点:b<7&&7>b),我们需要从C1开始,从A'中找到一个下标m,使得A'[m]<B'[C2]。找到之后,将A'(m,C1]复制到D的位置上。复制完成后,将C1和D都向左移动若干个位置。
这里需要注意比较的顺序性和区间半包性。
这一轮操作完,得到的结果是:
第三、四步操作:出现空集、死循环
由于此时A'[C1]<B'[C2],我们需要重复第一次归并操作。先C2开始,在数组B'中找到一个下标n,使得B'[n]<A'[C1]。但是,由于b<a(注意顺序),这一轮找到的n会等于C2。这就导致了需要复制到D中的元素集合B'(n,C2]是一个空集——或者用伪代码来说,我们需要将一个长度为0的数组复制到D的位置上去。
然后,由于B'[C2]<A'[C1],我们需要重复第二次归并操作。但是很显然,由于a<b(同样注意顺序),我们又会得到一个空集。
如果不加干预,排序操作会在这里无限循环下去。TimSort中的干预方式就是当检测到空集时,抛出异常。
- 【推荐】集合泛型定义时,在 JDK7 及以上,使用 diamond 语法或全省略。
说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型。
正例:
// diamond 方式,即<>
HashMap<String, String> userCache = new HashMap<>(16);
// 全省略方式
ArrayList<User> users = new ArrayList(10);
- 【推荐】集合初始化时,指定集合初始值大小。
说明:HashMap 使用 HashMap(int initialCapacity) 初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可。
正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。
注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
反例:HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容量 7 次被迫扩大,resize 需要重建 hash 表。
当放置的集合元素个数达千万级别时,不断扩容会严重影响性能。
-
【推荐】使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。
说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用Map.forEach 方法。
正例:values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合。 -
【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:
反例:由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,而事实上,存储null 值时会抛出 NPE 异常。 -
【参考】合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。
说明:有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次序是一定的。
如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是 order/sort。 -
【参考】利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的contains()进行遍历去重或者判断包含操作。