一、Collection接口
List是有序的队列,List中可以有重复的元素;
Set是数学概念中的集合,Set中没有重复元素!
二 、list集合源码解析
1、 Arraylist
数据结构:
特点:
- arrayList可以存放null。
- arrayList本质上就是一个elementData数组(默认值为10的数组)。
- arrayList区别于数组的地方在于
能够自动扩展大小
,其中关键的方法就是gorw()方法。 - arrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而clear是全是删除集合中的元素。
- arrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很多,有移动很多数据才能达到应有的效果
- arrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环。
- 新的容量 = 原始容量 * 1.5 --> int newCapacity = oldCapacity + (oldCapacity >> 1), --扩容代码
int newCapacity = oldCapacity + oldCapacity / 2
2、 LinkList
数据结构:(双向链表)
特点:
- linkedList本质上是一个双向链表,通过一个Node内部类实现的这种链表结构。
- 能存储null值
- 跟arrayList相比较,就真正的知道了,LinkedList在删除和增加等操作上性能好,而ArrayList在查询的性能上好
- 从源码中看,它不存在容量不足的情况(不需要指定元素个数,能链多长就链多长)
- linkedList不光能够向前迭代,还能像后迭代,并且在迭代的过程中,可以修改值、添加值、还能移除值。
- linkedList不光能当链表,还能当队列使用,这个就是因为实现了Deque接口。
3、Vector
- 矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。
- 但是ArrayList是非线程安全的,而Vector是线程安全的。
4. Stack
- 栈,它继承于Vector。
- 它的特性是:先进后出(FILO, First In Last Out)。
5.List总结
a.各自的使用场景:
1. 对于需要快速插入,删除元素,应该使用LinkedList。
2. 对于需要快速随机访问元素,应该使用ArrayList。
3. 对于“单线程环境 ,List仅仅只会被单个线程操作",此时应该使用非同步的类(如ArrayList、LinkedList)。
4.对于“多线程环境,且List可能同时被多个线程操作”,此时,应该使用同步的类(如Vector、
CopyOnWriteArrayList)。现在来说使用**CopyOnWriteArrayList**比较多
b.问答
1.为什么LinkedList中插入元素很快,而ArrayList中插入元素很慢!(“删除元素”与“插入元素”的原理类似)
LinkedList: 通过add(int index, E element)向LinkedList插入元素时。先是在双向链表中找到要插入节点的位置
index;找到之后,再插入一个新节点。双向链表查找index位置的节点时,有一个加速动作:若index < 双向链表长度 的1/2,则从前向后查找; 否则,从后向前查找。
(每次指定位置添加元素,都会判断位置的前后,从而减少查询的时间)
ArrayList: System.arraycopy(elementData, index, elementData, index + 1, size - index); 会移动index之
后所有元素即可。这就意味着,ArrayList的add(int index, E element)函数,会引起index之后所有元素的改变!
(每次指定位置添加元素,指定位置后的数据都要被copy一下)
2. 为什么LinkedList中随机访问很慢,而ArrayList中随机访问很快
LinkedList:通过get(int index)获取LinkedList第index个元素时。先是在双向链表中找到要index位置的元素;找到之后
再返回。双向链表查找index位置的节点时,有一个加速动作:若index < 双向链表长度的1/2,则从前向后查找; 否
则,从后向前查找。
ArrayList:通过get(int index)获取ArrayList第index个元素时。直接返回数组中index位置的元素,而不需要像 LinkedList一样进行查找。
主要原因:ArrayList实现了RandmoAccess接口,提供了随机访问功能,而LinkedList不能随机访问,只能一个个遍历过去
三 、map集合源码解析
1. HashMap
(推荐看)
(详细介绍)
https://www.hollischuang.com/archives/2431 (关于初始值问题,默认为16)
(介绍 “(n - 1) & hash”,就是通过它来获取指定key在数组中的位置)
数据结构:(数组加链表加红黑树)
hashMap中的常量:
threshold = capacity * loadFactor --阈值当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。
loadFactor -- 填充因子,一般我们设置默认值为0.75
size/capacity -- 计算HashMap的实时装载因子的方法为,而不是占用桶的数量去除以capacity
capacity -- 数组长度
size -- hashmap中数据的个数(链表中的也算)
特点:
1. 数据结构:在JDK1.8以前是一个链表散列这样一个数据结构,而在JDK1.8以后是一个数组加链表加红黑树的数据结构。
2. hashMap是一个能快速通过key获取到value值的一个集合
问答:
- hashMap它是何时、如何扩容的?
当插入一个元素(put())的时候,size加1,若size大于threshold的时候,就会进行扩容(resize():每次扩容2的次幂)。扩容会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免它调用resize()方法。
注意:当所有元素都在同一个桶里面,并且这个桶的长度超过了threshold,它也会进行扩容
- hashMap初始值应该怎么设置?
***注意:底层数组长度会一直保持2的次幂,例如初始是16,之后自动扩容为32、64...,
并且,初始值设为3,但是hashMao它会自动帮我们扩展为4,反正无论如何,都是2的次幂***
为了最大程度的避免扩容带来的性能消耗,我们建议可以把默认容量的数字设置成expectedSize / 0.75F + 1.0F 。在日常开发中,可以使用
- hashMap是如何储存数据的?
数组加链表加红黑树,数组里面存链表,当链表数》=8时,他就会自动变成红黑树。在这里,我们把数组的一个位置,称为一个桶,桶里面装的就是链表或红黑树。
- hashMap它查找数据为什么那么快?
原因是内部使用hash查找值的方法,能很快找到了我们指定Key存放的桶。之后我们再遍历桶找到我们需要的key-value。(例如100个元素平均装在10个桶里面,所以我们通过hash值找到了指定的桶之后,只要最大遍历10次就能找到需要的元素了。当然,根据 threshold = capacity * loadFactor = 10*0.75=75<100,明显超出了过了,早就得扩容了)
- 算出元素在数组中的位置索引: “(n - 1) & hash”,它是怎么计算的?
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);//通过与运算,将h的二进制,和length-1的二进制进行与运算得出的结果就是数组中的位置。
}
上面是源码,h是key的hash值,length是数组的长度。
当 array.ength长度是2的次幂时,key.hashcode % array.length == key.hashcode & (array.length - 1)
,相当于进行了取余操作!
为什么要这样做?
1. & 操作代替 % 操作,提升性能
2. 数组扩容时,仅仅关注 “特殊位” 就可以重新定位元素
3. 如何实现建议看上面的链接文章
2. LinkedHashMap
数据结构:(数组加双向链表加红黑树)
特点:
- 在实现上,LinkedHashMap 很多方法直接继承自 HashMap,仅为维护双向链表覆写了部分方法。
四、fail-fast原理
(主要看评论)
原因:每次我们队集合进行增删改操作的时候,都会先运行下面这个函数,若 “modCount 不等于 expectedModCount”,则抛出ConcurrentModificationException异常,产生fail-fast事件。
// 用来记录List修改的次数:每修改一次(添加/删除等操作),将modCount+1(它是在AbstractList中定义的)
protected transient int modCount = 0;
int expectedModCount = modCount; //这行代码在迭代器中定义的,所以我们使用迭代器的时候就已经获取了modCount的值,所以在迭代过程中,只要进行了增删改之类的操作,就会运行下面这个方法,进而报错
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
解决:使用 CopyOnWriteArrayList 就不会报错了
释: CopyOnWriteArrayList 直接继承了List接口,是自己实现Iterator。
原理:CopyOnWriteArrayList的add、set、remove等会改变原数组的方法中,都是先copy一份原来的array,再在copy数组上进行add、set、remove操作,这就才不影响COWIterator那份数组
问答:
1. 在CopyOnWriteArrayList的迭代器中没有比较两个modCount,如果在ArrayList中不比较两个modCount也不会爆那个异常? 所以报不报异常就看有没有比较modCount2. 因为这个异常是比较不一致以后人为主动扔出来的首先我们要知道ArrayList比较的目的是什么? 保证数据一致性啊3. 现在的问题是为什么CopyOnWriteArrayList可以不比较modCount也能保证数据一致性? 因为getArray()返回的array的类型是volatile的(强制内存一致性)
- 在CopyOnWriteArrayList的迭代器中没有比较两个modCount,如果在ArrayList中不比较两个modCount也不会爆那个异常。
所以报不报异常就看有没有比较modCount - 因为这个异常是比较不一致以后人为主动扔出来的首先我们要知道ArrayList比较的目的是什么?
保证数据一致性啊 - 现在的问题是为什么CopyOnWriteArrayList可以不比较modCount也能保证数据一致性?
因为getArray()返回的array的类型是volatile的(强制内存一致性)