List Set Map的区别? 有哪些实现类?底层实现是什么?
| 种类 | List | Set | Map |
|---|---|---|---|
| 元素是否有序 | 有序 | 无序 | 无序 |
| 元素是否重复 | 可重复 | 唯一 | key唯一 |
| 存储的元素类型 | key-value键值对 |
-
List:
- ArrayList: Object[] 数组
- LinkedList: 双向循环链表
- Vector: Object[] 数组
-
Set:
- HashSet: 基于HashMap实现的
- LinkedHashSet: 基于链表和哈希表, 元素的插入和取出的顺序满足FIFO
- TreeSet: 基于红黑树实现的
-
Map:
- HashMap: 数组+链表+红黑树
- Hashtable:
ArrayList和LinkedList的区别?
- 底层实现不同: ArrayList是基于动态数组的数据结构,而LinkedList是基于链表的数据结构
- 随机访问性能不同: ArrayList 优于 LinkedList, 因为 ArrayList 可以根据下标以 O(1) 时间复杂度对元素进程随机访问。 而LinkedList的访问时间复杂都为O(n), 因为它需要变量整个链表才能找到指定的元素。
- 插入和删除性能不同: LinkedList 优于 ArrayList ,因为LinkedList的底层物理结构是链表,因此根据索引曾访问效率低,但是插入和删除不需要移动元素,只需要修改前后元素的指向关系即可,而且链表的添加不会升级到扩容问题。 数据索引访问的效率高,但是非末尾位置的插入和删除效率不高,因为涉及到移动元素,另外添加操作时涉及到扩容问题,会增加时空消耗。
ArrayList和Vector的区别?
- 底层物理结构都是动态数组。
- ArrayList时新版的动态数组,线程不安全,效率高, Vector是旧版的动态数组,线程安全,效率低。
- 动态数组的扩容机制不同,ArrayList扩容为原来的1.5倍, Vector扩容增加为原来的2倍。
- 数组的初始化容量,如果再构建ArrayList和Vector的集合对象时,没有显示指定初始化容量,那么Vector的内部数组的初始容量默认为10,而ArrayList在JDK1.6之前也是10, JDK1.7之后ArrayList初始化长度为0的空数组,之后在添加第一个元素时,再创建长度为10的数组。(用的时候再创建数组,避免浪费。)
- Vector因为版本古老,支持Enumeration迭代器。但是该迭代器不支持快速失败。而Iterator和ListIterator迭代器支持快速失败。如果再迭代器创建后任意时间从结构上修改了向量(通过迭代器自身的remove和add之外的任何其他方式),则迭代器将抛出 ConcurrentModifiactionException. 因此,面对并发的修改,迭代器很快就完全失败,而不是冒着再将来不确定的时间任意发生不确定性的风险。
ArrayList的扩容机制
- 快速失败的思想:对可能发生的异常提前表示故障并停止运行,通过提前的发现和停止错误,可以降低故障的级联风险。
- 初始化ArrayList的长度为0的空数组,当插入第一个元素时,再创建长度为10的数组
- 插入数据时,判断插入后的最小需要容量是否大于当前数组长度,
- 如果大于,并且扩容后的新数组容量大于最小需要容量,就创建一个当前大小的1.5倍的空数组,将原来的数据复制过来,并进行插入,
- 如果大于,并且扩容后新数组的容量小于等于最小需要容量,就创建一个等于插入后的大小的数据,将原始数据复制过来,并进行插入。
Queue和Deque的区别
- Queue: 单端队列,只能从一端插入,一端删除,遵循FIFO原则。 扩展了Colletions,会因为容量问题导致操作失败而处理的方式不同:一种是操作失败后抛异常,一种是返回特殊值
| Queue接口 | 抛出异常 | 返回特殊值 |
|---|---|---|
| 插入末尾 | add(E e) | offer(E e) |
| 删除首位 | remove() | poll() |
| 查看首位 | element() | peek() |
- Deque: 双端队列,首尾两端都可以插入和删除,并且也因为容量问题导致操作失败而处理的方式不同
| Deque接口 | 抛出异常 | 返回特殊值 |
|---|---|---|
| 插入首位 | addFirst(E e) | offerFirst(E e) |
| 插入末尾 | addLast(E e) | offerLast(E e) |
| 删除首位 | removeFirst() | pollFirst() |
| 删除末尾 | removeLast() | pollLast() |
| 查看首位 | getFirst() | peekFirst() |
| 查看末尾 | getLast() | peekLast() |
ArrayDeque 和 LinkedList的区别:
- ArrayDeque 和 LinkedList都是实现了 Deque接口,都具有队列的功能。
- 从结构上:ArrayDeque通过可变长的数组+双指针实现; ListedList通过链表来实现
- 从扩容角度上说: ArrayDeque基于数组,可以根据插入的数据个数进行动态扩容,扩容之后,插入仍是O(1); LinkedList是每插入一个都会申请新的堆空间。
- 存储NUll值: ArrayDeque不支持, LinkedList支持
PriorityQueue 的特点
- 元素出队的顺序与优先级相关,优先级越高的越先出队
- 底层使用二叉堆结构实现的,并用可变长的数组来进行存储数据的。
- 非线程安全的。
- 默认是小顶堆,可以通过Comparator构建函数,来修改优先级的先后。
HashMap和Hashtable的区别?
- 都是实现了Map接口,用于存储键值对的数据结构,底层数据结构都是数组加链表形式。
- 线程安全:Hashtable是线程安全的,而HashMap是非线程安全的。
- 性能:因为Hashtable使用synchronized给整个方法加锁,所以相比HashMap来说,它的性能不如HashMap。
- 存储:HashMap允许key和value为null,而Hashtable不允许存储null键和null值。
- Hashtable不能存储的原因:key值进行哈希计算,如果为null的话,无法调用该方法,还会抛出空指针异常。而value为null也会主动抛出空指针异常。
- HashMap允许key和value为null的原因:hash()对null的值进行了特殊处理,如果为null,会把值赋值为0.
- Hashtable是线程安全,但性能差,不推荐使用。推荐使用ConcurrentHashMap在多线程场景下使用。
HashMap和HashSet的区别?
- HashSet实现了Set接口,只存储对象; HashMap实现了Map接口,用于存储键值对。
- HashSet底层用HashMap存储,HashSet封装了一系列HashMap的方法,HashSet将值保存到HashMap的key里。
- HashSet不允许有重复的值。 而HashMap的键不能重复,值可以重复。
HashMap
解决【index】冲突问题
- 虽然使用hashCode(),来尽量减少冲突,但仍然存在两个不同对象返回hashCode值相同的情况。
- JDK1.8之前使用:数组+链表
- JDK1.8之后使用:数组+链表/红黑树
- 即hash值相同的元素,存储在同一个桶table[index]中,使用链表或红黑树连接起来。
- 哈希冲突的键值对以链表的形式存储在同一索引位置,插入新节点时,采用尾插法(JDK8改进,JDK7是头插法(多线程并发扩容时,头头插法导致链表反转,产生循环引用))
为什么1.8会出现红黑树和链表共存
- 当hash冲突比较严重时,链表长度会很长,导致查询效率降低,而选择二叉树可以大大提高查询效率。但由于二叉树的结构太复杂,节点个数小时,选择链表反而更加简单。所以会出现红黑树与链表共存。
什么时候树化?什么时候反树化?
static final int TREEIFY_THRESHOLD = 8;//树化阈值
static final int UNTREEIFY_THRESHOLD = 6;//反树化阈值
static final int MIN_TREEIFY_CAPACITY = 64;//最小树化容量
- 当table[index]下的链表节点个数达到8,并且 table.length >= 64, 那么新Entry对象还添加到该table[index]中,那么就会将table[index]的链表进行树化。
- 当某table[index]下的红黑树节点个数少于6个,此时
- 当继续删除table[index下]的树结点,最后这个根节点的左右结点有null,或者根节点的左结点为null,会反树化。
- 当重新添加新的映射关系到map中,导致了map重新扩容了,这个时候如果table[index]下面还是小于等于6个,会反树化。
负载因子为什么是0.75
//初始化容量:
int DEFAULT_INITIAL_CAPACITY = 1 << 4;//16 目的是体现2的n次方
//①默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//②阈值:扩容的临界值
int threshold;
threshold = table.length * loadFactor;
//③负载因子
final float loadFactor;
- 如果太大,threshold就会很大,那么如果冲突比较严重的话,就会导致table[index]下面的结点个数很多,影响效率。
- 如果太小,threshold就会很小,那么数据扩容的频率就会提高,数组的使用率也会降低,那么会造成空间浪费。
1.7 put 源码分析
- 当第一次添加映射关系时,数组初始化为一个长度为16的HashMapEntry类型是实现了java.util.Map.Entry接口。
- 特殊考虑:如果key为null,index直接是[0],hash也是0.
- 如果key不为null,在计算index之前,会对key的hashCode()值,做一个hash(key)再次哈希的运算,这样可以使得Entry对象更加散列的存储到table中
- 计算index=table.length-1 & hash
- 如果table[index]下面,已经有映射关系的key与要添加的映射关系的key相同了,会用新的value替换旧的value
- 如果没有相同的,会把新映射关系添加到链表的头,原来table[index]下面的Entry对象连接到新的映射关系的next中。
- 添加之前先判断if(size >= thresold && table[index] != null) 如果该条件为true,①会扩容 ②会重新计算key的hash ③会重新计算index。
- size++
1.8put源码分析
(1)DEFAULT_INITIAL_CAPACITY:默认的初始容量 16
(2)MAXIMUM_CAPACITY:最大容量 1 << 30
(3)DEFAULT_LOAD_FACTOR:默认加载因子 0.75
(4)TREEIFY_THRESHOLD:默认树化阈值8,当链表的长度达到这个值后,要考虑树化
(5)UNTREEIFY_THRESHOLD:默认反树化阈值6,当树中的结点的个数达到这个阈值后,要考虑变为链表
(6)MIN_TREEIFY_CAPACITY:最小树化容量64
当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
(7)Node<K,V>[] table:数组
(8)size:记录有效映射关系的对数,也是Entry对象的个数
(9)int threshold:阈值,当size达到阈值时,考虑扩容
(10)double loadFactor:加载因子,影响扩容的频率
- 先计算key的hash值,如果key是null,hash值就是0,如果不为null,使用((h=key.hashCode)^(h >> 16)) 得到hash值。
- 若table是空的,先初始化table数组。
- 通过hash值计算存储的索引位置index = hash & (table.length - 1)
- 如果table[index]==null, 那么直接创建一个Node结点存储到table[index]中即可
- 如果table[index] != null
- 判断table[index]的根节点的key是否与新的key
相同(hash值相同并且满足key地址相同或key的equals返回true),如果是那么用一个Node结点变量e记录这个根结点 - 如果table[index]的根结点的key与新key
不相同,而且table[index]是一个TreeNode结点,说明table[index]下是一棵红黑树,如果该树的某个结点的key与新的key相同(hash值相同并且满足key的地址相同或者key的equals返回true),那么用一个Node结点变量e记录这个相同的结点,否则将(key,value)封装为一个TreeNode结点,连接到红黑树中 - 如果table[index]的根结点的key与新key
不相同,并且table[index]不是一个TreeNode结点,说明table[index]下是一个链表,如果该链表中的某个结点的key与新的key相同,那么用一个Node结点变量e来记录这个相同的结点,否则将新的映射关系封装为一个Node的结点直接链接到链表尾部,并且判断table[index]下结点个数达到 TREEIFY_THRESHOLD(8) 个,如果table[index]下的结点个数达到了8个, 那么判断talbe.length 是否达到 MIN_TREEIFY_CAPACITY(64) 个,如果没有达到,那么先扩容,扩容会导致所有元素重新计算index,并且调整位置,如果table[index]下结点个达到8个,并且table.length的个数达到64,那么会将该链表转为一颗自平衡的红黑树。
- 判断table[index]的根节点的key是否与新的key
- 如果table[index]下找到了新key
相同的结点,即e不为空,那么用新value替换原来的value,并返回旧value,结束put的方法。 - 如果新增结点而不是替换,那么size++,并且还要重新判断size是否达到threshold阈值,如果达到,还要扩容。
ConcurrentHashMap和Hashtable的区别?
- 底层结构:
- ConcurrentHashMap的1.7版本采用 分段的数组+链表; 1.8后采用数组+链表/红黑树的结构
- Hashtable 采用数组+链表的结构
- 实现线程安全的方式:
-
ConcurrentHashMap:
- 1.7 采用segment分段锁,将整个桶数组分割分段(分段锁),每把锁只锁定一部分数据,不同线程访问不同段的数据,就不会存在锁竞争,提高并发访问效率
- 1.8 摒弃分段锁,采用Node数组+链表+红黑树,采用synchronized 和 CAS 来实现线程安全的。
-
Hashtable(同一把锁): 使用synchronized进行修饰, 当一个线程进行访问同步方法,其他线程也进行访问,就会导致进入阻塞或轮询状态。效率低下,一个线程使用put,其他线程就不能访问put或get.
-
ConcurrentHashMap的线程安全实现方案和具体底层实现方案?
- 1.8底层采用Node数组+链表/红黑树,当Hash冲突到一定地步时,就会将链表转换为红黑树。
- put的流程:
- 1.通过key计算出哈希值
- 2.判断是否需要初始化
- 3.通过key的哈希值,定位Node数组的位置,如果为空,通过CAS写入,失败则自旋保证成功。
- 4.如果 hashCode == MOVED == -1 的值,则需要扩容。
- 5.如果不满足,通过synchronized锁定写入数据
-
- 如果数量大于 树化临界值就会执行树化方法 ,treeifyBin会判断数组长度>=64,会将链表转化为红黑树。
为什么ConcurrentHashMap不能添加null?
- 在HashMap中,Key和value值都可以为null
- 在ConcurrentHashMap中,key或者value值都不能为null。由于底层实现的时候,如果key和value若为null,会抛出NullPointerException的异常。更深层次的原因:如果允许ConcurrentHashMap的key或者value为null的情况下,就会存在经典的“二义性问题”
- 对应以上null的二义性:①这个值null表示一种具体的“null”值状态 ② null还表示“没有”的意思,因为没有设置。hashMap没有这个问题,因为单线程。 而ConcurrentHashMap是在多线程下,多个进程会改动数据,有这样的问题。