集合处理规约

  1. 【强制】关于 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<>();
  1. 【强制】判断所有集合内部的元素是否为空,使用 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.");
  }
  1. 【强制】在使用 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));
  1. 【强制】在使用 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));
  1. 【强制】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()));
  1. 【强制】使用 Map 的方法 keySet()/values()/entrySet()返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常。

  2. 【强制】Collections 类返回的对象,如:emptyList()/singletonList()等都是 immutable list,不可对其进行添加或者删除元素的操作。
    反例:如果查询无结果,返回 Collections.emptyList()空集合对象,调用方一旦进行了添加元素的操作,就会触发 UnsupportedOperationException 异常。

通过java.util.Collections.emptyList()方法的相关源码可以得知它实际上就是返回了一个空的List,
但是这个List和我们平时常用的那个List是不一样的。这个方法返回的ListCollections类的一个静态内部类,
它继承AbstractList后并没有实现add()remove()等方法,因此这个返回值List并不能增加删除元素。
既然这个List不能进行增删操作,那么它有何意义呢?
这个方法主要目的就是返回一个不可变的列表,使用这个方法作为返回值就不需要再创建一个新对象,可以减少内存开销。
并且返回一个size为0List,调用者不需要校验返回值是否为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");
  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
  1. 【强制】使用集合转数组的方法,必须使用集合的 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();
  1. 【强制】在使用 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);
  1. 【强制】使用工具类 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");
  1. 【强制】泛型通配符<? 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类型的父类。
  1. 【强制】在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行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;
  1. 【强制】不要在 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
迭代次数cursorsizeexpectedModCountmodCount
新创建0222
第一次next1222
执行remove操作后1123
以上就能看到,当remove操作后就导致了cursor和size一致,也就退出了while循环,避免了异常抛出。
判断条件为if ("2".equals(item)) 
迭代次数cursorsizeexpectedModCountmodCount
新创建0222
第一次next1222
第二次next2222
执行remove操作后2123
while判断条件仍为true,继续执行2123
final void checkForComodification() {
     if (modCount != expectedModCount)
         throw new ConcurrentModificationException();
     }
所以在next代码块中的checkForComodification就抛出了异常,以上也就完美了解释了为什么当判断条件为2时抛异常了。
  1. 【强制】在 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中的干预方式就是当检测到空集时,抛出异常。 
  1. 【推荐】集合泛型定义时,在 JDK7 及以上,使用 diamond 语法或全省略。
    说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型。

正例:

// diamond 方式,即<>
HashMap<String, String> userCache = new HashMap<>(16);
// 全省略方式
ArrayList<User> users = new ArrayList(10);
  1. 【推荐】集合初始化时,指定集合初始值大小。
    说明:HashMap 使用 HashMap(int initialCapacity) 初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可。
正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。
注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
反例:HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容量 7 次被迫扩大,resize 需要重建 hash 表。
当放置的集合元素个数达千万级别时,不断扩容会严重影响性能。
  1. 【推荐】使用 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 值组合集合。

  2. 【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:
    在这里插入图片描述
    反例:由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,而事实上,存储null 值时会抛出 NPE 异常。

  3. 【参考】合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。
    说明:有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次序是一定的。
    如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是 order/sort。

  4. 【参考】利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的contains()进行遍历去重或者判断包含操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值