往
期
回
顾
1.医生:经常熬夜加班还不猝死的几点建议,尤其程序员
2.Java老铁第三期资料分享说明
3.计算机组成原理、操作系统、计算机网络讲解视频
在Java8中对HashMap做了较大的改动,其中一个重要的改变就是引入了红黑树。
红黑树
红黑树本质种自平衡二叉查找树,是在计算机科学中用到的一种数据结构。但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。
红黑树5大特性
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶节点(NIL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
大概小结就是一句话:有红必有黑,红红不相连。
在Java8中HashMap底层存储数据时采用的是数组+链表+红黑树的数据结构,大致如下图所示:
注:叶子节点下面挂着两个虚节点(NIL) HashMap实现和继承的类与接口:下面我们通过源码具体看看:
默认的常量值:
//初始容量 必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
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;
其他的一些属性:
//第一次初始化时使用和需要扩容时
transient Node<K,V>[] table;
//map集合数据转换为set,主要用于hashMap迭代时
transient Set<Map.Entry<K,V>> entrySet;
//集合的大小
transient int size;
//hashMap修改的次数
transient int modCount;
//临界值 集合的大小大于该值时会进行扩容
int threshold;
//负载因子
final float loadFactor;
注:transient关键字标记的成员变量不参与序列化过程
四种构造方法:
1.传入了自定义的容量和加载因子
public HashMap(int initialCapacity, float loadFactor) {
//自定义的容量不合法
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//自定义的容量大于最大的容量,吧最大的容量值赋给自定义的容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子的合法性
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//吧自定义的值赋给成员变量,覆盖默认值
this.loadFactor = loadFactor;
//需要把自定义的容量调整为 大于该数而且是最小的2次方数(如传入12返回16)
this.threshold = tableSizeFor(initialCapacity);
}
2.传入自定义的容量 使用默认的加载因子
//自定义容量 使用默认的加载因子
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
3.使用默认的初始容量和加载因子
//空参构造 使用默认的容量和加载因子
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
4.使用默认的初始容量和加载因子
//传入一个map集合
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
//把传入的map放入hashMap
putMapEntries(m, false);
}
把map放入hashMap
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//集合的大小
int s = m.size();
if (s > 0) {
//table是否为空,也就是是否初始化
if (table == null) { // pre-size
//计算容量
float ft = ((float)s / loadFactor) + 1.0F;
//容量是否超过最大值
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
//大于临界值 重新赋值
threshold = tableSizeFor(t);
}
else if (s > threshold)
//扩容
resize();
//遍历传入的map,重新放到新定义的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);
}
}
}
返回给定目标容量的接近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);
}
获取key的hash值
static final int hash(Object key) {
int h;
//key的hashCode值 异或 (key的hashCode值右移16位,也就是该值的高16位)与1.7相比这样提高了hash值的随机性
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;
//数组的长度 i为下标
int n, i;
//判断table数组是否为空或者长度为0
if ((tab = table) == null || (n = tab.length) == 0)
//扩容并且初始化table,吧长度赋给n
n = (tab = resize()).length;
//如果不是空的,则为节点类型的p进行赋值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//如果该索引处发生碰撞,先查看key值是否相同,如果key值相同,则覆盖原来的值
//如果key值不同,也有链表和红黑树两种方式
else {
//e是一个临时的变量 存放节点
Node<K,V> e;
//k为存放临时节点的key
K k;
//hash相同 p节点有值 p赋给临时节点e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//如果是红黑树 按照红黑树的插入方式进行
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//按照链表的方式插入
//遍历链表
for (int binCount = 0; ; ++binCount) {
//p的下一个元素是否为null
if ((e = p.next) == null) {
//如果为下一个null 则添加元素,即在链表尾端创建节点并插入
p.next = newNode(hash, key, value, null);
//判断当前链表的数量是否大于树结构的阈值
//默认定义链表的长度为8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//如果大于 则链表转为红黑树(目的是优化查询性能)
treeifyBin(tab, hash);
break;
}
//hash相同 p节点有值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//p赋给临时节点e
p = e;
}
}
//key相同时 覆盖旧值 并返回旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//修改HashMap的次数自增
++modCount;
//容量自增和临界值比较
if (++size > threshold)
//扩容
resize();
afterNodeInsertion(evict);
//添加成功
return null;
}
判断是否是第一次初始化,如果是,则扩容。
计算hash值并计算出下标,判断该位置是否有为null,不为null,则把该值添加到该node节点上;
否则:发生哈希冲突,这里采用拉链法解决。
计算hash值以及下标,找出冲突位置,如果key相同,直接覆盖旧值。
key不同:
红黑树 putTreeVal()
链表,找到节点的next为null的位置,插入到后面,跟阈值减一的操作进行比较,大于,则进行树化; 哈希值相同并且该节点有值,直接覆盖。
判断是否需要扩容。
返回null,put成功。
扩容
final Node<K,V>[] resize() {
//扩容前的table值赋给临时Node<K,V>[]
Node<K,V>[] oldTab = table;
//扩容前table的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//扩容前的阈值
int oldThr = threshold;
//初始化新的容量和阈值
int newCap, newThr = 0;
//如果旧的容量大于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
}
//如果旧的阈值大于0
else if (oldThr > 0) // initial capacity was placed in threshold
//新的初始容量设置为阈值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//新的初始容量为默认初始容量
newCap = DEFAULT_INITIAL_CAPACITY;
//新的阈值=默认加载因子 * 默认初始容量
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果阈值等于0 得到新的阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//把新的阈值赋给临界点
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//初始化table
table = newTab;
if (oldTab != null) {
//遍历扩容前的table
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//下标为j的位置的元素部位null
if ((e = oldTab[j]) != null) {
//清空该位置的元素
oldTab[j] = null;
//判断下一个位置是否有元素
if (e.next == null)
//把e放入newTable中 e.hash & (newCap - 1)为新的下标
newTab[e.hash & (newCap - 1)] = e;
//判断是不是树形结构
else if (e instanceof TreeNode)
//拆分红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//链表
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//循环遍历链表并把它放入newTable中
do {
next = e.next;
//元素扩容后元素还在原来的位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//元素扩容后元素不在原来的位置
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;
}
先判断扩容前的容量是否大于0,也就是不是第一次初始化
如果大于0:
1.oldCap大于最大容量时,阈值更新为最大的整形数值,直接返回旧的容量。
2.新的容量=2*旧的容量 && 旧的容量大于等于初始容量时,新的阈值=旧的阈值 *2
旧的阈值大于0:
新的容量=旧的阈值
否则:
新的容量=默认初始容量
新的阈值=默认加载因子 * 默认初始容量
如果新的阈值为0:
重新赋值
创建Node[],将其赋值给原始的Node[]
如果旧的Node[]不为空:
遍历旧的Node,如果该位置不为null,就将其置位null,重新结算hash和下标,调整位置。
最后返回新的Node[]
移除
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
removeNode()
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//存储k v的数组 p是一个节点 n给长度赋值 index为索引
Node<K,V>[] tab; Node<K,V> p; int n, index;
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;
//没有发生hash冲突 也就是在数组中
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//找到位置
node = p;
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);
}
}
//确定node节点的位置之后删除
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的下一个结点为要删除结点的下一个结点
p.next = node.next;
//计数器加1
++modCount;
//大小减去1
--size;
afterNodeRemoval(node);
//返回删除的节点
return node;
}
}
//没有该节点 删除失败
return null;
}
clear()
public void clear() {
Node<K,V>[] tab;
//对hashmap的修改次数
modCount++;
//把hashMap中的数据赋给临时变量并判断非空 长度
if ((tab = table) != null && size > 0) {
//把长度置为0
size = 0;
//把每一个位置上的值置为null
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
get()
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode()
final Node<K,V> getNode(int hash, Object key) {
//tab为临时存储的hashMap first为头结点 n为hashMap的长度(size)
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;
//存在hash冲突
if ((e = first.next) != null) {
//红黑树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//链表
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//key值不存在
return null;
}
关注我
获取更多
Java干货
原创文章
视频资料
技术交流群