集合由两大接口派生而来,分别是Collection和Map;
Collection用来存放单一元素,三个主要的子接口:List,Set,Queue;
Map用来存放键值对;
Collection接口
有三个子接口,List,Set,Queue;
List:
ArrayList-对象数组, Vector-对象数组, LinkedList-双向链表;
ArrayList
ArrayList是List的主要实现类,底层采用 Object [] 数组存储,适用于频繁的查找工作,不同步,线程不安全;
插入和删除元素的时间复杂度受元素位置的影响,如果在add()插入元素,默认插入在ArrayList尾部,O(1) ; 如果在指定位置 i 插入/删除元素,O( n - i ), 因为集合中第 i 个元素以及后面的元素都要移动;
支持使用 get() 快速随机访问,并且实现了RandomAccess接口;(并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的,这个接口只是一个标志)
有一定的空间浪费,ArrayList 的 List 列表结尾会预留有一定的容量空间;
ArrayList的扩容机制
默认初始容量大小 DEFAULT_CAPACITY = 10;
以无参构造方法创建ArrayList的时候,初始化赋值的是一个空数组,在真正对数组进行添加元素操作的时候,才真正分配容量;
- 向数组中add第一个元素时,进入add方法,add方法调用了ensureCapacityInternal,传入了minCapacity参数,又调用了ensureExplicitCapacity,在这里比较minCapacity和elementData.length,判断是否要扩容,如果需要就进入grow()方法;添加第一个元素之前,令minCapacity = 10, 进入grow()方法,数组容量扩为10,;
- add第二个元素到第十个元素时都不会扩容,minCapacity = 2,… 10, 在添加第11个元素时要进入grow() 方法进行扩容, 此时 min Capacity = 11 > elementData.length;
- 在grow() 方法中,有int newCapacity = oldCapacity + (oldCapacity >> 1),因此每次扩容都会扩到原来的1.5倍左右;
Vector
Vector是List的古老实现类,底层也采用 Object [] 数组存储,线程安全;
LinkedList
底层采用双向链表实现,不同步,线程不安全;
如果在头尾插入/删除元素,O(1);如果在指定位置 i 插入/删除元素,需要先移动到指定位置再操作,是O(n) ;
不支持快速随机访问;
空间花费体现在LinkedList的每一个元素都要消耗比ArrayList更多的空间,因为不仅要存放数据,还要存前后指针;
Set
三个实现类都不线程安全
HashSet
底层数据结构是哈希表,基于HashMap实现的;
无序性:不等于随机性,是指存储的数据在底层数组中并非按照数据索引的顺序添加,而是根据数据的哈希值决定的;;
不可重复性:Set里已经有的数据不能加入;
如果新加入的数据的哈希值equals(Set中已经存在的数据),且他们的hashcode()也相等,证明冲突了,不能加入;Set要重写equals()和hashcode()方法;
用于不需要保证元素插入和取出的顺序的场景;
LinkedHashSet
底层数据结构是链表和哈希表,元素的插入和取出顺序FIFO;
是HashSet的子类,内部通过LinkedHashMap实现;
用于保证元素的插入和取出顺序满足FIFO的场景;
TreeSet
底层数据结构是红黑树(自平衡的排序二叉树);
元素是有序的;
用于支持对元素自定义排序规则的场景;
Queue
Queue和Deque
Queue是单端队列,扩展了Collection接口;
Deque是双端队列,扩展了Queue的接口,还可以用于模拟栈;
PriorityQueue
(经常出现在面试手撕中,堆排序,前K大数,带权图遍历)
Object[],用数组来实现二叉堆;
元素的出对顺序是与优先级相关的,优先级最高的先出;
利用了二叉堆的数据结构,底层用变长的数据来存储数据,插入/删除(顶)O(logn),不支持null和non-comparator对象,默认是小顶堆,可以接受Comparator作为构造参数,自定义元素优先级的先后。
ArrayQueue
Object[],可变长数组 + 双指针;
ArrayQueue与LinkedeLIst都实现了Deque接口;
ArrayQueue基于可变长的数组和双指针实现,不支持存储null数据,选用它实现队列比LinkedList更好,也可以用来实现栈;
Map接口
Map的主要实现类有HashMap,LinkedHashMap,HashTable,TreeMap;
HashMap
线程不安全;
JDK1.8之前HashMap的底层数据结构主要是由 数组 + 链表 组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”);
JDK1.8之后,解决哈希冲突有了较大变化,当链表长度大于阈值(默认8)的时候,将链表转为红黑树,用来减少搜索时间。在转之前,会判断当前数组的长度,如果小于64,会首先选择进行数组扩容,而不是转为红黑树。
初始默认大小16,每次扩变为2倍;如果初始化给值,默认扩成2的幂次方;
HashMap为什么线程不安全?
多线程环境下,在JDK1.7的扩容中,需要进行元素转移,使用的是头插法,也就是链表的顺序会反转,会形成死循环,还可能造成数据丢失;JDK1.8的多线程环境下发生哈希碰撞的时候不再用头插法的方式,而是直接插入链表尾部,不再形成循环链表,但是还是有可能出现数据错误的情况,多线程情况下一个线程插入的数据可能被另一个线程覆盖掉。
ConcurrentHashMap
底层数据结构:
1.8之前采用分段的数组+链表,1.8之后和HashMap一样;
实现线程安全的方式(*):
JDK1.7的时候,对整个桶数组进行了分割分段,采用了Segment分段锁,继承自ReentrantLock可重入锁,每一把锁只锁容器其中的一部分数据,多线程访问容器里不同数据段的数据,就不存在锁竞争,提高了并发访问率;由Segment数组结构和HashEntry数组结构组成,Segment数组中的每个元素包含一个HashEntry数组,每个HashEntry数组属于链表结构:最大并发度是Segment的个数,默认16;

(图片来自JavaGuide)
JDK1.8的时候,已经摒弃了Segment的概念,而是直接用Node数组+链表/红黑树的数据结构来实现,并发控制使用同步锁synchronized和CAS来操作,锁的粒度更细;整个看起来就像是优化过且线程安全的HashMap;Node数组 + 链表/红黑树,Node只能用于链表的情况,红黑树要使用TreeNode:synchronized只锁住当前链表或红黑树的首节点,这样只要hash不冲突,就不会影响其他node的读写;默认并发度是Node数组的大小;

(图片来自JavaGuide)
LinkedHashMap
继承自HashMap,底层还是数组+链表(红黑树)组成的。此外,增加了一条双向链表,使上面的结构可以保持键值对的插入顺序,实现访问顺序相关逻辑;
HashTable
线程安全;HashTable内部的方法都经过同步锁synchronized修饰;
不能存储null的key和value;
数组 + 链表, 数组是主体,链表解决哈希冲突
初始化不指定大小时,默认11, 每次扩都变为2n + 1;
TreeMap
红黑树(自平衡排序二叉树);
相比HashMap,TreeMap主要多了对集合中的元素根据键排序的能力,以及对集合内部元素的搜索能力;
本文详细介绍了Java集合框架中的Collection和Map接口,包括它们的子接口如List、Set和Queue,以及各种实现类如ArrayList、LinkedList、HashSet、HashMap和ConcurrentHashMap等。文章讨论了这些数据结构的实现原理、性能特点和线程安全性,特别是ArrayList的扩容机制和HashMap的红黑树转换策略。

被折叠的 条评论
为什么被折叠?



