复习准备秋招,内容有错的地方请各位看官评论区留言!不胜感激!
Java集合
List、Map、Set三者的区别
- List是一个有序的容器,元素存入List的顺序和取出的顺序一样。List中的元素可以重复,可以插入多个null,每个元素都有自己的索引。
- Map中使用键值对来存储。每个元素由键Key和值Value构成。Key不能重复,每个键只能对应一个值。。
- Set中元素不允许重复。不会有多个元素引用相同的对象。
List
ArrayList
-
底层结构
底层采用数组 Object[ ] elementData 。
查询效率高,增删效率低,线程不安全。使用频率很高。 -
线程不安全
怎么实现线程安全的ArrayList?
用Collections.synchronizedList把一个普通ArrayList包装成一个线程安全版本的数组容器也可以,原理同Vector是一样的,就是给所有的方法套上一层synchronized。 -
扩容策略
初始默认容量为0,添加第一个元素时初始化一个大小为10的数组。
扩容时采用位运算,每次扩容1.5倍。
LinkedList
实现了List 和 Deque 接口
-
底层结构
底层是基于双向链表实现的,支持高效的插入和删除操作。 -
线程不安全
Vector
-
底层结构
Object[] elementData
-
线程安全
方法都加了 synchronized
Map
HashMap
-
数据结构
JDK1.7:
数组+链表JDK1.8:
数组+链表+红黑树 -
初始化
JDK1.7:
数组初始化大小为16。JDK1.8:
数组初始化大小为16。 -
扩容机制
capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
JDK1.7:
数组+链表,因此扩容考虑数组扩容。JDK1.8:
单个链表长度大于等于8时,该链表会转为红黑树。当链表长度小于等于6时,会退化成链表。 -
put操作
JDK1.7:
先判断是否需要扩容,需要的话先扩容(如果当前的 size 已经达到了阈值,并且要插入的数组位置上已经有元素,那么就会触发扩容,扩容后,数组大小为原来的 2 倍),然后再将这个新的数据插入到扩容后的数组的相应位置处的链表的表头。JDK1.8:
第一次put值的时候,触发 resize( ) ,类似JDK1.7第一次put也要初始化数组长度;
第一次resize和后续的扩容不一样,第一次resize是数组从 null 初始化到默认的16或自定义的初始容量。
找到具体的数组下标,分情况:1.该位置有数据
2.该位置节点是红黑树节点
3.该位置上是一个链表(尾插法):
· 链表数据超过8个时,触发 treeifyBin 将链表转化成红黑树。
·如果在链表中找到了相等的“key”,则旧值覆盖
·HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
·如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容 -
存在问题
多线程下存在线程安全问题
-
解决Hash冲突
1.开放定址法(线性探测、二次探测、伪随机探测)
2.再哈希法
3.拉链法
4.建立一个公共溢出区:把冲突的都放在一边
Hashtable
-
扩容机制
初始大小为11,每次扩容后大小为:2倍+1
-
线程安全:会引发线程安全的方法前加了 synchronized
TreeMap
-
底层结构
红黑树
红黑树五个特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
LinkedListHashMap
在HashMap的基础上加了一个双向链表,维护了插入的顺序,常用来做LRU缓存
ConcurrentHashMap
-
概述
JDK1.7中:
HashEntry与HashMap中一样。但使用了 volatile 修饰。 分段锁思想JDK1.8:
Node代替Entry,使用volatile修饰volatile:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
- 禁止进行指令重排序。
- volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
-
特点
多线程环境下线程安全。
JDK1.7:
segment分段锁JDK1.8:
CAS+synchronized 保证线程安全。 -
初始化
JDK1.7:
- segment大小为16。
- 初始化槽(segment)时,需要考虑并发,因为很可能会有多个线程同时进来初始化同一个槽 segment[k],不过只要有一个成功了就可以。
- ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其他槽来说,在插入第一个值的时候进行初始化。
JDK1.8:
- 采用数组+链表+红黑树,数组默认初始大小为16,可以扩容(每次2倍)
- initTable。初始化一个合适大小的数组,然后设置 sizeCtl。初始化方法中的并发问题是通过对 sizeCtl 进行一个 CAS 操作来控制的。
-
扩容
-
put操作
JDK1.7:
- 1.根据hash值找到对应的segment,之后执行segment的put操作
- 2.往segment写入前,先获取该 segment 的独占锁
- 3.在该 segment内部数组中进行插入
- 4.利用hash值求得应该放置的数组下标
- 5.当该位置已经有一个链表存在时,直接覆盖旧值
- 6.当该位置没有任何元素时,(初始化)把当前节点设置成表头
- 7.添加元素后若超过了该segment的容量阈值,则扩容
JDK1.8:
- 1.得到hash值
- 2.如果数组为空,则初始化数组
- 3.数组不为空,找到hash值对应的数组下标,得到第一个节点
○ 如果该位置为空,则用一次CAS 操作将新值放入其中;若CAS失败,即有并发操作,进到下一个循环 - 4.hash值等于 MOVED,此时在扩容使用 helpTranser 方法帮助数据迁移
- 5.数组不为空,hash值对应的节点是该位置的头节点并且不为空,
○ 头节点的hash值大于等于0,说明目前是链表,则遍历链表——synchronized代码块
— 如果发现了相同的“key”,判断是否要进行值覆盖,然后break
— 如果到了链表最末端,将这个新值放到链表的最后
○ 头节点为红黑树节点,则调用红黑树的插值方法插入新节点 - 6.判断链表是否要转成红黑树,临界值为8,并且数组长度大于64
-
get操作
由于加了 volatile 关键字,因此可以根据hash值去获取元素
Set
HashSet
-
概述
- 1.HashSet继承AbstractSet类,实现Set、Cloneable、Serializable接口。
- 2.基于HashMap实现,底层使用HashMap保存所有元素。
- 3.当两个hashcode相同但key不相等的entry插入时,仍然会连成一个链表,长度超过8时依然会和hashmap一样扩展成红黑树。
- 4.当add方法发生冲突时,如果key相同,则替换value,如果key不同,则连成链表。
LinkedListHashSet
- LinkedHashSet内部使用LinkedHashMap对象来存储和处理它的元素。
- LinkedHashSet是HashSet的一个“扩展版本”,HashSet并不管什么顺序,不同的是LinkedHashSet会维护“插入顺序”。
- LinkedHashSet是如何维护插入顺序的?
1).LinkedHashSet使用LinkedHashMap对象来存储它的元素,插入到LinkedHashSet中的元素实际上是被当作LinkedHashMap的键保存起来的。
2).LinkedHashMap的每一个键值对都是通过内部的静态类Entry<K, V>实例化的。这个 Entry<K, V>类继承了HashMap.Entry类。这个静态类增加了两个成员变量,before和after来维护LinkedHasMap元素的插入顺序。这两个成员变量分别指向前一个和后一个元素,这让LinkedHashMap也有类似双向链表的表现。
TreeSet
- 1.继承AbstractSet,实现NavigableSet、Cloneable、Serializable接口。
- 2.TreeSet同样是基于TreeMap实现的。
- 3.TreeSet同样也是一个有序的,它的作用是提供有序的Set集合。