ArrayList
ArrayList 实现于 List、RandomAccess 接口。可以插入空数据,也支持随机访问。其中最重要的两个属性分别是: elementData 数组,以及 size 大小。 默认初始化容量为10,每次扩容会扩容1.5倍(新容量=旧容量+旧容量>>1)。有序、非线程安全的。
执行add(E)方法:
- 首先记录对该列表进行结构修改的次数
- 然后执行添加元素,默认添加到末尾
- 判断数组的容量是否满了,如果是就先进行扩容
- 将元素添加到指定位置,修改size大小
执行add(index,e)方法,添加元素到指定位置:
- check下标是否越界,并记录列表结构修改次数
- 判断数组是否需要扩容
- 通过System.arraycopy方法复制指定的元素向后移动
- 将添加的元素赋值给指定的下标 ,修改size大小
扩容方法grow():
- 主要是通过旧的容量+旧的容量,然后右位移1位来计算新的容量
- 通过容量创建一个新的数组,进行数组复制。
Vector
底层使用数组实现,扩容方式与List不同,如果初始化Vector时没有指定容量增量,那么会默认扩容2倍(新容量=旧容量+旧容量),如果指定了容量增量,那么扩容的容量就是(新容量=旧容量+容量增量),使用synchronized包装了类的方法,所以是线程安全的。并且有序。
执行add(E e)方法:
- 判断是否需要扩容
- 赋值元素到指定的下标
- 修改容器大小
LinkedList
采用双向链表实现,get指定索引的值会先对链表的大小进行右位移1,来判断获取的索引值在链表的上半部分还是下半部分,如果是上半部分会从头部节点开始遍历查找,如果是下半部分会从尾部节点开始遍历查找,有序、非线程安全。
执行add(E e)方法:
- 默认添加到链表的最后面,先取出lastNode的一个临时变量。
- 将要添加的元素包装成一个newNode节点,将newNode节点的前置节点设置为当前链表的最后一个节点
- 将newNode设置为新的last节点
- 判断lastNode是否为空,如果为空,那么此时添加的是链表的第一个节点,那么直接设置firstNode等于newNode。如果不是就将newNode链接到lastNode后边
- 修改改链表大小及结构修改次数
执行get(int index)方法:
- 先判断要获取的索引是否超出链表大小
- 通过将size左位移一位来判断index是在链表的上半部分还是下半部分
- 如果在上半部分则通过头节点开始遍历查找
- 如果在下半部分则通过尾节点开始遍历查找
HashSet如何保证数据不重复?
HashSet底层结构是一个HashMap,HashSet将值放在HashMap的键中,如果HashMap的键相同时会发生覆盖,因此HashSet的值不会重复。HashMap检查Key是否相同会通过equles方法,并通过比较hash值判断是否重复。
TreeSet
底层使用NavigableMap实现,所有添加到set中的元素最终都会添加到map的key中,value用一个final的object对象填充。有序不重复,非线程安全
HashMap
HashMap初始容量为16的Note数组,数组内的链表容量大于8时会自动转换为红黑树,只有当这个数大于2并值至少有8个才能满足树的假设,当这个值缩小到6的时候就会转换为链表。
为什么HashMap中链表长度超过8会转换成红黑树?
原因:链表的时间复杂度是O(n),红黑树的时间复杂度O(logn),很显然,红黑树的复杂度是优于链表的,因为树节点所占空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。也就是说,节点少的时候,尽管时间复杂度上,红黑树比链表好一点,但是红黑树所占空间比较大,综合考虑,认为只能在节点太多的时候,红黑树占空间大这一劣势不太明显的时候,才会舍弃链表,使用红黑树。
还有选择6和8的原因是:
中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
时间复杂度的排序是O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(2^n) < O(n!) < O(n^n)。
在hashMap中get操作:
- 计算key的hash值,判断get的元素是不是firstNoe,如果直接返回
- 如果不是firstNode,那么判断是否是树,如果是的话通过遍历树查找。
- 否则遍历链表找到key相等的值。
在hashMap中put操作:
- 计算key的hash值,算出元素在底层数组中的下标位置。如果下标位置为空,直接插入。
- 通过下标位置定位到底层数组里的元素(也有可能是链表也有可能是树)。
- 取到元素,判断放入元素的key是否==或equals当前位置的key,成立则替换value值,返回旧值。
- 如果是树,循环树中的节点,判断放入元素的key是否==或equals节点的key,成立则替换树里的value,并返回旧值,不成立就添加到树里。
- 否则就顺着元素的链表结构循环节点,判断放入元素的key是否==或equals节点的key,成立则替换链表里value,并返回旧值,找不到就添加到链表的最后。
- 精简一下,判断放入HashMap中的元素要不要替换当前节点的元素,key满足以下两个条件即可替换:
HashMap原理及冲突解决办法
当拿到一个hash值,通过indexFor(hash, table.length)获取数组下标,先查询是否存在该hash值,若不存在,则直接以Entry<V,V>的方式存放在数组中,若存在,则再对比key是否相同,若hash值和key都相同,则替换value,若hash值相同,key不相同,则形成一个单链表,将hash值相同,key不同的元素以Entry<V,V>的方式存放在链表中,这样就解决了hash冲突,这种方法叫做分离链表法
LinkedHashMap
主体的实现都是借助于 HashMap 来完成的,只是对其中的 recordAccess(), addEntry(), createEntry() 进行了重写。
总的来说 LinkedHashMap
其实就是对 HashMap
进行了拓展,使用了双向链表来保证了顺序性。因为是继承与 HashMap
的,所以一些 HashMap
存在的问题 LinkedHashMap
也会存在,比如不支持并发等。
LinkedHashMap
的排序方式有两种:
- 根据写入顺序排序。
- 根据访问顺序排序。
ConcurrentHashMap
1.8 中的 ConcurrentHashMap 数据结构和实现与 1.7 还是有着明显的差异。其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。其中的 val和next 字段都用了 volatile 修饰,保证了可见性。
- 根据 key 计算出 hashcode 。
f
即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。- 如果当前位置的
hashcode == MOVED == -1
,则需要进行扩容。 - 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于
TREEIFY_THRESHOLD
则要转换为红黑树
数组和链表的底层区别?
数组插入数据因为需要连在一起,如果内存空间不连续的话就得全体迁移,甚至出现内存空间足够但是由于不在一起而导致无法为数组分配空间。
链表插入数据根本不需要迁移数据,所以速度快,而且避免了内存空间足够但是连续空间不够导致无法分配内存的情况。
数组的优点:由于数组在内存中连续,我们可以轻松的知道每一个元素的内存地址,用数组的起始位置+数据的大小*元素编号,那随机访问的速度就会很快了,而链表因为不连续无法计算出每个元素的内存地址就需要一个个去往后找了,因此访问的很慢