Java Container
- Container 容器是什么?
用来组织多个相同类型的数据,进行增删改查操作的集合
- Java中已经有数组了,为什么还需要Container?
数组长度固定、集合长度可变,数组不可以满足我们的自定义要求,如:按照一定顺序存取, 一组数据之间不能相同
1、继承关系
四大接口:Set、List、Queue、Map
Set的重要实现类:HashSet、LinkedHashSet、TreeSet
Queue重要的实现类:PriorityQueue、LinkedList
List重要实现类:ArrayList、LinkedList、Vector
Map重要实现类:HashMap、LinkedHashMap、TreeMap
2、Collection
Collection就是把集合共性的内容不断往上提取,最终形成集合的继承体系顶层接口
提供了一些所有集合都有的操作
如:add、remove、clear、contains、iterator
Collection接口继承自Iterable接口,实现了iterator方法
Collection<E> extends Iterable<E>
2.1、迭代器
Iterable
是一个接口,里面有个抽象方法Iterator<T> iterator();
public interface Iterable<T> {
Iterator<T> iterator();
}
Iterator
也是⼀个接⼝,它只有三个⽅法:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
}
找到ArrayList的
Iterator iterator()`实现:
它是在ArrayList
以内部类的⽅式实现的!并且, 从源码可知:Iterator
实际上就是在遍历集合
遍历集合Collection
的元素都可以使⽤Iterator,⾄于它的具体实现是以内部类的⽅式实现的!
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {}
private class ListItr extends Itr implements ListIterator<E> {}
3、List
List集合的特点:有序(存储顺序和取出顺序⼀致),可重复
public interface List<E> extends Collection<E>{
// 继承的Collction的方法
ListIterator<E> listIterator();
// 多出来的方法
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
}
ListIterator
也是一个接口,继承自Iterator
,多了一些方法
public interface ListIterator<E> extends Iterator<E> {
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
}
3.1、ArrayList
概括:底层数据结构是数组transient Object[] elementData
, 适用于频繁的查找工作,线程不安全 ,能存放null值。ArrayList中有扩容这么⼀个概念, 正因为它扩容,所以相比于数组它能够实现动态增⻓
3.1.1、类和属性
public class ArrayList<E>
extends
AbstractList<E> // 继承抽象List
implements
List<E>, // 实现了List接口
RandomAccess, // 标识接口,实现随机访问
Cloneable, // 可克隆
java.io.Serializable // 可序列化
{
// 初始容量
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;
}
3.1.2、构造⽅法
// 无参构造,初始化一个 无参构造的空数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 接受一个初始化容量的int参数,
public ArrayList(int initialCapacity) {
// 如果初始化容量大于0, 赋值数组为该长度即可
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
// 如果初始化容量等于0, 赋值elementData为 有参构造的空数组
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
// 如果初始化容量小于0 —— 直接报错
} else {
throw new IllegalArgumentException("Illegal Capacity");
}
}
// 接收一个Collection 的容器,将它转化为ArrayList
public ArrayList(Collection<? extends E> c) {
// 首先转化为数组
Object[] a = c.toArray();
// 如果长度不为0
if ((size = a.length) != 0) {
// 如果c就是一个ArrayList,elementData直接指向即可
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
// 否则就复制该数组到elementData
elementData = Arrays.copyOf(a, size, Object[].class);
}
// c的长度为0, 赋值elementData为 有参构造的空数组
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
3.1.2、API
GET:检查角标,返回数组索引元素即可
SET:检查⻆标 、替代元素 、返回旧值
REMOVE:检查⻆标 、删除元素 、计算出需要移动的个数,并移动 、设置为null,让Gc回收
删除元素时不会减少容量,若希望减少容量则调⽤trimToSize()
,让size作为新数组长度
3.1.3、add及其扩容
ADD:确保容量,赋值
public boolean add(E e) {
// 确保容量
ensureCapacityInternal(size + 1); // Increments modCount!!
// 在后面添加
elementData[size++] = e;
return true;
}
扩容机制:
以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10
此后每次当添加后的容量超过数组长度,就会将原来的数组扩容1.5倍,并将elementData指向新数组
如果要扩容的新数组超过了Integer.MAX_VALUE - 8
, 就看一下需要的最小数组有没有超过
如果也超过了,就将新数组扩容到Integer.MAX_VALUE
, 否则就扩容到Integer.MAX_VALUE - 8
// add进来
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算扩容大小
// 如果arrayList是无参构造的, 首先会返回初始化10
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果elementData是无参构造的空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取默认的容量和传入参数的较大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 只有需要的容量大于数组长度才进行扩容
if (minCapacity - elementData.length > 0)
// 调用grow方法进行扩容,调用此方法代表已经开始扩容了
// minCapacity为需要的最小长度,此时已经大于数组长度了
grow(minCapacity);
}
// 要分配的最大数组大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 扩容
// minCapacity为需要的最小长度
private void grow(int minCapacity) {
// oldCapacity为旧容量
// newCapacity为新容量, 是旧的数组的1.5倍
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果需要的最小容量大于了扩容后的新数组,就将最小的容量更新到新数组
// 否则还是原来的新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 新容量大于 MAX_ARRAY_SIZE
// 进入hugeCapacity()方法来比较minCapacity和MAX_ARRAY_SIZE,
// 如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`
// 否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 扩容操作
elementData = Arrays.copyOf(elementData, newCapacity);
}
我们再来通过例子探究一下grow()
方法 :
- 当 add 第 1 个元素时,oldCapacity 为 0,经比较后第一个 if 判断成立,newCapacity = minCapacity(为 10)。但是第二个 if 判断不会成立,即 newCapacity 不比 MAX_ARRAY_SIZE 大,则不会进入
hugeCapacity
方法。数组容量为 10,add 方法中 return true,size 增为 1。 - 当 add 第 11 个元素进入 grow 方法时,newCapacity 为 15,比 minCapacity(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeCapacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。
// 如果newCapacity大于Integer.MAX_VALUE - 8, 将需要的minCapacity进入该方法
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
3.2、LinkedList
概括:基于双向链表的,不保证线程安全,它支持高效的插入和删除操作, 它实现了Deque接口,使得LinkedList类也具有栈和队列的特性 ,能存放null值的数据接口
3.2.1、类和属性
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
// 长度
transient int size = 0;
// 头尾节点
transient Node<E> first;
transient Node<E> last;
// 无参构造
public LinkedList() {}
// 构造2,将继承自Colelction赋值为LinkedList
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
}
3.2.2、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;
}
}
3.2.3、API
ADD:可以发现,LinkedList的中first和last,都没有哨兵节点
//
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
GET:这里除了检查有没有越界之外有个小技巧
如果要get的下标在半部分就从头遍历,否则从尾巴遍历
public E get(int index) {
//检查index范围是否在size之内
checkElementIndex(index);
//调用Node(index)去找到index对应的node然后返回它的值
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
REMOVE:删除元素
// 删除指定元素,不过该方法一次只会删除一个匹配的对象
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
// 删除指定位置的元素
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
3.2.4、栈和队列使用
LinkedList实现了队列的接口Deque
,而Deque
是继承自Queue
的,是一个双端队列,实现Queue接口的主要有:
- LinkedList:双向队列,链表实现
- ArrayDeque:双向队列,循环数组实现(JDK1.6)
- PriorityQueue:优先队列,可以实现最大堆、最小堆
还有一个Vector的子类:Stack,属于线程安全的Stack实现
在实现普通队列上,相比于LinkedList,我们推荐使用ArrayDeque
,因为效率高,而 LinkedList 还会有其他的额外开销
ArrayDeque和LinkedList区别:
- ArrayDeque 是一个可扩容的数组,LinkedList 是链表结构;
- ArrayDeque 里不可以存 null 值,但是 LinkedList 可以;
- ArrayDeque 在操作头尾端的增删操作时更高效,但是 LinkedList 只有在当要移除中间某个元素且已经找到了这个元素后的移除才是 O(1) 的;
- ArrayDeque 在内存使用方面更高效。
Deque接口下的两组方法返回值情况
throw Exception | 返回false或null | |
---|---|---|
添加元素到队尾 | add(E e) | boolean offer(E e) |
取队首元素并删除 | E remove() | E poll() |
取队首元素但不删除 | E element() | E peek() |
3.2.5、JDK1.6
在JDK 1.7,1.6的headerEntry
循环链表被替换成了非循环链表。
-
first / last有更清晰的链头、链尾概念,代码看起来更容易明白。
-
first / last方式能节省new一个headerEntry(哨兵节点)。(实例化headerEntry是为了让后面的方法更加统一,否则会多很多header的空校验)
-
在链头/尾进行插入/删除操作,first /last方式更加快捷。
在中间插入/删除,两者都是一样,先遍历找到index,然后修改链表index处两头的指针。
在两头,对于循环链表来说,由于首尾相连,还是需要处理两头的指针。而非循环链表只需要处理一边first.previous/last.next,所以理论上非循环链表更高效。恰恰在两头(链头/链尾) 操作是最普遍的】
3.3、Vector
Vector是比较老的一个容器类,它和ArrayList相似,只是它比起ArrayList是用于synchronized
关键字线程安全,除此之外,ArrayList在底层数组不够⽤时在原来的基础上扩展0.5倍,Vector是扩展1倍。
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
// ...
}
同步方法
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized E remove(int index) {
modCount++;
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
E oldValue = elementData(index);
int numMoved = elementCount - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--elementCount] = null; // Let gc do its work
return oldValue;
}
3.4、小结
3.4.1、ArrayList和Vector的区别
ArrayList
是List
的主要实现类,底层使用Object[ ]
存储,适用于频繁的查找工作,线程不安全 ;Vector是List
的古老实现类,底层使用Object[ ]
存储,线程安全的。- ArrayList在底层数组不够⽤时在原来的基础上扩展1.5倍,Vector是扩展2倍。
3.4.2、ArrayList和LinkedList的区别
-
是否保证线程安全:
ArrayList
和LinkedList
都是不同步的,也就是不保证线程安全; -
底层数据结构:
Arraylist
底层使用的是Object
数组;LinkedList
底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表( headerEntry ),JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) -
插入和删除是否受元素位置的影响:
①
ArrayList
采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)
方法的时候,ArrayList
会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)
)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。②
LinkedList
采用链表存储,所以对于add(E e)
方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i
插入和删除元素的话((add(int index, E element)
) 时间复杂度近似为o(n)
因为需要先移动到指定位置再插入。 -
是否支持快速随机访问:
LinkedList
不支持高效的随机元素访问,而ArrayList
支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。 -
内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
4、Map
4.1、介绍
4.1.1、散列表
什么是散列表(Hash 表)?
根据键(Key)而直接访问在内存储存位置的[数据结构]。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
散列中重要的两个问题:
-
如何建立hash高效的函数
散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快定位。 一般方法有:
直接定址法、数字分析法、折叠法、除留余数法、随机数法
-
如何解决散列冲突
开放定址法、单独链表法、双散列法、再散列法
4.1.2、Map
Map是Java中用来实现上述hash表的数据结构的一个顶层接口,在它下面有很多实现类
其中最重要的是HashMap
、ConcurrentHashMap
、还有LinkedHashMap
、TreeMap
等
它提供了一些基本功能:put、get、size等,其中内部有个接口作为一个哈希表节点
public interface Map<K,V> {
int size();
boolean isEmpty();
boolean containsKey(Object key);
V put(K key, V value);
V get(Object key);
interface Entry<K,V> {
int hashCode();
boolean equals(Object o);
V setValue(V value);
V getValue();
K getKey();
}
}
Map和Collection有什么区别呢?
- Map集合存储的元素是成双成对出现的,一个Map中的键是唯一的,value可以重复
- Collection集合存储元素是单独出现的,Collection的继承接口Set利用了Map的思想不可以重复,而List是允许重复的
4.1.3、hashCode()、Equals()、==
在java中,所有的对象都是继承于Object类。
Ojbect类中有两个方法equals、hashCode,这两个方法都是用来比较两个对象是否相等的。
-
==
比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。但是对于基本数据类型,==
比较的是指的大小 -
equals
用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object
类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object
类中的方法,而Object
中的equals
方法返回的却是==的判断。那里的 equals是比较两个对象的内存地址,显然我们new了2个对象内存地址肯定不一样
public native int hashCode();
public boolean equals(Object obj) {
// 比较的是内存地址
return (this == obj);
}
如果我们对equals方法进行了重写,一定要对hashCode方法重写
保证equals
用来比较的是两个对象的内容是否相等,内容相同的对象返回相同的hash值
// Integer中的hashcode 和 equals
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
public int hashCode() {
return Integer.hashCode(value);
}
public static int hashCode(int value) {
return value;
}
// String
public int hashCode() {
int h = hash; // hash的缓存
if (h == 0 && value.length > 0) {
char val[] = value;
// 对于自己的char数组,每个都进行这样的操作得出h
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
public boolean equals(Object anObject) {
// 如果内存地址一样那肯定是同一个
if (this == anObject) {
return true;
}
// 内存地址不一样但是是String
// 比较的是char[] 数组中每个字符是否一样
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
// 如果不是String就直接返回false
return false;
}
hashCode()与 equals()的相关规定:
- 如果两个对象相等,则 hashcode 一定也是相同的
- 两个对象相等,对两个 equals 方法返回 true
- 两个对象有相同的 hashcode 值,它们也不一定是相等的
- 综上,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
4.2、HashMap
HashMap是Java中最重要的类之一
4.2.1、HashMap底层数据结构分析
JDK1.8之前:
数组 + 链表, HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash
判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
// JDK 1.8
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// JDK 1.7
// 相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8
Node数组 + 链表 / 红黑树,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
(TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构)
4.2.2、HashMap类和属性
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<K,V>[] table;
// 存放具体元素的集
transient Set<Map.Entry<K,V>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 负载因子
final float loadFactor;
// Node节点
static class Node<K,V> implements Map.Entry<K,V> {
// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较
final int hash;
final K key;
V value;
// 指向下一个节点
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
// 重写hashcode方法, 一个Node的hashcode是key和value的^操作
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {}
// 重写的 equals() 方法
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
// 红黑树节点源码
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red; // 判断颜色
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// ...
}
}
-
loadFactor
加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。
给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
-
threshold = capacity * loadFactor
,当size >= threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。
4.2.3、构造方法
HashMap中一共有4个构造方法
// 1、默认构造函数。
public HashMap() {
// 默认负载因子为0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 2、包含另一个“Map”的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
// 默认负载因子为0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 将map类型的转化为hashmap
putMapEntries(m, false);
}
// 3、指定“容量大小”的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 4、指定“容量大小”和“加载因子”的构造函数
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) // 初始容量不能小于0
throw new IllegalArgumentException("Illegal initial capacity");
if (initialCapacity > MAXIMUM_CAPACITY) // 初始容量不能大于最大值
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor)) // 加载因子限制
throw new IllegalArgumentException("Illegal load factor");
this.loadFactor = loadFactor;
// 构造当前的threshold
this.threshold = tableSizeFor(initialCapacity);
}
// tableSizeFor: 保证了HashMap总是使用 2 的幂作为哈希表的大小。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
HashMap 的长度为什么是 2 的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash
”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash % length == hash & (length - 1)
的前提是 length 是 2 的 n 次方)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。
总而言之,让hashmap的长度称为2的幂次方可以方便计算hash % size从而找到相应的数组索引
4.2.4、put / get / resize / remove
put
HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。
对putVal方法添加元素的分析如下:
- 如果定位到的数组位置没有元素 就直接插入。
- 如果定位到的数组位置有元素就和要插入的key比较,
- 如果key相同就直接覆盖
- 如果key不相同,就判断p是否是一个树节点,如果是就调用将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,
// 桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素
else {
// p : 指向该Node的第一个节点
// e : 要插入的节点(如果没有会创建一个新的)
// k : 指向某个key的指针
Node<K,V> e; K k;
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一个元素赋值给e,用e来记录
e = p;
// hash值不相等,即key不相等并且为红黑树结点
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 为链表结点
else {
// 在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
// 结点数量达到阈值,转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
// 即找到了equal相同的节点、或者初始化了新的节点
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调,hashcode里面为空
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调,hashcode里面为空
afterNodeInsertion(evict);
return null;
}
/**
* JDK1.7 put方法的代码
* 如果定位到的数组位置没有元素 就直接插入。
* 如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较
* 如果key相同就直接覆盖,不同就采用头插法插入元素。
*/
public V put(K key, V value)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) { // 先遍历
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i); // 再插入
return null;
}
resize
进行扩容或者初始化,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 如果以前的table已经初始化了
if (oldCap > 0) {
// 超过最大值就不再扩充了,就只好随你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 没超过最大值,就扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY
&& oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 如果没有初始化就会判断 thr有没有每设置值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 如果没有被设置值就用默认值16, 新的thr就是 16*0.75 = 12
else {
// signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY
&& ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//创建新的node数组, 将新的thr赋值
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 进行扩容操作
if (oldTab != null) {
// 把每个bucket都移动到新的buckets中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((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;
// 原索引
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
get
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 数组元素相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 桶中不止一个节点
if ((e = first.next) != null) {
// 在树中get
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 在链表中get
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
remove
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 如果桶不是空的,并且hash映射的桶也存在
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 如果就是第一个节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 不是第一个节点,并且下一个节点不是null
else if ((e = p.next) != null) {
// 如果是一棵红黑树
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 如果是链表
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 找到了对应节点,并且value值对应上就去删除
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 如果是树节点
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 如果是头节点
else if (node == p)
tab[index] = node.next;
else //不是头节点
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
4.2.5、HashMap1.7 VS 1.8
-
jdk7 Entry数组+单链表 ——jdk8 Node数组+(单链表+红黑树) (👌)
-
jdk7 计算hash运算多——jdk8 计算hash运算少(👌)
-
jdk7 链表头插——jdk8 链表尾插 (👌)
为什么头插?
1、resize()后 transfer()数据时不需要遍历链表到尾部再插入
2、最近put的可能等下就被get,头插遍历到链表头就匹配到了
为什么变成尾插?
1、resize后链表可能倒序
2、并发resize可能产生循环链(死链,Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。)
-
jdk7 先扩容再put——jdk8 先put再扩容 ()
JDK1.7 中:先扩容后插入
当你发现你插入的桶是不是为空,如果不为空说明存在值就发生了hash冲突,那么就必须得扩容,但是如果不发生Hash冲突的话,说明当前桶是空的(后面并没有挂有链表),那就等到下一次发生Hash冲突的时候在进行扩容,但是当如果以后都没有发生hash冲突产生,那么就不会进行扩容了,减少了一次无用扩容,也减少了内存的使用
JDK1.8 中:先插入后扩容
主要是因为对链表转为红黑树进行的优化,因为你插入这个节点的时候有可能是普通链表节点,也有可能是红黑树节点
如果是链表节点,是否达到了 链表转化为红黑树的阈值是8,如果没有那么就还可以继续插入。
如果是红黑树节点,需要看插入红黑树节点在扩容后是否还能满足当前是红黑树的特性,如果还能继续满足即还没有达到扩容的临界条件
-
扩容后数据迁移流程(transfer)不一样(👌)
JDK7:
1、首先新创建 2*length 大小的新数组
2、将原来老数组上的数据挨个拍的迁移到新的数组上
JDK8:
1、首先新创建 2 * length 大小的新数组
2、扩容前的原始位置 + 扩容的大小值,这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式,非常的高效。(https://www.cnblogs.com/liang1101/p/12728936.html)
4.3、其他实现类
4.3.1、Hashtable
Hashtable 和 HashMap 大多数逻辑一样,是一个比较老的hash类,不建议使用
Hashtable 和 HashMap 区别:
线程是否安全:
HashMap
是非线程安全的,HashTable
是线程安全的,因为HashTable
内部的方法基本都经过synchronized
修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap
吧!);效率: 因为线程安全的问题,
HashMap
要比HashTable
效率高一点。另外,HashTable
基本被淘汰,不要在代码中使用它;对 Null key 和 Null value 的支持:
HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出NullPointerException
。初始容量大小和每次扩充容量大小的不同 :
① 创建时如果不指定容量初始值,
Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而
HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的tableSizeFor()
方法保证,下面给出了源代码)底层数据结构: JDK1.8 以后的
HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
4.3.2、LinkedHashMap
继承自HashMap的一个类,其主要特点是:
-
底层是散列表和双向链表
-
允许为null,不同步
-
插⼊的顺序是有序的(底层链表致使有序)
-
装载因⼦和初始容量对LinkedHashMap影响是很⼤的
当我们希望有顺序地去存储key-value时,就需要使用LinkedHashMap, 且默认为插入顺序。
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
// 构造1
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
// 构造2,重载
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
// 构造3 无参构造
public LinkedHashMap() {
super();
accessOrder = false;
}
// 构造4 转化
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
// 构造5
// accessorder :访问顺序
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
}
LinkedHashMap的put⽅法和HashMap是⼀样的,在创建节点的时候,调⽤的是LinkedHashMap重写的⽅法,所不同的是在调用get方法的时候,如果是访问顺序的话, get完之后把链表放到最后面
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder) // LRU cache
afterNodeAccess(e);
return e.value;
}
总结一下:
LinkedHashMap⽐HashMap多了⼀个双向链表的维护,在数据结构⽽⾔它要复杂⼀些,⼤多都由HashMap实现了, 这其中体现了多态的强大作用,⼦类⽤⽗类的⽅法,⼦类重写了⽗类的部分⽅法即可达 到不⼀样的效果! 比如:
LinkedHashMap并没有重写put⽅法,⽽put⽅法内部的 newNode() ⽅法重写了。
LinkedHashMap调⽤⽗类的put⽅法,⾥⾯回调的是重写后的 newNode() ,从⽽达到⽬的
LinkedHashMap可以设置两种遍历顺序:
- 访问顺序(access - ordered)
- 插⼊顺序(insertion - ordered)
该循环双向链表的头部存放的是最久访问的节点或最先插入的节点,尾部为最近访问的或最近插入的节点
LinkedHashMap遍历的是内部维护的双向链表,所以说初始容量对LinkedHashMap遍历是不受影响的
4.3.3、TreeMap
TreeMap 简介
TreeMap 是一个排序的key-value集合,它是通过红黑树实现的。
TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
TreeMap 实现了Cloneable接口,意味着它能被克隆。
TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。
TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fast的。
public class TreeMap<K, V> extends AbstractMap<K, V> implements NavigableMap<K, V>, Cloneable, Serializable {
5、Set
无序、不重复
5.1、Set集合的实现类
主要有三个比较重要的子类:
HashSet:采用 Hashmap 的 key 来储存元素,主要特点是无序的,基本操作都是 O(1) 的时间复杂度,很快
LinkedHashSet:这个是一个 HashSet + LinkedList
的结构,特点就是既拥有了 O(1) 的时间复杂度,又能够保留插入的顺序
TreeSet:采用红黑树结构,特点是可以有序,可以用自然排序或者自定义比较器来排序;缺点就是查询速度没有HashSet
快
每个 Set 的底层实现其实就是对应的 Map:
数值放在 map 中的 key 上,value 上放了个 PRESENT,是一个静态的 Object,相当于 place holder,每个 key 都指向这个 object
private static final Object PRESENT = new Object();
5.2、Comparable 、Comparator
有点类似于Iterable
和Iterator
Comparable
接口实际上是出自java.lang
包 它有一个compareTo(Object obj)
方法用来排序Comparator
接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)
方法用来排序
// java.lang
public interface Comparable<T> {
int compareTo(T var1);
}
// java.util
public interface Comparator<T> {
int compare(T var1, T var2);
}
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()
方法或compare()
方法,当我们需要对某一个集合实现两种排序方式,比如一个 song 对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo()
方法和使用自制的Comparator
方法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort()
.
class Student implements Comparable<Student>{
int age;
String name;
// 构造函数、getter/setter, toString()
@Override
public int compareTo(Student s) {
// 引用类型(可以排序的类型)可以直接调用CompareTo方法
// 基本类型 使用减
return this.age.compareTo(s.age); // 表示排序会按照升序
}
}
List<Integer> list = new ArrayList<>();
list.add(new Student(22, "Jennifer"));
list.add(new Student(11, "Alex"));
list.add(new Student(20, "Sum"));
list.add(new Student(19, "James"));
list.add(new Student(50, "Cook"));
list.add(new Student(60, "Steve"));
// 如果想要调用Collections中的sort方法,
// 如果不实现Comparable接口,则必须传入Comparator对象, 可以自定义排序规则
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
return b.age - a.age; // 表示会按照降序
}
});
Comparable:
Comparable是内部比较器,一个类如果想要使用 Collections.sort(list) 方法进行排序,则需要实现该接口
Comparable是需要比较的对象来实现接口。这样对象调用实现的方法来比较。对对象的耦合度高(需要改变对象的内部结构,破坏性大)。
Comparator:
Comparator: 外部比较器用于对那些没有实现Comparable接口或者对已经实现的Comparable中的排序规则不满意进行排序.无需改变类的结构,更加灵活。(策略模式)
Comparator相当于一通用的比较工具类接口。需要定制一个比较类去实现它,重写里面的compare方法,方法的参数即是需要比较的对象。对象不用做任何改变。解耦
对于TreeMap或TreeSet等排序集合,其范性类型必须实现Comparable接口或者穿入自定义的Comarartor自定义排序规则
6、Concurrent
并发安全容器
6.1、Java并发容器
一般来说我们可以将Java并发容器分为三个大类
-
遗留的线程安全类:Hashtable、Vector
-
修饰的安全集合类:Collections.synchronizedList()、Collections.synchronizedMap()
-
JUC安全类:
- Blocking类(BlockingQueue)、
- CopyOnWrite类(CopyOnWriteArrayList)、
- Concurrent类(如ConcurrentHashMap)
我们重点说JUC安全类,一般来说有三种关键词
- Blocking大部分实现基于锁,并且提供阻塞的方法
- CopyOnWrite之类的容器修改开销比较重
- Concurrent类型的容器:
- 内部采用CAS + Synchronized优化,一般提供较高的吞吐量
- 有弱一致性
什么是弱一致性?
遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍
历,这时内容是旧的
求大小弱一致性,size 操作未必是 100% 准确
读取弱一致性,读到的不一定是最新的值
6.2、fast-fail机制
fast-fail
快速失败机制是Java集合中一种错误检测机制,在使用迭代器对集合进行遍历的时候(注:增强 for 循环也是借助迭代器进行遍历),我们在多线程下操作非JUC包下的容器可能就会触发 fail-fast 机制,导致抛出 ConcurrentModificationException
异常。 另外,在单线程下,如果在遍历过程中对集合对象的内容进行了修改的话也会触发 fail-fast 机制。
原因是:每当迭代器使用 hashNext()
/next()
遍历下一个元素之前,都会检测 modCount
变量是否为 expectedModCount
值,是的话就返回遍历;否则抛出异常,终止遍历。如果我们在集合被遍历期间对其进行修改的话,就会改变 modCount
的值,进而导致 modCount != expectedModCount
,进而抛出 ConcurrentModificationException
异常。
fast-safe
安全失败机制基于容器的一个克隆。因此,对容器内容的修改不影响遍历。juc包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。常见的的使用fail-safe
方式遍历的容器有ConcerrentHashMap
和CopyOnWriteArrayList
等。
原理:采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception
。
缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。弱一致性的体现
6.3、ConcurrentHashMap
见另外文件
6.4、CopyOnWriteArrayList
6.4.1、底层原理
CopyOnWriteArraySet
和CopyOnWriteArrayList
原理一样
private final CopyOnWriteArrayList<E> al;
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
CopyOnWriteArrayList
是ArrayList
一个线程安全版本,主题思想是写入时拷贝
有多少线程安全的ArrayList?
- Vector:JDK早期线程安全的List,他在每个方法上加了
syn关键字
保证安全- Collections.SynchronizedList修饰的List,也是几乎在每个方法上加了
syn关键字
,他锁的是一个Object
对象final Object mutex
,该对象在父类的构造方法中被传入this- CopyOnWriteArrayList
Vector和SynchronizedList修饰的List有什么问题?- 加锁粒度大、并发性低、性能差
什么是写入时拷贝: 增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其它线程的并发读,读写分离,适用于读多写少的场景
COW维基百科: 是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
6.4.2、API
这里是Java 8,在Java 11后采用了syn关键字加锁
add
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 拿到原来的数组
Object[] elements = getArray();
int len = elements.length; // 原来数组长度
// 复制新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e; // 将要add的元素加入新数组
setArray(newElements); // 设置新数组替代旧数组
return true;
} finally {
lock.unlock(); //解锁
}
}
get
public E get(int index) { // get操作不用加锁
return get(getArray(), index);
}
// 和add操作流程类似
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
6.5、其他并发容器
基于Java 8
6.5.1、BlockingQueue阻塞队列
LinkedBlockingQueue
通过链表实现了BlockingQueue
接口,初此之外还有数组的实现ArrayBlockingQueue
,这里主要看链表的实现
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
transient Node<E> head; // 头
private transient Node<E> last; // 尾
// 两把锁
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
// Node类
static class Node<E> {
E item;
// next指向自己代表出队
Node<E> next;
Node(E x) { item = x; }
}
// 构造函数会初始化一个dummy节点,让last和head都指向它
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
//入队
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
// 出队
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
}
加锁机制
LinkedBlockingQueue
最妙的地方在于它的加锁机制——(两把锁和dummy节点)
- 用一把锁,同一时刻最多只允许有一个线程(生产者或消费者)执行
- 用两把锁,同一时刻可以有两个线程(一个生产者和一个消费者)执行
- 生产者和生产者、消费者和消费者依然是串行执行
线程安全分析
- 当节点总数大于2时(包括
dummy节点
) ,putLock
保证的是last节点的线程安全,takeLock
保证的是head
节点的线程安全。两把锁保证了入队和出队没有竞争 - 当节点总数等于2时(即一个dummy节点, 一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
- 当节点总数等于1时(就一个dummy节点)这时take线程会被
notEmpty
条件阻塞,有竞争,会阻塞
// put
public void put(E e) throws InterruptedException {
// 不允许null
if (e == null) throw new NullPointerException();
int c = -1; // 检查空位
Node<E> node = new Node<E>(e); // 包装e
final ReentrantLock putLock = this.putLock; //
final AtomicInteger count = this.count; // 维护元素个数
putLock.lockInterruptibly(); //上锁
try {
while (count.get() == capacity) { // 看队列满不满
notFull.await(); // 进入等待
}
enqueue(node); //入队
c = count.getAndIncrement();
if (c + 1 < capacity) // 除了自己put外,队列还没满就叫醒其他put
notFull.signal();
} finally {
putLock.unlock(); //解锁
}
// 如果队列中有一个元素就叫醒take线程
if (c == 0)
signalNotEmpty(); // 内部是signal()
}
// take
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 如果队列中有一个空位,叫醒put线程
if (c == capacity)
signalNotFull();
return x;
}
LinkedBlockingQueue
和ArrayBlockingQueue
的区别:
- Linked底层链表,支持有界队列、Array底层数组,强制有界
- Linked懒加载、Array需要提前new数组
- Linked两把锁、Array一把锁
6.5.2、ConcurrentLinkedQueue并发队列
ConcurrentBlockingQueue
和LinkedBlockingQueue
很像:
- 两把锁
- dummy节点
- 锁是cas实现
public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
implements Queue<E>, java.io.Serializable {
}
7、核心源码解读
主要针对ArrayList和HashMap
7.1、ArrayList
ackage java.util;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
/**
* 默认初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组(用于空实例)。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
//用于默认大小空实例的共享空数组实例。
//我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 保存ArrayList数据的数组
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* ArrayList 所包含的元素个数
*/
private int size;
/**
* 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//如果传入的参数大于0,创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果传入的参数等于0,创建空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
//其他情况,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
*默认无参构造函数
*DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
*/
public ArrayList(Collection<? extends E> c) {
//将指定集合转换为数组
elementData = c.toArray();
//如果elementData数组的长度不为0
if ((size = elementData.length) != 0) {
// 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断)
if (elementData.getClass() != Object[].class)
//将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 其他情况,用空数组代替
this.elementData = EMPTY_ELEMENTDATA;
}
}
/**
* 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
//下面是ArrayList的扩容机制
//ArrayList的扩容机制提高了性能,如果每次只扩充一个,
//那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。
/**
* 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
//如果是true,minExpand的值为0,如果是false,minExpand的值为10
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
//如果最小容量大于已有的最大容量
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
//得到最小扩容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取“默认的容量”和“传入参数”两者之间的最大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
/**
* 要分配的最大数组大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//再检查新容量是否超出了ArrayList所定义的最大容量,
//若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
//如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
//比较minCapacity和 MAX_ARRAY_SIZE
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
/**
*返回此列表中的元素数。
*/
public int size() {
return size;
}
/**
* 如果此列表不包含元素,则返回 true 。
*/
public boolean isEmpty() {
//注意=和==的区别
return size == 0;
}
/**
* 如果此列表包含指定的元素,则返回true 。
*/
public boolean contains(Object o) {
//indexOf()方法:返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1
return indexOf(o) >= 0;
}
/**
*返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
//equals()方法比较
if (o.equals(elementData[i]))
return i;
}
return -1;
}
/**
* 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。.
*/
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
/**
* 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。)
*/
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
//Arrays.copyOf功能是实现数组的复制,返回复制后的数组。参数是被复制的数组和复制的长度
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// 这不应该发生,因为我们是可以克隆的
throw new InternalError(e);
}
}
/**
*以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。
*返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。
*因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。
*/
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
/**
* 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素);
*返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。
*否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。
*如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。
*(这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。)
*/
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
// 新建一个运行时类型的数组,但是ArrayList数组的内容
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
//调用System提供的arraycopy()方法实现数组之间的复制
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
// Positional Access Operations
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
/**
* 返回此列表中指定位置的元素。
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
/**
* 用指定的元素替换此列表中指定位置的元素。
*/
public E set(int index, E element) {
//对index进行界限检查
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
//返回原来在这个位置的元素
return oldValue;
}
/**
* 将指定的元素追加到此列表的末尾。
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//这里看到ArrayList添加元素的实质就相当于为数组赋值
elementData[size++] = e;
return true;
}
/**
* 在此列表中的指定位置插入指定的元素。
*先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
*再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//arraycopy()这个实现数组之间复制的方法一定要看一下,下面就用到了arraycopy()方法实现数组自己复制自己
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
/**
* 删除该列表中指定位置的元素。 将任何后续元素移动到左侧(从其索引中减去一个元素)。
*/
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
//从列表中删除的元素
return oldValue;
}
/**
* 从列表中删除指定元素的第一个出现(如果存在)。 如果列表不包含该元素,则它不会更改。
*返回true,如果此列表包含指定的元素
*/
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
/**
* 从列表中删除所有元素。
*/
public void clear() {
modCount++;
// 把数组中所有的元素的值设为null
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
/**
* 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。
*/
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
/**
* 将指定集合中的所有元素插入到此列表中,从指定的位置开始。
*/
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
/**
* 从此列表中删除所有索引为fromIndex (含)和toIndex之间的元素。
*将任何后续元素移动到左侧(减少其索引)。
*/
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
// clear to let GC do its work
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
size = newSize;
}
/**
* 检查给定的索引是否在范围内。
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* add和addAll使用的rangeCheck的一个版本
*/
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 返回IndexOutOfBoundsException细节信息
*/
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
/**
* 从此列表中删除指定集合中包含的所有元素。
*/
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
//如果此列表被修改则返回true
return batchRemove(c, false);
}
/**
* 仅保留此列表中包含在指定集合中的元素。
*换句话说,从此列表中删除其中不包含在指定集合中的所有元素。
*/
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
/**
* 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。
*指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。
*返回的列表迭代器是fail-fast 。
*/
public ListIterator<E> listIterator(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
/**
*返回列表中的列表迭代器(按适当的顺序)。
*返回的列表迭代器是fail-fast 。
*/
public ListIterator<E> listIterator() {
return new ListItr(0);
}
/**
*以正确的顺序返回该列表中的元素的迭代器。
*返回的迭代器是fail-fast 。
*/
public Iterator<E> iterator() {
return new Itr();
}
}
7.2、HashMap
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<K,V>[] table;
// 存放具体元素的集
transient Set<Map.Entry<K,V>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 负载因子
final float loadFactor;
// Node节点
static class Node<K,V> implements Map.Entry<K,V> {
// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较
final int hash;
final K key;
V value;
// 指向下一个节点
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
// 重写hashcode方法
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {}
// 重写 equals() 方法
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
// 红黑树节点源码
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red; // 判断颜色
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// ...
}
// 1、默认构造函数。
public HashMap() {
// 默认负载因子为0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 2、包含另一个“Map”的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
// 默认负载因子为0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 将map类型的转化为hashmap
putMapEntries(m, false);
}
// 3、指定“容量大小”的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 4、指定“容量大小”和“加载因子”的构造函数
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) // 初始容量不能小于0
throw new IllegalArgumentException("Illegal initial capacity");
if (initialCapacity > MAXIMUM_CAPACITY) // 初始容量不能大于最大值
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor)) // 加载因子限制
throw new IllegalArgumentException("Illegal load factor");
this.loadFactor = loadFactor;
// 构造当前的threshold
this.threshold = tableSizeFor(initialCapacity);
}
// tableSizeFor: 保证了HashMap总是使用 2 的幂作为哈希表的大小。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
// put操作
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,
// 桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素
else {
Node<K,V> e; K k;
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一个元素赋值给e,用e来记录
e = p;
// hash值不相等,即key不相等;为红黑树结点
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 为链表结点
else {
// 在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
// 结点数量达到阈值,转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
// 扩容或初始化
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 如果以前的table已经初始化了
if (oldCap > 0) {
// 超过最大值就不再扩充了,就只好随你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 没超过最大值,就扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY
&& oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 如果没有初始化就会判断 thr有没有每设置值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 如果没有被设置值就用默认值16, 新的thr就是 16*0.75 = 12
else {
// signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY
&& ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//创建新的node数组, 将新的thr赋值
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 进行扩容操作
if (oldTab != null) {
// 把每个bucket都移动到新的buckets中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((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;
// 原索引
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
// get操作
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 数组元素相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 桶中不止一个节点
if ((e = first.next) != null) {
// 在树中get
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 在链表中get
do {
if (e.hash == hash &&
(
(k = e.key) == key ||
(key != null && key.equals(k))
)
)
return e;
} while ((e = e.next) != null);
}
}
return null;
}
}