Java中的集合(也叫容器)主要由两个接口派生,一个是Collection,一个是Map。
Collection
List
List 和 Set 都是继承自 Collection 接口,List 的特点是元素有放入顺序,元素可以重复(可以插入多个null),可以通过元素索引访问元素。即,支持for循环,也支持迭代器。
List 基于实现有数组和链表等,可以动态增长,基于实现不同对插入、删除与查找的效率有区别。
ArrayList
底层是动态数组(Object[]),容量可以动态增长,是非线程安全的。如果指定元素插入的位置时间复杂度为O(n-i),如果插入在末尾时间复杂度为O(1),支持快速随机访问,属于预分配内存。可以初始化size,也可以不初始化,默认容量大小为10。自动增长可能会触发扩容,每次扩容后容量会提升为之前的1.5倍(递增式再分配),扩容会带来数据向新数组的重新拷贝的过程,因此如果可以预知所要盛放的元素数量可以在构造方法中指定初始化的容量。如果未在构造方法中声明,后续需要添加大量元素,为减少递增式再分配带来的频繁拷贝性能损耗,可以调用ensureCapacity()执行增加ArrayList实例的容量。
构造方法详解
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* 默认初始化容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 实际arrayList的元素个数,并不是容量大小
*/
private int size;
/**
* 保存添加到ArrayList中的元素的数组
*/
transient Object[] elementData;
/**
* 空列表(有参传0初始化)
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 空列表(无参初始化)
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 指定容量的构造方法
*/
public ArrayList(int initialCapacity) {
// 如果指定的容量 > 0 则构建对应大小的数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 指定的容量为 0 则构建一个空列表
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 指定容量 < 0 则抛异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* 无参构造方法
*/
public ArrayList() {
// 构造一个初始容量为10的空列表
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 指定collection元素列表的构造方法
*/
public ArrayList(Collection<? extends E> c) {
// 转换构造数组对象
Object[] a = c.toArray();
// 数组长度不为0
if ((size = a.length) != 0) {
// 如果集合本身就是ArrayList,则直接赋值
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
// 返回一个数组 Object[].class类型的,大小为size, 元素为a中元素
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// 数组长度为0
elementData = EMPTY_ELEMENTDATA;
}
}
......
}
备注:无参构造初始情况size = 0,elementData = {}。在添加第一个元素的时候,会触发 grow() 方法,实现了elementData数组初始容量为10的构造。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* 添加元素
*/
public boolean add(E e) {
modCount++;
// 调用重载方法
// 默认无参/0长度构造 elementData = {} , size = 0
add(e, elementData, size);
return true;
}
/**
* 添加元素重载
*/
private void add(E e, Object[] elementData, int s) {
// 调用grow()方法
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
private Object[] grow() {
// 传递1
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
// 透传1
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
private int newCapacity(int minCapacity) {
// 此时 oldCapacity = 0, minCapacity = 1
int oldCapacity = elementData.length;
// 计算后 newCapacity = 0
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 此时 满足 0 - 1 <= 0
if (newCapacity - minCapacity <= 0) {
// 无参构造 此时容量变为10返回
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
// 报错
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 返回 1
return minCapacity;
}
// private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
......
}
ArrayList 的扩容机制
默认ArrayList的扩容机制是一种递增式再分配的方式,新容量会是之前容量的1.5倍,当计算出新容量后进行实例化,将所有元素复制到新数组中。或者调用 ensureCapacity 进行扩容,扩容后也是将所有元素复制到新数组中。
// 扩容为之前1.5倍的核心实现代码
int newCapacity = oldCapacity + (oldCapacity >> 1);
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* 指定扩容
*/
public void ensureCapacity(int minCapacity) {
// 指定容量 > 当前数组长度 并且
// 数组容量不是默认 且 指定容量不小于默认容量10
if (minCapacity > elementData.length
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) {
modCount++;
grow(minCapacity);
}
}
/**
* 执行扩容
*/
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
......
}
备注:
1:ArrayList 是可以添加 null 值的
2:ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响
3:线程安全的ArrayList的可以使用Vector,也可以使用 Collections.synchronizedList ,使用 CopyOnWriteArrayList (读多写少),使用同步机制控制ArrayList的读写
LinkedList
底层使用双向链表实现(JDK1.6之前使用循环链表,JDK1.7&JDK1.7+弃用循环链表),头插尾插时间复杂度都是O(1),指定位置插入时间复杂度O(n),非线程安全。继承自 Deque,持有双端队列特性。没有实现 RandomAccess 不支持随机访问,由于底层实现为链表结构在内存上是不连续的,只能通过指针来定位而无法实现快速访问。
常用增删特性详解
1:add(E e) 方法 尾插元素
用于在 LinkedList 的尾部插入元素,即将新元素作为链表的最后一个元素,时间复杂度为 O(1)
2:add(int index, E element) 指定位置插入元素
需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)
3:addFirst(E e) 头插,addLast(E e) 尾插
只需要修改头/尾结点的指针即可完成插入操作,因此时间复杂度为 O(1)
4:获取元素
getFirst() 获取链表的第一个元素
getLast() 获取链表的最后一个元素
get(int index) 获取链表指定位置的元素
5:removeFirst()
删除并返回链表的第一个元素,时间复杂度为 O(1)
6:removeLast()
删除并返回链表的最后一个元素,时间复杂度为 O(1)
7:remove(E e)
删除链表中首次出现的指定元素,如果不存在该元素则返回 false
8:remove(int index)
删除指定索引处的元素,并返回该元素的值
9:void clear()
移除此链表中的所有元素
LinkedList中的Node
private static class Node<E> {
// 当前元素
E item;
// 后继
Node<E> next;
// 前驱
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
Vector
底层使用动态数组(Object[]),容量可以动态增长,是线程安全的。线程安全主要是因为方法级别的synchronized关键字。自动增长可能会触发扩容,每次扩容后容量会提升为之前的2倍(即100%扩容)。
Vector 与 Stack
Stack 继承自 Vector,实现后进先出,并保证了线程安全。
CopyOnWriteArrayList
线程安全的ArrayList,采用读写分离的并发策略实现,写时复制的思想。允许并发读且读操作不加锁,实现高性能读操作。写操作先从原有的数组中拷贝一份出来,然后在新的副本数组做写操作,操作结束后再将原数组引用指向到新数组。
场景详解
优点:读读不互斥,性能有保障。读写操作的时候,由于操作作用在不同的list容器,因此也不会有modCount的问题触发 ConcurrentModificationException
缺点:内存占用高,系统开销大,每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC。数据无法保障实时性,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是旧 list 的数据。
写时复制实现详解
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// lock 同步锁对象
final transient Object lock = new Object();
// volatile 修饰的array
private transient volatile Object[] array;
// 写操作 新增元素
public boolean add(E e) {
// 尾部插入元素 使用 synchronized
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
......
}
备注:在JDK1.8以及以前的版本中以及很多源码讲解中,CopyOnWriteArrayList 的锁实现都是用了一个重入锁实现(ReentrantLock),重入锁基于AQS实现。ReentrantLock是Lock的实现,是JDK的接口实现是JVM无法控制的,需要配合程序代码的finally硬编码解锁,很容易发生死锁。而 synchronized 是关键字,而且当前JDK都拥有了锁升级的过程,因此轻易都不会遇到瓶颈,并且JVM可以在 synchronized 发生异常时,自动释放线程占有的锁,不会导致死锁现象发生。并且编码风格简练(本文源码基于JDK11)。
// JDK1.8中的CopyOnWriteArrayList的锁实现:
final transient Object lock = new Object();
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// ****
return true;
} finally {
// 解锁
lock.unlock();
}
}
Set
Set 和 List 都是继承自 Collection 接口,Set 的特点是元素无放入顺序,元素不可重复,重复元素会覆盖(只可以插入一个null),只能基于元素本身访问。即,只支持迭代器。
Set 对元素的检测效率低,删除和插入的效率高,插入与删除元素不会引起整体容器内元素位置的变化。
三大实现的对比
HashSet | LinkedHashSet | TreeSet | |
父接口 | Set | Set | Set |
元素唯一 | 是 | 是 | 是 |
线程安全 | 否 | 否 | 否 |
底层数据结构 | 哈希表(基于HashMap) | 链表+哈希表 | 红黑树 |
是否有序 | 无 | FIFO | 自然排序或定制排序 |
使用场景 | 不需要保证元素插入和取出顺序 | 保证元素的插入和取出顺序满足 FIFO | 支持对元素自定义排序规则的场景或自然排序 |
HashSet的详解
HashSet 基于 HashMap 实现。放入HashSet中的元素实际上由HashMap的key来保存,而HashMap的value则存储了一个静态的Object对象
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
// 底层基于HashMap
private transient HashMap<E,Object> map;
// 静态val对象
private static final Object PRESENT = new Object();
// 是否存在的判断
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
......
}
详细解析见HashMap,在高版本的JDK中无论HashSet中是否已经存在了某个元素,HashSet都会直接插入,由于在HashMap的putVal() 方法中如果插入位置没有元素返回null,否则返回上一个元素,HashSet的add()方法正是通过这个返回值判断插入前是否存在相同元素
Comparable和Comparator的区别
Comparable:java.lang包,使用 compareTo(Object obj) 方法用来排序。实现Comparable接口并重写compareTo方法就可以实现某个类的排序了,它支持Collections.sort和Arrays.sort的排序。正序从小到大的排序规则是:使用当前的对象值减去要对比对象的值;而倒序从大到小的排序规则刚好相反:是用对比对象的值减去当前对象的值。如果自定义对象没有实现Comparable接口,那么是不能使用Collections.sort方法的,也就是说使用Comparable必须要修改原有的类。
Comparator:java.util 包,使用 compare(Object obj1, Object obj2)方法用来排序,实现一个自定义的比较器。Comparator除了可以通过创建自定义比较器外,还可以通过匿名类的方式,更快速、便捷的完成自定义比较器的功能。无论那种方式,Comparator都无需修改原有类。
无序性和不可重复性
1:无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的
2:不可重复性是指添加的元素按照 equals() 判断时 ,返回 false,需要同时重写 equals() 方法和 hashCode() 方法
Queue
继承自 Collection 接口,队列集合。按特定的排队规则来确定先后顺序,存储的元素是有序的且可重复的。
Queue与Deque
Queue:单端队列,只能从一端插入元素,另一端删除元素,先进先出(FIFO)
Queue失败后处理方式详解
抛出异常 | 返回特殊值 | |
插入队尾 | add(E e) | offer(E e) |
删除队首 | remove() | poll() |
查询队首元素 | element() | peek() |
Deque:双端队列,在队列的两端均可以插入或删除元素,另外提供有 push() 和 pop() 等其他方法用于模拟栈
Deque失败后处理方式详解
抛出异常 | 返回特殊值 | |
插入队首 | addFirst(E e) | offerFirst(E e) |
插入队尾 | addLast(E e) | offerLast(E e) |
删除队首 | removeFirst() | pollFirst() |
删除队尾 | removeLast() | pollLast() |
查询队首元素 | getFirst() | peekFirst() |
查询队尾元素 | getLast() | peekLast() |
备注:
1:ArrayDeque 基于可变长的数组和双指针来实现队列,不支持存储null,插入的时间复杂度O(1),也可以用于实现栈。
2:PriorityQueue 利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据,元素出队顺序是与优先级相关的,优先级最高的元素先出队。通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素,支持存储 NULL 和 non-comparable 的对象,默认是小顶堆,但可以接收一个 Comparator 作为构造参数,从而自定义元素优先级
BlockingQueue
BlockingQueue (阻塞队列)是一个继承自 Queue的接口。BlockingQueue阻塞的情况
1:支持当队列没有元素时一直阻塞,直到有元素
2:支持如果队列已满,一直等到队列可以放入新元素时再放入
生产消费模型的典型实现
常见实现对比
ArrayBlockingQueue | LinkedBlockingQueue | |
是否线程安全 | 是 | 是 |
底层实现 | 数组 | 链表 |
队列模式 | 有界队列 | 无界队列/有界队列 |
定界方式 | 创建时指定容量大小 | 不指定容量大小,默认是Integer.MAX_VALUE,无界队列 创建时指定队列大小,实现有界队列 |
线程安全锁实现 | 生产和消费用同一个锁 | 防止生产者和消费者线程之间的锁争夺,生产用的是putLock,消费是takeLock |
内存占用 | 预分配内存 | 动态分配内存 |
Map
HashMap
HashMap基于JDK的版本不同底层实现有细微差异,为数组+链表或数组+链表+红黑树的方式,无论哪种实现,都是线程不安全的。HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。HashMap 默认的初始化大小为 16,之后每次扩充,容量变为原来的 2 倍。HashMap 中存在一个 tableSizeFor() 方法,该方法只会把构造方法的传入值变成参考值,即传入值会经过 tableSizeFor() 转换成向上取最近的2的幂作为声明的哈希表的大小。
在JDK1.8以及以上的版本,HashMap底层全部更迭为数组 + 链表(尾插法,与LinkedList同理:为了避免出现逆序且链表死循环的问题) + 红黑树的数据结构,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。当树中的数据节点数量小于6的时候,红黑树又会退化成链表(只会出现在resize()逻辑中)。红黑树是一种时间复杂度平衡树,在执行插入、删除以及查找的时间复杂度都是 O(log2n)。
备注:红黑树详解 :http://t.csdnimg.cn/YjLdN
HashMap存储详解
HashMap 通过 key 的 hashcode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(n为数组length)。如果当前数组位不存在元素,则直接构建链表存放,如果存在元素,则判断元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。其中二次扰动是为了防止部分hashCode()的实现散列性差,经过二次扰动可以尽量减少碰撞
构造方法与常量
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 默认大小 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量 1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子(基于泊松分布)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表 -> 树 阈值
static final int TREEIFY_THRESHOLD = 8;
// 树 -> 链表 阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 最小转换树的数组长度阈值
static final int MIN_TREEIFY_CAPACITY = 64;
// 默认无参构造 负载因子是0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
// 有参容量构造
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 可传入自定义容量与负载因子构造
public HashMap(int initialCapacity, float loadFactor) {
// 容量不可以为负
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 数组最大容量为 1073741824
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 负载因子 非浮点数值 或 小于等于0 都不合法
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 执行tableSizeFor() 构建2的幂的容量size
this.threshold = tableSizeFor(initialCapacity);
}
......
}
负载因子与哈希冲突
负载因子的作用就是标识Hash表中的填满程度(公式为:填入的元素个数 / 散列表的长度)。负载因子越大,填满的元素越多,空间利用率越高,但发生冲突的机会变大。而加载因子越小,填满的元素越少,冲突发生的机会减小,但空间利用率降低,还会提高扩容rehash操作的次数。
HashMap是一个典型的插入慢查询快的数据结构,类似这种数据结构存在两种问题,哈希冲突和为了避免发生哈希冲突导致的空间利用率不高,因此需要有负载因子的调和,在HashMap中进行调和的不仅仅有负载因子,还有解决hash冲突的两个算法选择,开放地址 + 拉链法。
// todo 开放地址和拉链法
数据存储逻辑详解
/**
* put(key, val); 逻辑
*/
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// key.hashCode() 和 key.hashCode() >>> 16 进行异或操作
static final int hash(Object key) {
int h;
// 高16bit补0,一个数和0异或不变,高16bit不变,低16bit和高16bit做了一个异或
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 实际putVal的过程
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果当前table == null 或者 table的长度为0
if ((tab = table) == null || (n = tab.length) == 0)
// 执行resize() 此处一般针对默认new HashMap<>()的情况 此时n = 16
n = (tab = resize()).length;
// 计算index (n - 1) & hash
// 判断index 是否存在hash碰撞,未发生碰撞,则生成新节点
if ((p = tab[i = (n - 1) & hash]) == null)
// newNode节点 存放在tab数组中 逻辑就是插入增加mod就结束了 返回null
tab[i] = newNode(hash, key, value, null);
else {
// 发生了碰撞逻辑
Node<K,V> e; K k;
// key的hash值相等且equals为true或内存地址相同 (key是相同的)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 赋值到标记node e (后续直接覆盖的时候返回旧val)
e = p;
else if (p instanceof TreeNode)
// 此时已经是红黑树逻辑
// 在红黑树中进行插入
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 此时还是链表 (拉链法迭代)
for (int binCount = 0; ; ++binCount) {
// 遍历链表key不存在(到达链表的尾部)
if ((e = p.next) == null) {
// 尾插新节点
p.next = newNode(hash, key, value, null);
// 判断链表的长度是否达到转化红黑树的临界值,临界值为8
if (binCount >= TREEIFY_THRESHOLD - 1)
// 链表转红黑树(不一定100%变树 具体实现在treeifyBin解析中)
treeifyBin(tab, hash);
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 遍历标记位
p = e;
}
}
// e 不为空 就是key相同覆盖的逻辑
if (e != null) {
// 获取key对应的旧的value
V oldValue = e.value;
// 执行覆盖
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
// 返回key对应的旧value值(不存在则是null,HashSet判断元素在集合是否存在的依据)
return oldValue;
}
}
// 增加模数
++modCount;
// 判断是否需要扩容
if (++size > threshold)
resize();
// 插入后的后置处理逻辑 在LinkedHashMap有重写处理实现自定义迭代
afterNodeInsertion(evict);
// 返回null 标识插入新节点, HashSet判断元素在集合是否存在的依据
return null;
}
}
扩容与转树部分见下文
tableSizeFor详解
数组最小容量为1,最大为1073741824,数组长度一定为2的幂
// 最大容量 1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
// 最小容量为1
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
// 给定一个int类型数据,返回这个数据的二进制串中从最左边算起连续的“0”的总数量
// int类型的数据长度为32, 高位不足的地方会以“0”填充
@HotSpotIntrinsicCandidate
public static int numberOfLeadingZeros(int i) {
// HD, Count leading 0's
if (i <= 0)
return i == 0 ? 32 : 0;
int n = 31;
if (i >= 1 << 16) { n -= 16; i >>>= 16; }
if (i >= 1 << 8) { n -= 8; i >>>= 8; }
if (i >= 1 << 4) { n -= 4; i >>>= 4; }
if (i >= 1 << 2) { n -= 2; i >>>= 2; }
return n - (i >>> 1);
}
备注:由于在key的操作中有 ( n - 1) & hash() 的方法存在(开放地址法),因此为了配合按位运算的特性:当对应位置的数据都为1时,运算结果也为1,在当 n 为2的幂次方的时候 (n - 1) 可以保证二进制位都是 1,从而在执行散列 hash 的时候可以达到充分散列,减少 hash 碰撞的效果。
HashMap的扩容
/**
* 扩容相关逻辑
*/
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 链表转红黑树的逻辑
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果当前数组长度小于 64 则执行扩容而不是转红黑树
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
// 扩容逻辑
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 旧的数组容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 旧的扩容阈值
int oldThr = threshold;
int newCap, newThr = 0;
// 旧的数组容量 > 0 已经存在元素
if (oldCap > 0) {
// 旧数组元素个数大于等于限定的最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
// 直接返回旧数组, threshold 设置Integer最大值
// 后面还是可以继续往HashMap中添加数据
// 但是每次进行扩容的时候都会直接返回旧数组,不会再进行扩容操作
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果数组元素个数在正常范围内,新数组容量扩容为之前的2倍
// 新数组的容量小于最大容量 并且 旧数组容量大于等于默认初始化容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新的扩容阈值变为之前的2倍
newThr = oldThr << 1;
}
else if (oldThr > 0)
// new HashMap的时候传了构造参数
// 旧的数组没有值但是旧扩容阈值有值,设置新数组的容量为该阀值
newCap = oldThr;
else {
// 针对new HashMap<>();
// cap 是默认容量 16
newCap = DEFAULT_INITIAL_CAPACITY;
// thr 是默认容量与负载因子计算 16 * 0.75
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 新的扩容阈值为0
// 1:新数组的容量大于等于HashMap最大容量了
// 2:new HashMap的时候传了构造参数
if (newThr == 0) {
// 计算ft 新数组容量 * 负载因子
float ft = (float)newCap * loadFactor;
// 扩容阈值计算 如果小于最大容量则为ft 否则设置int最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 赋值到阈值字段
threshold = newThr;
// 实现扩容逻辑
@SuppressWarnings({"rawtypes","unchecked"})
// 新数组(也可能是第一次put初始化的数组)
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 非第一次put 正在扩容
if (oldTab != null) {
// 迭代数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 数组节点存在元素,需要转移元素到新数组
// 不存在元素的节点 直接跳过即可
if ((e = oldTab[j]) != null) {
// 释放引用(gc)
oldTab[j] = null;
// 判断是否有后继(是否存在hash冲突)
if (e.next == null)
// 存储到新数组 基于hash值和数组长度来进行取模
// 默认情况下
// 元素在新数组的位置是原位置或原位置 + 旧数组长度
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 是一个红黑树节点 (树中元素小于6变为链表在这里)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 链表节点
// 低位首尾节点
Node<K,V> loHead = null, loTail = null;
// 高位首尾节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍历链表执行扩容重分配
do {
next = e.next;
// 元素hash 与 旧数组容量运算
// hash值小于老数组长度
if ((e.hash & oldCap) == 0) {
// 空链表
if (loTail == null)
// 低位链表头指向该节点
loHead = e;
else
// 非空链表 尾插
loTail.next = e;
// 低位链表的尾设置为当前元素
loTail = e;
}
// hash值大于老数组长度
else {
// 空链表
if (hiTail == null)
// 高位链表头指向该节点
hiHead = e;
else
// 非空链表 尾插
hiTail.next = e;
// 高位链表的尾设置为当前元素
hiTail = e;
}
} while ((e = next) != null);
// 低位链表不为空
if (loTail != null) {
// 组成的链表还是放置在原来的位置
loTail.next = null;
newTab[j] = loHead;
}
// 高位链表不为空
if (hiTail != null) {
// 组成的链表放置在原来的位置 + 原数组长度 (偏移原数组长度)
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新数组
return newTab;
}
}
备注:HashMap执行扩容后,元素位置规律为原位置或原位置偏移旧数组长度位置。
ConcurrentHashMap
JDK1.7以及之前的ConcurrentHashMap采用分段数组+链表实现。通过对数组进行分段分割以及分段锁Segment的方式,一把锁控制容器的一部分,通过分散锁的方式降低锁竞争提升并发效率。分段数组由 Segment 数组结构和 HashEntry 数组结构组成,其中 Segment 继承自 ReentrantLock,是一种可重入锁,扮演锁的角色,HashEntry 数组结构用于存储键值对数据。Segment 的个数一旦初始化就不能改变,因此并发粒度完全由 Segment 个数决定 (Segment 默认大小是16,最大65536),同时并发写的线程数量与 Segment 数量强一致。
JDK1.8以及以后采用了Node 数组 + 链表 / 红黑树的数据结构(当冲突链表达到一定长度时,链表会转换成红黑树),采用 Node+CAS+synchronized 来保证并发安全。synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升,并发度与Node数组数量一致。
构造方法与常量
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
// 默认容量
private static final int DEFAULT_CAPACITY = 16;
// 默认并发级别
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 负载因子
private static final float LOAD_FACTOR = 0.75f;
// 初始化状态标记变量
private transient volatile int sizeCtl;
// 默认构造
public ConcurrentHashMap() {
}
// 传递容量
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, LOAD_FACTOR, 1);
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
// 指定容量 负载因子 并发级别
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// 默认容量如果小于并发粒度 则初始化容量为默认并发级别
if (initialCapacity < concurrencyLevel)
initialCapacity = concurrencyLevel;
// size
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
// 初始化
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
......
}
初始化与数据存储
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
// 初始化标记变量
private transient volatile int sizeCtl;
// 通过自旋和 CAS 操作初始化
// sizeCtl = -1 说明正在初始化
// sizeCtl = -N 说明有 N-1 个线程正在进行扩容
// sizeCtl = 0 表示 table 初始化大小
//
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 未初始化 执行初始化
while ((tab = table) == null || tab.length == 0) {
// 另外的线程执行CAS 成功,正在进行初始化
if ((sc = sizeCtl) < 0)
// 让出CPU
Thread.yield();
else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
try {
// 初始化
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
// 赋值
sizeCtl = sc;
}
break;
}
}
return tab;
}
}
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
public V put(K key, V value) {
return putVal(key, value, false);
}
// 放值
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key value 都不可以为空
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// 目标位置元素
Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
// 数组桶为空,初始化通过自旋+CAS
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// CAS 放入,不加锁,成功了就直接 break 跳出
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
// 需要进行扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
// 加锁处理
synchronized (f) {
if (tabAt(tab, i) == f) {
// 链表
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
break;
}
}
}
else if (f instanceof TreeBin) {
// 红黑树
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
备注:
1:ConcurrentHashMap 的 key 和 value 不能为 null
2:ConcurrentHashMap本身的线程安全无法保障复合操作的原子性,如果需要保障复合操作的原子性需要使用提供的复合原子性操作方法
Hashtable
Hashtable 底层基于数组 + 链表的组合,是线程安全的(内部方法基本都使用了 synchronized 修饰),Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。如果构造方法传入了初始化参数,则会初始化成给定值的大小。
SortedMap(TreeMap)
典型实现就是TreeMap,构造方法可以传入自定义的Comparator,否则默认以自然语言排序。默认的元素迭代顺序是升序排列,由小到大,也可以使用TreeMap.descendingKeySet() 实现降序迭代。
其他补充
foreach 为什么不支持集合删除/添加操作,而需要Iterator
在执行循环或迭代时,会首先创建一个迭代实例,这个迭代实例的 expectedModCount 赋值取值为集合的 modCount。在迭代器使⽤ hashNext() / next() 遍历下⼀个元素之前,每次都会先去执行检测 modCount 变量与 expectedModCount 的值是否相等,相等的话执行遍历,否则就会向外抛出 ConcurrentModificationException 异常,快速失败终止遍历。在循环中添加或删除元素的时候,调用的是集合操作中的删除/添加方法,这会导致 modCount 减少/增加,这些方法操作的时候只会改变集合的modCount,并不会修改迭代实例的 expectedModCount,因此会导致迭代实例的 expectedModCount 与集合的 modCount 不相同,抛出异常。而如果使用迭代器的方式执行添加删除操作,会在调用集合的删除/添加方法后,将expectedModCount 重新赋值为modCount,所以在迭代器中增加、删除元素是可以正常运行的。
迭代器 Iterator
Iterator迭代器可以用同一种逻辑来遍历集合。可以把访问逻辑从不同类型的集合类中抽象出来,不需要了解集合内部实现便可以遍历集合元素,统一使用 Iterator 提供的接口去遍历。它的特点是更加安全,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
备注:Map及其子类没有直接实现Interable、Iterator,所以是无法直接使用迭代器迭代Map,但是Map如果先获取Collection视图,就可以间接实现Iterable接口和生成Iterator。
例如:Set<K> keySet(); Collection<V> values(); Set<Map.Entry<K, V>> entrySet();
数组
数组(array)是一组具有相同类型的有序数据的集合,是有序的元素序列。
数组的初始化方式
静态初始化
只指定初始值,不指定长度,JVM自动决定数组长度
String[] arr1 = {"i", "am", "ironman"};
String[] arr2 = new String[]{"i", "am", "ironman"};
动态初始化
初始化指定长度,JVM基于数组的类型自动为每个元素分配初始值
String[] arr3 = new String[3];
(此种情况栈区变量执行的堆内存中,各元素都是null,无地址指向)
数组与其他JAVA集合的对比
数组首先也是一种集合,但是数组是固定长度的,Java集合是可变长度的
数组可以存放基本类型,也可以存放引用类型,但是Java集合只可以存储引用类型
数组存储的元素必须为同一数据类型,Java集合存储的对象可以是不同的数据类型
Arrays.sort()算法说明
length < 47 :插入排序
length < 286 :快排
length >= 286 :归并排序