一、简要关系结构图
二、ArrayList
1、jdk 1.7 原理
调用空构造器时初始化 Object[] elementData 数组,长度为10;
初始化 size 属性,值为0;
当elementData 数组中的10个位置都有元素后,添加第11个元素时数组扩容;
扩容长度为原数组的1.5倍;
扩容逻辑是创建一个长度为 15 的数组,将旧数组的数据拷贝到新数组中。
2、jdk 1.8 原理
调用空构造器时初始化 Object[] elementData 数组,长度为 0;
初始化 size 属性,值为0;
添加第一个元素时,进行数组扩容;创建一个长度为 10 的数组,将该元素添加到新数组中;
剩余扩容逻辑与 jdk 1.7 一致;
3、特点
查询快,增删慢;
三、LinkedList
1、原理
是一个双向链表;维护了头节点和尾节点。
维护了一个size,用于计数。
每一个节点对应的类是 linkedList的内部类Node<E>
每一个节点包含三个主要参数:上一个节点地址、下一个节点地址、当前节点存储的数据;
调用 get方法传入索引值,底层会判断当前索引是否大于size的一半,用来判断从头遍历还是从尾部遍历(for循环遍历);
使用for循环和for-each循环实际上是使用 iterator 迭代器进行遍历;
2、特点
查询慢,增删快。
四、Vector
一个被淘汰的集合
底层数组扩容为原数组的 2 倍;
add方法被 synchronized 修饰,表示线程安全;
五、迭代器
1、面试题:iterator()、Iterator、Iterable的关系?
-- Iterable<E>是一个接口,Collection 接口继承 Iterable<E>接口;
-- iterator() 是 Iterable<E>接口 中的一个抽象方法,在具体实现类中被实现;
-- iterator() 方法的返回值类型 是 Iterator<E> 接口,这个接口有两个经典抽象方法:
1、hasNext();
2、next();
-- hasNext()和next()这两个方法在 ArrayList 的内部类 Itr中被实现;
-- hasNext() 方法具体实现是 判断计数器是否与当前size相同;
-- next() 方法具体实现是 根据下标返回数组中的元素;
2、ListIterator
是一个增强型的 iterator ,提供了更多对 List的操作;
TreeMap实现类
唯一,有序(按升序或者降序)
key对应的类型一定要实现比较器(内部比较器、外部比较器)
底层:二叉树,每个节点都是一个 Entry<K,V>对象
每个节点对应的类型:Entry<K,V>,这个类包含6个属性,key、value,parent等信息
六、HashMap
1、原理
分类 | 解释 |
构造器 | -- key唯一,且可以为 null;value没特别要求; -- 底层数据结构是 数组 + 链表; -- 提供空参构造器和带参构造器,如果使用带参构造器指定数组长度,会默认将这个长度改成 2 ^ n; -- 底层数据结构有个 Entry<K,V>类型的主数组,默认长度为16; -- 且这个数组的长度只能是 2 的 n 次方; -- 目的是使用"与运算"代替"取余运算"提高效率; -- 同时减少"哈希冲突" ; -- 在 put 方法中 ,对key的hashCode 和 数组长度做取余运算,来确定存放位置下标; -- 有个加载因子的属性,默认是0.75; -- 有个size属性,每put一个元素,size+1; -- 有个threshold属性,用来表示当前map的扩容边界值; |
put方法 | -- 在put方法中,先对key求hashCode,再用这个hashCode 和 主数组 取余,来确定主数组的下标; -- 如果在主数组中当前下标处没东西,就直接把元素放在这个位置; -- JDK 1.7 头插法; |
扩容 | -- size >= 扩容临界值,且当前节点的数组下标位置已有数据,会触发扩容; -- 扩容为原数组的2倍,将旧数组的数据存到新数组中; -- 重新计算当前节点的下标; |
面试题 | 1、为什么加载因子默认为0.75? -- 如果太大,会增加hash碰撞概率,导致链表太长,查询效率低; -- 如果太小,会导致主数组容易扩容,链表太短,浪费空间; 2、为什么主数组长度一定是 2 ^ n? -- 在put方法中通过key的hashCode计算数组下标时,使用与运算代替取余运算提高效率; -- 降低下标位置冲突概率; |
2、JDK 1.8 HashMap 原理
分类 | 解释 |
底层数据结构 | -- 数组 + 链表(单向链表) + 红黑树。 -- 数组的类型是 Node<K,V> 是HashMap的一个内部类。 |
树化 | -- 链表的个数达到8个且主数组长度达到64时才会树化,否则只进行扩容操作。 |
初始化 | -- 调用构造器创建HashMap对象只会声明一个主数组的类型。 -- 在put第一个元素的时候才会对这个主数组做初始化操作,长度为16。 |
七、TreeMap
1、原理
-- 底层是二叉树结构。
-- 如果没指定比较器就使用key的 hashCode 做排序,来确定新的节点放左边还是右边。
-- 初始化:声明一个comparator(比较器)。
-- 底层维护了一个root节点,第一次put键值对就保存进root节点内。
-- 之后put键值对就使用比较器确定新节点放到左边还是右边。
八、HashTable
1、原理
-- 初始化:默认容量 = 11,加载因子 = 0.75f,创建一个 Entry<?,?>[] table;
-- 底层维护了一个table(数组),数组中每个元素都有next属性,结构和HashMap一样,保存键值对。
-- put方法:
有返回值,如果key重复,返回上一次保存的value,新value覆盖旧value。
如果key不重复,计算hashCode且取余后确定key的位置。
被 synchronized 修饰,表示线程安全。
九、LinkedHashMap
1、原理
-- 是HashMap的子类。
-- 初始化:创建一个HashMap,声明输出顺序以插入顺序为准。
-- put方法:就是HashMap的put方法。
-- get方法:是自己的,取到节点后做后置处理。
2、比较
HashMap | HashTable | LinkedHashMap |
JDK 1.2 诞生。 线程不安全,效率高。 允许key = null。 | JDK 1.0 诞生。 线程安全,效率低。 不允许key = null。 | -- 可以实现按输入顺序来输出; |
十、TreeSet实现类
唯一,有序(按升序或者降序);
-- 借助 TreeMap 实现,每个节点的value都是一个Object对象 --> 只在key的位置保存数据;
-- 所以 TreeSer存储数据的特点就是 TreeMap的key的特点;
十一、HashSet实现类
-- 借助 HashMap实现,每个节点的value都是一个Object对象 --> 只在key的位置保存数据;
-- 所以 HashSet存储数据的特点就是 HashMap的key的特点;
十二、ConcurrentHashMap
1、原理
-- 初始化:只声明一个对象,不申请存储空间。
-- put方法:初次put,创建存储空间,长度 = 16,这个过程是线程安全的。
-- 在写数据过程中都是数据安全的,只锁住当前占用数组位置的数据,提高了执行效率。
2、比较
ConcurrentHashMap | HashTable | HashMap |
--线程安全 --细粒度锁,高效 --锁在片区上,16个片区 | --线程安全 --粗粒度锁,低效 --锁在主数组上 | --线程不安全 --无锁,高效 |
十三、同步集合
1、线程不安全示例
2、线程安全示例
十四、COW并发容器
1、Copy On Write 写时复制容器
写数据时先复制一个容器,新容器容量 +1,在加出来的位置写入新数据,修改老容器的指向。
执行过程:
1、此时有一个数组 N
2、需要向数组 N 添加数据时,先复制数组 N,得到 M
3、给 M 数组的长度 + 1
4、将新元素放到 M 中 加出来的长度的位置
5、把 M 的地址指向给 N
2、CopyOnWriteArrayList
add()
addIfAbsent() --> 使用这个方法不会添加重复数据,每次都要对数组进行遍历。
3、CopyOnWriteArraySet
底层容器是 CopyOnWriteArrayList
add() 方法就是上面的 addIfAbsent()
十五、Queue 队列
1、关系结构图
2、BlockingQueue 阻塞队列
Queue 继承自 Collection ,具有Collection的功能。
BlockingQueue继承自Queue,具有队列的特点。
具有阻塞的特点。
入队:
非阻塞队列:队列满时,放入第11个数据,数据丢失。
阻塞队列:队列满时,放入第11个数据,进入阻塞,等待队列中有空位。
出队:
非阻塞队列:队列空时,取数据得到 null。
阻塞队列:队列空时,取数据进入阻塞,等待队列中有数据。
3、ArrayBlockingQueue
基于数组实现的阻塞队列,具有阻塞队列的特点;
需要指定数组长度;
不能添加null,会报空指针异常;
因为是基于数组实现,所以不支持读写同时操作,效率略低;
底层插入元素和取出元素用的是同一把锁;
底层实现基于生产者消费者模型和线程通信;
① 基本方法对比
添加数据 | 取出数据 |
add() 非阻塞; 队列满时添加数据报错 | peek() 非阻塞; 取头部数据; 不移除这个数据; 队列空时获取到 null; |
offer() 不完全阻塞; 可以指定阻塞时间,阻塞时间到达时队列满返回 false | poll() 不完全阻塞; 取头部数据; 移除这个数据; 可以指定阻塞时间,时间到时队列空获取到 null; |
put()完全阻塞; 队列满时阻塞,一直满一直阻塞 | take() 完全阻塞; 取头部数据; 移除这个数据; 队列空时阻塞,一直空一直阻塞 |
② 源码解析
4、面试题
问:put()和take()中的 while 为什么不用 if 替换?
答:
因为在放数据的线程被唤醒的瞬间,可能有其他线程先放入数据了,那么这时候底层数组是满的。
又因为线程在被唤醒后,会沿着await()后面的代码继续执行。
所以如果换成 if 会导致数据丢失。
5、LinkedBlockingQueue
基于链表实现,具有阻塞队列的特点。
可以不指定链表长度,不指定长度就是无限长。
支持读写同时操作,效率高。
底层插入元素和取出元素用的不是同一把锁。
以上ArrayBlockingQueue的方法,在LinkedBlockingQueue中均支持。
底层由其内部类Node<E> 实现。
Node 类有两个参数: E item,Node<E> next。
是一个单向链表。
① 源码解析
6、SynchronousQueue
特点:
这个队列必须先从队列中取出元素,然后才能向队列中添加元素。
这个队列连1个容量都没有。
优点:
方便、高效地进行线程间数据的传送。
效率极高,且不会产生队列中数据争抢问题。
① SynchronousQueue 工作图例
② 测试代码
一直蹲着等着取,有一个取一个。
7、PriorityBlockingQueue
是一个有顺序的阻塞队列,放元素的时候没体现顺序性,取元素的时候才会体现顺序性。
底层支持自动扩容,所以认为是无限大的队列。
队列中的元素必须实现比较器。
8、DelayQueue
是一个无界的阻塞队列。
用来存放实现了Delayed接口的对象。
① 案例代码