集合源码详解
集合API源码分析
程序=数据结构+算法
集合概述
集合是存储数据的容器
ArrayList详解
类图结构
首先我们来看下ArrayList的类图结构
源码分析
声明的属性
属性 | 作用 |
---|---|
DEFAULT_CAPACITY | 默认的数组容量 |
EMPTY_ELEMENTDATA | 用于共享的空实例数组 |
DEFAULTCAPACITY_EMPTY_ELEMENTDATA | 也是一个空的实例数组 |
elementData | 这个是ArrayList中真实存储数据的容器 |
size | 记录集合中的元素的个数 |
构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
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);
}
}
public ArrayList(Collection<? extends E> c) {
// 将传递的集合转换为数组 然后赋值给了 elementData
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
添加方法
方法名 | 描述 |
---|---|
public boolean add(E e) | 将指定的元素追加到此列表的末尾。 |
public void add(int index, E element) | 在此列表中的指定位置插入指定的元素。 |
public boolean addAll(Collection<?> c ) | 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。 |
public boolean addAll(i nt index,Collection<? extends E> c) | 将指定集合中的所有元素插入到此列表中,从指定的位置开始。 |
add(E e)
public boolean add(E e) {
// 校验内部容量,判断是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;//size++位置上添加数据
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// calculateCapacity计算容量
// ensureExplicitCapacity 继续校验
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
calculateCapacity方法
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 判断集合存数据的数组是否等于空容量的数组,通过无参构造器创建的对象,插入第一个元素
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 通过最小容量[第一次就是1]和默认容量[10] 求出较大值 (用于第一次扩容)
return Math.max(DEFAULT_CAPACITY, minCapacity);//返回10
}
return minCapacity;
}
ensureExplicitCapacity方法
private void ensureExplicitCapacity(int minCapacity) {
//实际修改集合次数++ (在扩容的过程中没用,主要是用于迭代器中)
modCount++;
//判断最小容量 - 数组长度是否大于 0
if (minCapacity - elementData.length > 0)
//将第一次计算出来的容量传递给 核心扩容方法
grow(minCapacity);
}
private void grow(int minCapacity) {
//记录数组的实际长度,此时由于没有存储元素,长度为0
int oldCapacity = elementData.length;
//核心扩容算法 原容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//判断新容量 - 最小容量 是否小于 0, 如果是第一次调用add方法必然小于
if (newCapacity - minCapacity < 0)
//还是将最小容量赋值给新容量
newCapacity = minCapacity;
//判断新容量-最大数组大小 是否>0,如果条件满足就计算出一个超大容量
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity);
// 调用数组工具类方法,创建一个新数组,将新数组的地址赋值给
elementData elementData = Arrays.copyOf(elementData, newCapacity);
}
add(int index, E element)
public void add(int index, E element) {
// 检查index的合法性
rangeCheckForAdd(index);
// 校验内部容量
ensureCapacityInternal(size + 1); // Increments modCount!!
// index = 3
// [1,2,3,4,5] --> [1,2,3, ,4,5]
// 将 elementData index=3后的元素复制到 elementData 的 index+1=4后的位置
// 也就是空出来了 index 的位置
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
addAll(Collection<?> c )
public boolean addAll(Collection<? extends E> c) {
//把集合的元素转存到Object类型的数组中
Object[] a = c.toArray();
//记录数组的长度
int numNew = a.length;
//调用方法检验是否要扩容,且让增量++
ensureCapacityInternal(size + numNew); // Increments modCount
//调用方法将a数组的元素拷贝到elementData数组中
System.arraycopy(a, 0, elementData, size, numNew);
//集合的长度+=a数组的长度
size += numNew;
//只要a数组的长度不等于0,即说明添加成功
return numNew != 0;
}
addAll(i nt index,Collection<? extends E> c)
public boolean addAll(int index, Collection<? extends E> c) {
// 校验index是否合法
rangeCheckForAdd(index);
// 传递进来的集合转换为数组
Object[] a = c.toArray();
// 记录数组的长度
int numNew = a.length;
// 校验是否要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
// 计算 数组要插入到 集合的index 下标
int numMoved = size - index;
if (numMoved > 0)
// 调整 elementData 中的数据的位置 给新插入的数据流出空间
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
// 将数据中的数据插入到新的数组对应的位置中即可
System.arraycopy(a, 0, elementData, index, numNew);
// 集合容量增加
size += numNew;
return numNew != 0;
}
Vector
类图结构
和ArrayList的区别
本质上也是通过数组的形式来实现的,只是在每个实现的方法中都添加了一个synchronized关键字,所以Vector是数据安全的。
HashSet
类图结构
HashSet的本质
HashSet的本质其实就是HashMap,这个我们可以通过HashSet的构造方法可以看到
所以我们要搞清楚HashSet本质上搞清楚了HashMap就可以了
TreeSet
类图结构
TreeSet的本质
和HashSet一样,TreeSet的功能也不是自己来实现的,而是借助TreeMap来实现的,在TreeSet的构造方法中我们可以看到
TreeMap
类图结构
源码分析
成员变量
// 比较器
private final Comparator<? super K> comparator;
// 根节点 Entry 相当于之前的 RBNode 类型
private transient Entry<K,V> root;
/**
* The number of entries in the tree
*/
private transient int size = 0;
/**
* The number of structural modifications to the tree.
*/
private transient int modCount = 0;
Entry的具体声明
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 = BLACK;
}
put方法
public V put(K key, V value) {
// 1.查找插入的位置
Entry<K,V> t = root;
// 第一次插入 插入的节点设置为 root 节点
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1; // 记录集合中的元素个数
modCount++; // 记录操作的次数
return null;
}
int cmp; // 比较的值
Entry<K,V> parent; // 记录插入节点的父节点
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) { // 比较器不为空
// 遍历找到插入的位置
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
// 插入的值在容器中有相同的值 直接覆盖
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
// 比较器为空就创建一个比较器
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
// 找到插入的位置
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
// 需要插入的 k v 对封装为 Entry对象
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
// 变色和调整操作
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
fixAfterInsertion(e)方法:
private void fixAfterInsertion(Entry<K,V> x) {
// 插入节点 默认就是红色节点
x.color = RED;
// 只有 插入节点的 父节点是 黑色节点才需要 调整平衡
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
// 插入节点的 父亲节点是 爷爷节点的左节点
// 获取叔叔节点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
// 存在叔叔节点
// 父亲和叔叔节点 变为黑色
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
// 爷爷节点变为红色
setColor(parentOf(parentOf(x)), RED);
// 将插入节点调整为 爷爷节点 然后递归处理
x = parentOf(parentOf(x));
} else {
// 没有叔叔节点
if (x == rightOf(parentOf(x))) {
// 插入节点是父节点的右子节点 需要先根据父亲节点左旋一次
// 6 6
// / /
// 4 ==> 5
// \ /
// 5 4
x = parentOf(x);
rotateLeft(x);
}
// 然后父亲节点设置为 黑色 爷爷节点设置为红色
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
// 右旋一次即可
rotateRight(parentOf(parentOf(x)));
}
} else {
// 和上面的情况刚好相反
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
// 将 root 节点设置为 黑色节点
root.color = BLACK;
}
HashMap
设计原理
HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的 key、value 都可以为 null,此外,HashMap 中的映射不是有序的。
jdk1.8 之前 HashMap 由 数组 + 链表 组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的 hashCode 方法计算的哈希值经哈希函数算出来的地址被别的元素占用)而存在的(“拉链法”解决冲突)。jdk1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8 )并且当前数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储。
补充:将链表转换成红黑树前会判断,即便阈值大于 8,但是数组长度小于 64,此时并不会将链表变为红黑树,而是选择逬行数组扩容。
这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要逬行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对要快些。所以结上所述为了提高性能和减少搜索时间,底层阈值大于8并且数组长度大于64时,链表才转换为红黑树,具体可以参考 treeifyBin() 方法。
当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于 8 并且数组长度大于 64 时,链表转换为红黑树时,效率也变的更高效。
HashMap 特点:
- 存储无序的。
- 键和值位置都可以是 null,但是键位置只能存在一个 null。
- 键位置是唯一的,是底层的数据结构控制的。
- jdk1.8 前数据结构是链表+数组,jdk1.8 之后是链表+数组+红黑树。
- 阈值(边界值)> 8 并且数组长度大于 64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。
7.1.1 JDK1.7
7.1.2 JDK1.8
7.1.3 扩容
7.2 源码分析
7.2.1 成员变量
serialVersionUID
序列化版本号
DEFAULT_INITIAL_CAPACITY
集合的初始化容量(必须是 2 的 n 次幂)
// 默认的初始容量是16 1 << 4 相当于 1*2的4次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
DEFAULT_LOAD_FACTOR
默认的负载因子(默认值 0.75)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
MAXIMUM_CAPACITY
集合最大容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 2的30次幂
TREEIFY_THRESHOLD
当链表的值超过8则会转为红黑树(jdk1.8新增)
// 当桶(bucket)上的结点数大于这个值时会转为红黑树
static final int TREEIFY_THRESHOLD = 8;
UNTREEIFY_THRESHOLD
当链表的值小于 6 则会从红黑树转回链表
// 当桶(bucket)上的结点数小于这个值,树转为链表
static final int UNTREEIFY_THRESHOLD = 6;
MIN_TREEIFY_CAPACITY
当 Map 里面的数量超过这个值时,表中的桶才能进行树形化,否则桶内元素太多时会扩容,而不是树形化为了避免进行扩容、树形化选择的冲突,这个值不能小于4*TREEIFY_THRESHOLD(8)
// 桶中结构转化为红黑树对应的数组长度最小的值
static final int MIN_TREEIFY_CAPACITY = 64;
table
table 用来初始化(必须是二的n次幂)(重点)
// 存储元素的数组
transient Node<K,V>[] table;
在 jdk1.8 中我们了解到 HashMap 是由数组加链表加红黑树来组成的结构,其中 table 就是 HashMap 中的数组,jdk8 之前数组类型是 Entry<K,V> 类型。从 jdk1.8 之后是 Node<K,V> 类型。只是换了个名字,都实现了一样的接口:Map.Entry<K,V>。负责存储键值对数据的。
entrySet
用来存放缓存
// 存放具体元素的集合
transient Set<Map.Entry<K,V>> entrySet;
size
HashMap 中存放元素的个数(重点)
// 存放元素的个数,注意这个不等于数组的长度
transient int size;
size 为 HashMap 中 K-V 的实时数量,不是数组 table 的长度。
modCount
用来记录 HashMap 的修改次数
// 每次扩容和更改 map 结构的计数器
transient int modCount;
threshold
用来调整大小下一个容量的值计算方式为(容量*负载因子)
// 临界值 当实际大小(容量*负载因子)超过临界值时,会进行扩容
int threshold;
loadFactor
哈希表的负载因子(重点)
// 负载因子
final float loadFactor;
构造方法
HashMap()
构造一个空的HashMap,默认初始容量(16)和默认负载因子(0.75)
public HashMap() {
// 将默认的负载因子0.75赋值给loadFactor,并没有创建数组
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
HashMap(int initialCapacity)
构造一个具有指定的初始容量和默认负载因子(0.75)HashMap 。
// 指定“容量大小”的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
HashMap(int initialCapacity, float loadFactor)
构造一个具有指定的初始容量和负载因子的 HashMap。
/*
指定“容量大小”和“负载因子”的构造函数
initialCapacity:指定的容量
loadFactor:指定的负载因子
*/
public HashMap(int initialCapacity, float loadFactor) {
// 判断初始化容量initialCapacity是否小于0
if (initialCapacity < 0)
// 如果小于0,则抛出非法的参数异常IllegalArgumentException
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
// 判断初始化容量initialCapacity是否大于集合的最大容量MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
// 如果超过MAXIMUM_CAPACITY,会将MAXIMUM_CAPACITY赋值给initialCapacity
initialCapacity = MAXIMUM_CAPACITY;
// 判断负载因子loadFactor是否小于等于0或者是否是一个非数值
if (loadFactor <= 0 || Float.isNaN(loadFactor))
// 如果满足上述其中之一,则抛出非法的参数异常IllegalArgumentException
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
// 将指定的负载因子赋值给HashMap成员变量的负载因子loadFactor
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
// 最后调用了tableSizeFor,来看一下方法实现:
/*
返回比指定初始化容量大的最小的2的n次幂
*/
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(Map<? extends K, ? extends V> m)
包含另一个 “Map” 的构造函数
// 构造一个映射关系与指定 Map 相同的新 HashMap。
public HashMap(Map<? extends K, ? extends V> m) {
// 负载因子loadFactor变为默认的负载因子0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
最后调用了 putMapEntries(),来看一下方法实现:
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//获取参数集合的长度
int s = m.size();
if (s > 0) {
//判断参数集合的长度是否大于0,说明大于0
if (table == null) { // 判断table是否已经初始化
// 未初始化,s为m的实际元素个数
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
// 计算得到的t大于阈值,则初始化阈值
if (t > threshold)
threshold = tableSizeFor(t);
}
// 已初始化,并且m元素个数大于阈值,进行扩容处理
else if (s > threshold)
resize();
// 将m中的所有元素添加至HashMap中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
注意:
float ft = ((float)s / loadFactor) + 1.0F; 这一行代码中为什么要加 1.0F ?
s/loadFactor 的结果是小数,加 1.0F 与 (int)ft 相当于是对小数做一个向上取整以尽可能的保证更大容量,更大的容量能够减少 resize 的调用次数。所以 + 1.0F 是为了获取更大的容量。
例如:原来集合的元素个数是 6 个,那么 6/0.75 是8,是 2 的n次幂,那么新的数组大小就是 8 了。然后原来数组的数据就会存储到长度是 8 的新的数组中了,这样会导致在存储元素的时候,容量不够,还得继续扩容,那么性能降低了,而如果 +1 呢,数组长度直接变为16了,这样可以减少数组的扩容。
put方法
put方法的实现文字说明:
-
先通过 hash 值计算出 key 映射到哪个桶;
-
如果桶上没有碰撞冲突,则直接插入;
-
如果出现碰撞冲突了,则需要处理冲突:
- a 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;
- b 否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;
-
如果桶中存在重复的键,则为该键替换新值 value;
-
如果 size 大于阈值 threshold,则进行扩容;
put方法的源码说明
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* hash:key 的 hash 值
* key:原始 key
* value:要存放的值
* onlyIfAbsent:如果 true 代表不更改现有的值
* evict:如果为false表示 table 为创建状态
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
/*
1)transient Node<K,V>[] table; 表示存储Map集合中元素的数组。
2)(tab = table) == null 表示将空的table赋值给tab,然后判断tab是否等于null,第一次肯定是null。
3)(n = tab.length) == 0 表示将数组的长度0赋值给n,然后判断n是否等于0,n等于0,由于if判断使用双或,满足一个即可,则执行代码 n = (tab = resize()).length; 进行数组初始化,并将初始化好的数组长度赋值给n。
4)执行完n = (tab = resize()).length,数组tab每个空间都是null。
*/
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/*
1)i = (n - 1) & hash 表示计算数组的索引赋值给i,即确定元素存放在哪个桶中。
2)p = tab[i = (n - 1) & hash]表示获取计算出的位置的数据赋值给结点p。
3) (p = tab[i = (n - 1) & hash]) == null 判断结点位置是否等于null,如果为null,则执行代码:tab[i] = newNode(hash, key, value, null);根据键值对创建新的结点放入该位置的桶中。
小结:如果当前桶没有哈希碰撞冲突,则直接把键值对插入空间位置。
*/
if ((p = tab[i = (n - 1) & hash]) == null)
// 创建一个新的结点存入到桶中
tab[i] = newNode(hash, key, value, null);
else {
// 执行else说明tab[i]不等于null,表示这个位置已经有值了
Node<K,V> e; K k;
/*
比较桶中第一个元素(数组中的结点)的hash值和key是否相等
1)p.hash == hash :p.hash表示原来存在数据的hash值 hash表示后添加数据的hash值 比较两个hash值是否相等。
说明:p表示tab[i],即 newNode(hash, key, value, null)方法返回的Node对象。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
而在Node类中具有成员变量hash用来记录着之前数据的hash值的。
2)(k = p.key) == key :p.key获取原来数据的key赋值给k key 表示后添加数据的key比较两个key的地址值是否相等。
3)key != null && key.equals(k):能够执行到这里说明两个key的地址值不相等,那么先判断后添加的key是否等于null,如果不等于null再调用equals方法判断两个key的内容是否相等。
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
/*
说明:两个元素哈希值相等,并且key的值也相等,将旧的元素整体对象赋值给e,用e来记录
*/
e = p;
// hash值不相等或者key不相等;判断p是否为红黑树结点
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 说明是链表结点
else {
/*
1)如果是链表的话需要遍历到最后结点然后插入
2)采用循环遍历的方式,判断链表中是否有重复的key
*/
for (int binCount = 0; ; ++binCount) {
/*
1)e = p.next 获取p的下一个元素赋值给e。
2)(e = p.next) == null 判断p.next是否等于null,等于null,说明p没有下一个元素,那么此时到达了链表的尾部,还没有找到重复的key,则说明HashMap没有包含该键,将该键值对插入链表中。
*/
if ((e = p.next) == null) {
/*
1)创建一个新的结点插入到尾部
p.next = newNode(hash, key, value, null);
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
注意第四个参数next是null,因为当前元素插入到链表末尾了,那么下一个结点肯定是null。
2)这种添加方式也满足链表数据结构的特点,每次向后添加新的元素。
*/
p.next = newNode(hash, key, value, null);
/*
1)结点添加完成之后判断此时结点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树。
2)int binCount = 0 :表示for循环的初始化值。从0开始计数。记录着遍历结点的个数。值是0表示第一个结点,1表示第二个结点。。。。7表示第八个结点,加上数组中的的一个元素,元素个数是9。
TREEIFY_THRESHOLD - 1 --》8 - 1 ---》7
如果binCount的值是7(加上数组中的的一个元素,元素个数是9)
TREEIFY_THRESHOLD - 1也是7,此时转换红黑树。
*/
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 转换为红黑树
treeifyBin(tab, hash);
// 跳出循环
break;
}
/*
执行到这里说明e = p.next 不是null,不是最后一个元素。继续判断链表中结点的key值与插入的元素的key值是否相等。
*/
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
/*
要添加的元素和链表中的存在的元素的key相等了,则跳出for循环。不用再继续比较了
直接执行下面的if语句去替换去 if (e != null)
*/
break;
/*
说明新添加的元素和当前结点不相等,继续查找下一个结点。
用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
*/
p = e;
}
}
/*
表示在桶中找到key值、hash值与插入元素相等的结点
也就是说通过上面的操作找到了重复的键,所以这里就是把该键的值变为新的值,并返回旧值
这里完成了put方法的修改功能
*/
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
// 用新值替换旧值
// e.value 表示旧值 value表示新值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 修改记录次数
++modCount;
// 判断实际大小是否大于threshold阈值,如果超过则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
上面的源码中使用到了hash方法,我们来看下hash方法的源码
static final int hash(Object key) {
int h;
/*
1)如果key等于null:返回的是0.
2)如果key不等于null:首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的
二进制进行按位异或得到最后的hash值
*/
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
从上面可以得知 HashMap 是支持 key 为空的,而 HashTable 是直接用 Key 来获取hashCode 所以 key 为空会抛异常。
(n - 1) & hash 的实现介绍:
- key.hashCode();返回散列值也就是 hashcode,假设随便生成的一个值。
- n 表示数组初始化的长度是 16。
- &(按位与运算):运算规则:相同的二进制数位上,都是 1 的时候,结果为 1,否则为0。
- ^(按位异或运算):运算规则:相同的二进制数位上,数字相同,结果为 0,不同为 1。