一 简介
Java容器可大致分为 List、Queue、Set、Map 四种。
List
:存储的元素是有序的、可重复的Set
:存储的元素是无序的、不可重复的Queue
:按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的Map
:使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值
1 线程安全的容器
Collections
工具类提供的 synchronizedXX()
方法,将容器包装成线程安全的【效率低,不推荐】古老的线程安全集合类 Vector
,Hashtable
【效率低,不推荐】 java.util.concurrent
包下的集合类【推荐】
Concurrent
开头的集合类:ConcurrentHashMap, ConcurrentSkipListMap...
CopyOnWrite
开头的集合类:CopyOnWriteArrayList, CopyOnWriteArraySet...
2 CopyOnWrite 写时复制机制
适用于读多写少 的场景,降低写性能换取读性能
读操作无需加锁 写操作时会执行内存复制,在新内存区域执行写操作(此时如果有读操作,读的是旧内存区域的值 ),然后再将原引用指向修改后的内存区域 缺点是复制操作给内存造成较大压力,且无法保证实时可见性,有可能读到旧数据
二 底层数据结构总结
1 List
类 数据结构 备注 ArrayList
Object[]
Vector
Object[]
LinkedList
双向链表 JDK1.6 之前为循环链表,JDK1.7 取消了循环,常用的Java栈、队列实现
栈采用 LinkedList
或 ArrayDeque
的 Deque
(双端队列)接口 队列使用 LinkedList
的 Queue
接口 栈和队列都是双端队列的特殊情况
private static class Node < E > {
E item;
Node < E > next;
Node < E > prev;
}
2 Set
Set
是 Map
的特殊形式,只有 key
有意义,其 value
均为相同的固定值
类 数据结构 备注 HashSet
HashMap
存储的元素无序 LinkedHashSet
LinkedHashMap
存储的元素可以选择按插入顺序 或访问顺序 排序,默认是插入顺序 TreeSet
TreeMap
存储的元素有序
3 Queue
类 数据结构 备注 PriorityQueue
Object[]
实现二叉堆存储的元素按照指定的权值和顺序 组织,首个元素有序,其它元素无序 (常用的Java堆实现 ) ArrayDeque
Object[]
+ 双指针
4 Map
类 数据结构 备注 HashMap
数组+链表/红黑树 存储的元素无序 LinkedHashMap
数组+链表/红黑树,且 Entry
存在双向的引用 存储的元素可以选择按插入顺序 或访问顺序 排序 TreeMap
红黑树(自平衡的排序二叉树) 存储的元素有序 HashTable
数组+链表 存储的元素无序
HashMap
JDK1.8 之前 HashMap
由 数组+链表 组成的,数组是 HashMap
的主体,链表则是主要为了使用拉链法解决哈希冲突而存在的 JDK1.8 以后当链表长度大于阈值(默认为 8)且键值对 个数大于64时,将链表转化为红黑树,以减少搜索时间 如果键值对 个数小于 64,那么会选择先进行数组扩容,而不是转换为红黑树) 相较于 AVL 树,红黑树减弱了对平衡的要求,降低了保持平衡需要的开销
AVL树:
追求绝对平衡,任何节点的两个子树的高度差的绝对值不超过1 实现相对复杂 这种严格的平衡条件使得AVL树在节点增删时,需要进行多次旋转操作来维持树的平衡 红黑树:
追求大致平衡,确保从根到叶子的最长路径不会超过最短路径的两倍长 实现相对简单 这种宽松的平衡条件使得红黑树在节点增删时,只需要进行少量的旋转操作(最多三次旋转)就能达到平衡
class HashMap < K , V > extends . . . {
Entry < K , V > [ ] table;
int size;
int threshold;
float loadFactor;
static class Entry < K , V > implements Map. Entry < K , V > {
K key;
V value;
Entry < K , V > next;
int hash;
}
}
LinkedHashMap
继承自 HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成 每个 Entry
增加了双向引用,构成一条双向链表,可以 保持键值对的插入顺序或访问顺序 访问顺序指的是,对一个 key
执行 put/get
后,将键值对移动到链表末尾(便于实现 LRU) 指定按访问顺序组织链表,需要调用构造方法 public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
并指定 accessOrder = true
TreeMap
存放的元素必须是全序的:构造时元素需要实现 Comparable
接口,或者指定 Comparator
迭代时输出按键排序
class TreeMap < K , V > extends . . . {
Comparator < ? super K > comparator;
Entry < K , V > root;
int size;
static final class Entry < K , V > implements Map. Entry < K , V > {
K key;
V value;
Entry < K , V > left;
Entry < K , V > right;
Entry < K , V > parent;
boolean color;
}
} *
三 Collection 的子接口 List
1 ArrayList 与 Vector
ArrayList
和 Vector
的底层都是 Object[]
存储的,即对数组进行封装ArrayList
是 List
的主要实现类,适用于频繁的查找工作,线程不安全Vector
是 List
的古老实现类,线程安全
2 ArrayList 与 LinkedList
都是线程不安全的 ArrayList
底层使用 Object[]
存储(支持随机访问 ),LinkedList
使用双向链表存储(不支持随机访问 )ArrayList
的空间浪费主要体现在列表的结尾会预留一定的容量空间,而 LinkedList
的空间花费则体现在它的每一个元素都需要消耗比 ArrayList
更多的空间(因为要存放直接后继和直接前驱以及数据)获取/插入/删除元素的时间复杂度不同,体现在数组与链表的区别
3 ArrayList 的 JDK 7/8 差异
JDK 7:ArrayList
的对象的创建类似于单例的饿汉式
ArrayList list = new ArrayList ( ) ;
list. add ( 123 ) ;
list. add ( 11 ) ;
JDK 8: ArrayList
的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存
ArrayList list = new ArrayList ( ) ;
list. add ( 123 ) ;
4 ArrayList 的构造方法与扩容机制*
参考链接
三种构造方法 :无参、传入指定长度、传入 Collection
private static final int DEFAULT_CAPACITY = 10 ;
private static final Object [ ] EMPTY_ELEMENTDATA = { } ;
private static final Object [ ] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = { } ;
transient Object [ ] elementData;
private int size;
"""
容量此时是0,元素个数size为默认值0
"""
public ArrayList ( ) {
this . elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA ;
}
"""
initialCapacity > 0时,容量为initialCapacity,元素个数size为默认值0
initialCapacity = 0时,容量为0,元素个数size为默认值0
initialCapacity < 0时,抛出异常
"""
public ArrayList ( int initialCapacity) {
if ( initialCapacity > 0 ) {
this . elementData = new Object [ initialCapacity] ;
} else if ( initialCapacity == 0 ) {
this . elementData = EMPTY_ELEMENTDATA ;
} else {
throw new IllegalArgumentException ( "Illegal Capacity: " + initialCapacity) ;
}
}
"""
如果传入的Collection不包含元素,容量是0,元素个数size为0
如果传入的Collection包含元素,容量为传入序列的长度,元素个数size也为序列长度,此时的ArrayList是满的
"""
public ArrayList ( Collection < ? extends E > c) {
elementData = c. toArray ( ) ;
if ( ( size = elementData. length) != 0 ) {
if ( elementData. getClass ( ) != Object [ ] . class )
elementData = Arrays . copyOf ( elementData, size, Object [ ] . class ) ;
} else {
this . elementData = EMPTY_ELEMENTDATA ;
}
}
扩容 :add & grow
public boolean add ( E e) {
modCount++ ;
add ( e, elementData, size) ;
return true ;
}
private void add ( E e, Object [ ] elementData, int s) {
if ( s == elementData. length)
elementData = grow ( ) ;
elementData[ s] = e;
size = s + 1 ;
}
private Object [ ] grow ( ) {
return grow ( size + 1 ) ;
}
"""
if语句中不会处理 用默认 __无参构造方法__ 创建的数组的 __初始扩容__ 情况,其余扩容情况都是由if语句处理
ArraysSupport.newLength函数的作用是创建一个大小为oldCapacity + max(minimum growth, preferred growth)的数组
minCapacity是传入的参数,上面显示它的值是当前容量+1,那么minCapacity - oldCapacity的值就恒为1,minimum growth的值也就恒为1
oldCapacity >> 1 右移一位,也就是减半,preferred growth的值即为oldCapacity大小的一半(当oldCapacity为0时,右移后还是0)
"""
private Object [ ] grow ( int minCapacity) {
int oldCapacity = elementData. length;
if ( oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA ) {
int newCapacity = ArraysSupport . newLength ( oldCapacity,
minCapacity - oldCapacity,
oldCapacity >> 1 ) ;
return elementData = Arrays . copyOf ( elementData, newCapacity) ;
} else {
return elementData = new Object [ Math . max ( DEFAULT_CAPACITY , minCapacity) ] ;
}
}
容量增加 1 的情况:
原来的容量为 0,而且是有参构造器创建的 ArrayList
(传入 0 或者是空 Collection ,不能是无参构造器创建) 容量变为原来1.5倍的情况:
容量变为 max(DEFAULT_CAPACITY, minCapacity)
的情况:
原来的容量为 0,而且是无参构造器创建的 ArrayList
(其中 DEFAULT_CAPACITY = 10
) 用默认无参构造方法创建的数组在添加元素前,ArrayList
的容量为0,添加一个元素后,ArrayList
的容量就变为 10
四 Collection 的子接口 Set
1 HashSet、LinkedHashSet 和 TreeSet
HashSet
LinkedHashSet
底层数据结构是 LinkedHashMap
,元素的插入和取出顺序满足 FIFO 或 LRU 规则 是 HashSet
的子类,在添加数据的同时,每个 Entry
还维护了两个引用,记录此数据前一个数据和后一个数据 对于频繁的遍历操作,LinkedHashSet
效率高于 HashSet
TreeSet
底层数据结构是 TreeMap
,元素是有序的,排序的方式有自然排序和定制排序 比较两个对象是否相同的标准为:compare()
返回 0,而不再是 equals()
不能放入无法排序的对象 使用场景
都不是线程安全的 HashSet
用于不需要保证元素插入和取出顺序的场景LinkedHashSet
用于保证元素的插入和取出顺序满足 FIFO 或 LRU 的场景TreeSet
用于支持对元素自定义排序规则的场景
public void test2 ( ) {
Comparator com = new Comparator ( ) {
@Override
public int compare ( Object o1, Object o2) {
} else {
}
}
} ;
TreeSet set = new TreeSet ( com) ;
}
2 HashSet / HashMap 加入实例时,查重的方式**
当对象加入 HashSet
时,HashSet
会先计算对象的 hashcode值来判断对象加入的位置
如果该位置无对象,则 HashSet 中无重复元素,加入成功 如果该位置有对象(下标相同的两个对象, hashcode不一定相同 ),与相同位置的其他的对象的 hashcode 值作比较
如果没有相符的 hashcode,则无重复元素,加入成功 有相同 hashcode 值的对象,这时会调用 equals()
方法来检查 hashcode 相等的对象是否真的相同,如果两者相同,HashSet
就不会让加入操作成功
public void test3 ( ) {
HashSet set = new HashSet ( ) ;
Person p1 = new Person ( 1001 , "AA" ) ;
Person p2 = new Person ( 1002 , "BB" ) ;
set. add ( p1) ;
set. add ( p2) ;
System . out. println ( set) ;
p1. name = "CC" ;
set. remove ( p1) ;
System . out. println ( set) ;
set. add ( new Person ( 1001 , "CC" ) ) ;
System . out. println ( set) ;
set. add ( new Person ( 1001 , "AA" ) ) ;
System . out. println ( set) ;
}
五 Collection 的子接口 Queue
1 Queue 与 Deque
2 PriorityQueue【Java堆实现】
PriorityQueue
利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据PriorityQueue
通过堆元素的上浮和下沉,实现了在 O(logn)
的时间复杂度内插入元素和删除堆顶元素。PriorityQueue
是非线程安全的,且不支持存储无法比较的对象PriorityQueue
同样要求元素实现 Comparable
接口,或传入 Comparator
对象遍历输出时,只有首个元素是有序的
3 ArrayDeque 与 LinkedList【Java栈和队列实现】
ArrayDeque
和 LinkedList
都实现了 Deque
接口,两者都具有双端队列的功能,可以实现栈和队列 ArrayDeque
是基于可变长的数组和双指针来实现,而 LinkedList
则通过链表来实现ArrayDeque
插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)
虽然 LinkedList
不需要扩容,但是每次插入数据时需要申请空间,均摊性能相比更慢 从性能的角度上,选用 ArrayDeque
来实现队列要比 LinkedList
更好
六 Collection 的子接口 Map
1 HashMap 和 Hashtable
Hashtable
线程安全,内部的方法基本都经过 synchronized
修饰 不允许有 null 键和 null 值,否则会抛出 NullPointerException
创建时如果给定了容量初始值,会直接使用给定的大小 基本被淘汰,不要在代码中使用它 HashMap
非线程安全 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个 总是使用 2 的整数次幂作为数组长度,创建时如果给定了容量初始值,会将其扩充为 2 的幂次方大小
2 HashMap 的长度为什么是 2 的幂次方
Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的,但一个 40 亿长度的数组,内存是放不下的,用之前还要先做对数组的长度取模 运算,得到的余数才能用来要存放的位置也就是对应的数组下标。位运算比求模更高效,hash%length == hash&(length-1)
的前提是 length
是 2 的整数次幂 便于调整原数据在新数组中的索引,根据原来的 hash 值新增的 bit 是0还是1,0=索引不变,1=原来的索引 + 原来哈希表的长度
3 ConcurrentHashMap 和 Hashtable
均线程安全 ConcurrentHashMap
不允许 key/value 为 null
,但是 HashMap
允许
如果允许 value = null 当调用 map.get(key)
返回 null 的时候,代表两种含义
不存在这个key 这个key是存在的,只是 value 设置为 null 此时如果有A、B两个线程,A线程调用 get(key)
方法返回 null,但是不知道属于上述哪种情况 假设是第一种情况,调用 containsKey(key)
方法去做一个判断,期望的返回结果是 false 但是恰好在A线程 get(key)
之后,调用 constainsKey(key)
方法之前B线程执行了 put(key, null)
,那么当A线程执行完 containsKey(key)
方法之后我们得到的结果是 true,与预期的结果不符
类型 数据结构 使用的锁 ConcurrentHashMap
JDK1.7Segment 数组 + HashEntry 数组 + 链表 Segment
(本质是 ReentrantLock
),每次锁若干 HashEntry
ConcurrentHashMap
JDK1.8Node 数组 + 链表/红黑树 synchronized
,每次锁一个 Node
Hashtable
数组+链表 synchronized
,每次锁全表
在 JDK1.7 的时候,ConcurrentHashMap
采用分段锁机制,对整个桶数组进行了分割分段(Segment
,每个 Segment
都是一个可重入锁),每一个 Segment
只锁容器其中一部分数据,多线程访问容器里不同数据段的数据不会存在锁竞争,提高并发访问率
static class Segment < K , V > extends ReentrantLock implements Serializable { . . . }
JDK1.8 的时候已经摒弃了 Segment
的概念,synchronized
只锁定当前链表或红黑二叉树的首节点,并发控制使用 synchronized
和 CAS 来操作
在 JDK1.8 中还能看到 Segment
的数据结构,只是为了兼容旧版本 CAS 的操作体现在:写入 (key, value) 时,如果数组位置为空,则使用 CAS 写入当前键值对;如果数组位置不为空,则通过 synchronized
加锁写入链表/红黑树
Hashtable
(同一把锁 ) :使用 synchronized
来保证线程安全,效率非常低下
当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低
4 为什么JDK1.8的 ConcurrentHashMap 使用 CAS+Synchronized 代替 Segment*
Segment
数组本质上是 ReentrantLock
的数组,其中的每一个 ReentrantLock
锁的是 HashEntry
数组的若干个位置
如果把每个 ReentrantLock
锁的范围细化为一个位置,是否能与 synchronized
锁一个位置的效果相同?答案是否定的,因为Synchronized
在优化后有偏向锁、轻量级锁和重量级锁三个等级,在不同场景下均优于 ReentrantLock
:
锁被细化到一个哈希桶,出现并发争抢的可能性就很低了。对于一个哈希桶,在没有多线程竞争时,使用 Synchronized
的偏向锁机制的效率是最高的 出现争抢时,Synchronized
轻量级锁具有自旋机制,避免线程状态切换引起的开销;而 ReentrantLock
倾向于将获取不到锁的线程挂起
5 CAS (Compare And Swap)
V:要更新的变量(var) E:预期值(expected) N:新值(new)
比较并交换的过程:判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做 当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作
6 JDK8 相较于 JDK7 在底层实现方面的不同
JDK 8 new HashMap()
时,底层没有创建数组,首次调用 put()
方法时,底层创建长度为16的数组(和 ArrayList
的实现一样,延迟申请空间,由饿汉式变为懒汉式) JDK7 底层结构是 HashEntry 数组+链表 ;JDK8 中底层结构:Node 数组 + 链表 / 红黑树 (数组元素类型的改变是因为支持了链表转红黑树)
当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前键值对个数 > 64时,此时此索引位置上的所数据改为使用红黑树存储
7 HashMap 底层实现的参数
DEFAULT_INITIAL_CAPACITY
: HashMap的默认容量(数组长度),16 DEFAULT_LOAD_FACTOR
:HashMap的默认装载因子:0.75 ,是对空间和时间效率的一个平衡选择threshold
:扩容的临界值,数值上等于 容量*填充因子:16 * 0.75 => 12TREEIFY_THRESHOLD
:Bucket中链表长度大于该默认值,转化为红黑树,默认8MIN_TREEIFY_CAPACITY
:桶中的Node被树化时最小的hash表容量,默认64
9 JDK 8 HashMap 的扩容
HashMap
的数组长度一定是 2 的幂次,目的是方便计算哈希值对数组长度取模HashMap
只有在插入元素时才会初始化(创建长为16的 Node
数组),或者扩容
具体地,扩容还要满足两个条件之一
存入当前数据导致大于扩容阈值 存入数据到某一条链表时,该链表数据个数大于 8,且键值对个数小于 64 扩容的具体操作:
将 Node 数组长度变为原来的 2 倍 调整原数据在新数组中的索引,调用 resize
方法,根据原来的 hash 值新增的 bit 是0还是1,0=索引不变,1=原来的索引 + 原来哈希表的长度
七 Iterator
1 类与接口
Iterable
接口有方法 iterator()
返回 Iterator
对象,使用该对象的方法进行遍历对象实现了 Iterable
,就可以使用 for each
语法 不实现 Iterable
的类也可以创建 Iterator
对象
public interface Iterable < T > {
Iterator < T > iterator ( ) ;
}
public interface Iterator ( ) {
boolean hasNext ( ) ;
E next ( ) ;
void remove ( ) ;
}
2 只读使用方法
Iterator iterator = coll. iterator ( ) ;
while ( iterator. hasNext ( ) ) {
System . out. println ( iterator. next ( ) ) ;
}
3 迭代的问题:在循环中增加或删除元素
只能使用 Iterator
,因为迭代器内部会维护一些索引位置相关的数据,迭代过程中容器不能发生结构性变化,否则这些数据会失效 如果还未调用 next()
或在上一次调用 next()
方法之后已经调用了 remove()
方法,再调用 iterator.remove()
都会抛出 IllegalStateException
iterator 调用的 remove()
,和集合对象的 remove()
不同
Iterator iterator = coll. iterator ( ) ;
while ( iterator. hasNext ( ) ) {
Object obj = iterator. next ( ) ;
if ( "Tom" . equals ( obj) ) {
iterator. remove ( ) ;
}
}
八 Collections 工具类
核心思想是面向接口编程:Collections
提供了很多针对容器接口 的通用算法和功能,只要实现了指定接口,就能调用其中的功能 提供的功能大概分为如下两类
1 对传入的容器接口对象进行操作:查找/替换/排序/添加/修改
public static < T > int binarySearch ( List < ? extends Comparable < ? super T > list, T key> ) ;
public static < T > int binarySearch ( List < ? extends T > list, T key, Comparator < ? super T > c) ;
public static int frequency ( Collection < ? > c, Object o) ;
. . .
2 返回一个容器接口对象
适配器:将其它类型的数据转换成容器接口对象(输入其它类型,输出容器)
public static final < T > List < T > emptyList ( ) ;
public static final < T > Set < T > emptySet ( ) ;
public static final < K , V > Map < K , V > emptyMap ( ) ;
public static < T > Iterator < T > emptyIterator ( ) ;
public static < T > Set < T > singleton ( T o) ;
public static < T > List < T > singletonList ( T o) ;
public static < K , V > Map < K , V > singletonMap ( K key, V value) ;
装饰器:修饰给定的容器接口对象,对其增强(输入容器,输出容器)
public static < T > Collection < T > unmodifiableCollection ( Collection < ? extends T > c) ;
public static < T > List < T > unmodifiableList ( List < ? extends T > list) ) ;
public static < T > Set < T > unmodifiableSet ( Set < ? extends T > set) ) ;
public static < K , V > Map < K , V > unmodifiableMap ( Map < ? extends K , ? extends V > m) ;
public static < E > List < E > checkedList ( List < E > list, Class < E > type) ;
. . .
public static < T > List < T > synchronizedList ( List < T > list) ) ;
. . .