前言
Java 容器分为 Collection 和 Map 两大类,其下又有很多子类,如下所示是Collection和Map梳理讲解
一、List
1. ArrayList
原理:数组,底层使用Object数组
扩容机制:Arraylist的默认初始值大小为10,默认扩容大小为1.5倍
优点:查询速度快,时间复杂度O(1)
缺点:增删速度慢,O(n)
线程不安全
2. LinkList
原理:底层是双向链表
优点:查询速度快,时间复杂度O(n)
缺点:增删速度慢,O(1)
线程不安全
3. Vector
底层使用sychronize关键字保证,扩容是原来的2倍
常见面试题:
1、是否保证线程安全?
2、插入和删除是否受元素位置的影响
3、是否支持快速随机访问?
4、内存空间占用?
5、ArrayList 和 Vector 的区别是什么?
二、Set
Set集合本身只定义有不允许重复的存储。Set接口完整的继承了Collection接口,也就是说Set集合几乎与Collection的操作是对等的。
1.HashSet
(1)打开源代码之后发现整个HashSet类里面存在有一个HashMap,但是很明显,没使用这个Map的Value,只使用了Key(HashMap的主要特点是key不能重复);
(2)在HashSet类执行add()方法的时候发现是利用了Map接口中的put()实现的;
2.LinkHashSet
如果这个时候需要进行连续的保存(FIFO),就可以使用另外一个子类。它之所以可以进行顺序保存,是因为在进行存储的时候采用的是链表形式完成的
3.TreeSet
(1) 里面存放的是TreeMap,TreeMap的特点在于所有的key都是可以排序的,排序的依据是Comparable(往往会忽略掉Comparator);
(2) 在TreeMap之中对于大小的关系判断强制使用了Comparable接口中的compareTo()完成。
三、Map
1.hashMap
1)数据结构
数组 + 链表/红黑树
2)扩容机制
加载因子*最大容量
JDK1.7扩容复制时候是头插法,1.8是尾插法
3)允许null值存在
4)put的过程
part1:特殊key值处理,key为null;
part2:计算table中目标bucket的下标【key的hash值,取余(length-1)】;
part3:指定目标bucket,遍历Entry结点链表,若找到key相同的Entry结点,则做替换;
part4:若未找到目标Entry结点,则新增一个Entry结点。
参考:
https://www.cnblogs.com/tianzhihensu/p/11972780.html#11-%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86
2、ConcurrentHashmap详解
1.8为例子
1、PUT过程
1、判断Node[]数组是否初始化,没有则进行初始化操作
2、通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环。
3、检查到内部正在扩容,就帮助它一块扩容。【增删改才会扩容,读的会去查ForwardingNode】
4、如果f!=null,则使用synchronized锁住f元素(链表/红黑树的头元素)。如果是Node(链表结构)则执行链表的添加操作;如果是TreeNode(树型结构)则执行树添加操作。
5、判断链表长度已经达到临界值8(默认值),当节点超过这个值就需要把链表转换为树结构。
6、如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
2、get过程
1.计算hash值,定位到该table索引位置,如果是首节点符合就返回。
2.如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回。
3.以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回nul
3、ConcurrentHashMap的扩容
触发条件:
1)达到扩容状态
2)链表大于8,数组小于64
过程
检查到内部正在扩容,就帮助它一块扩容。【增删改才会扩容,读的会去查ForwardingNode】
4、在JDK1.7和JDK1.8中的区别
在JDK1.8主要设计上的改进有以下几点:
1、不采用segment而采用node,锁住node来实现减小锁粒度。
2、设计了MOVED状态 当resize的中过程中 线程2还在put数据,线程2会帮助resize。
3、使用3个CAS操作来确保node的一些操作的原子性,这种方式代替了锁。
4、sizeCtl的不同值来代表不同含义,起到了控制的作用。
采用synchronized而不是ReentrantLock