1.ArrayList
扩容规则
-
ArrayList调用无参构造函数时,数组大小为0。
-
当传入参数时,则创建指定大小空间的数组
-
当传入集合时,则会根据集合大小创建初始容量
-
当容量不足时,数组会进行扩容,长度为之前的1.5倍。
例如:[0,10,15,22,33,49...]
-
当传入集合个数大于扩容规则时,会取较大值
例如:当我们调用无参构造函数时,初始大小为0,但我们传入一个14个元素的数组时,容量并不会先扩大到10再扩大到15,而是会直接扩大到14,取较大值。
fail-fast、fail-safe机制
-
fail-fast指的是当数组遍历的时候,其他人对数组进行修改,则会抛出异常
-
fail-safe指的是当数组遍历的时候,其他人来修改数组的时候,应该会有应对的策略,例如牺牲一致性继续遍历
Iterator_fail-fast、fail-safe原理分析
-
当我们使用foreach首次输出元素时会new一个迭代器的对象,在list中存在一个modCount元素,会记录集合修改的次数。
-
遍历数组时,会将集合的修改次数传给迭代器expectedModCount,迭代器遍历的时候会进行判断,集合修改次数与初始化时传的值是否相同,以次来判断遍历时有无修改了集合。
CopyOnWriteArrayList是fail-safe的典型代表,遍历的同时可以修改,原理是读写分离。
2.LinkedList
ArrayList与LinkedList
ArrayList
-
基于数组,需要连续存储
-
随机访问快(实现了RandomAccess接口)
-
尾部插入删除性能可以,其他部分插入删除需要移动数据性能低
-
可以利用cpu缓存,局部性原理
LinkedList
-
基于双向链表
-
随机访问慢
-
头部插入删除性能高
-
占用内存多
注:
RandomAccess接口为空接口,若实现该接口,则在遍历时会通过找下标的方式去遍历。若没实现只能通过迭代器去访问。
3.HashMap(重点)
-
底层数据结构,1.7与1.8的区别
-
1.7数组+链表,1.8数组+链表(元素少于等于八个时)、红黑树
-
HashMap存储原理
-
计算元素的哈希码,经过二次hash对map长度进行求余,得到桶下标
-
桶下标即为数组的下标,若存在多个元素通下表相同,则用链表链接
-
当元素达到hashmap长度的3/4时会进行扩容
-
当容量达到64且链表长度达到8个以上时会进行链表转换红黑树
为什么不一上来便树化?
-
红黑树用来避免Dos攻击,防止链表长度过长降低性能
-
hash表的查找更新时间复杂度为O(1),红黑树的查找效率为O(log2n),而且TreeNode占用空间也比Node大
-
hash值足够随机时候,符合泊松分布,负载因子为0.75时,长度超过八的概率是0.00000006。
红黑树退化链表的两种情况
-
扩容时进行差分树,链表的长度小于等于6
-
remove结点之前若root、root.left、root.left.left、root.right为null,则会退化成链表
索引的计算方法
-
计算出对象的hashCode,在调用HashMap的hash()方法进行二次哈希,使用&(capacity-1)得到索引
-
二次哈希的目的是为了让哈希分布更均匀
HashMap的容量为啥是2的n次幂
-
计算索引时,如果是2的n次幂,可以使用位运算来代替模运算,效率更高。扩容时hash&odlCap==0的元素留在原来位置,否则新的位置=旧位置+oldCap
HashMap的put方法流程
-
HashMap是惰性创建数组,首次使用put才会创建数组
-
计算索引
-
如果桶下标还未占用,则先创建Node占位,返回k-v
-
如果桶下标被占用了
-
如果是普通的链表,则走链表的添加逻辑,如果查过树化阈值则走树化逻辑
-
已经是TreeNode走红黑树的更新逻辑
-
-
返回前检查容量是否超过阈值,一旦超过则进行扩容
1.7和1.8put流程有何不同
-
链表插入节点时,1.7是头插法,1.8是尾插法
-
1.7是大于等于阈值且 没有空位时才扩容,1.8是大于阈值就扩容
-
1.8计算Node索引时,会优化
负载因子为何默认是0.75
-
在空间占用与查询时间之间取得较好的权衡
-
大于这个值,节省空间,但链表就会比较长影响性能
-
小于这个值,冲突减少,扩容更频繁,空间占用多
注:多线程下
-
1.7会发生扩容死链问题
-
1.7、1.8会发生数据错乱问题
key值要求
-
HashMap的key可以为null
-
作为key对象,必须实现hashCode和equals,并且key的内容不能修改(hashCode相同的两个对象不一定相同,但是equals相同的两个对象,hashCode一定相同)