HashMap源码

1、HashMap

HashMap是基于哈希表的 Map 接口的实现。允许使用 null 值和 null 键。此类不保证数据顺序(Hash取模长度),特别是它不保证该顺序恒久不变(扩容)。且是非线程安全的。

2、结构


相比List,Hash的类图结构相对没那么复杂

2.1、Map

先来看下顶级接口Map
文档说明:

1、Map是一个将键映射到值的对象。映射不能包含重复的键;每个键最多可以映射到一个值。
2、这个接口取代了Dictionary类,后者是一个完全抽象的类,而不是一个接口。
3、提供了三个集合视图,允许将Map的内容视为一组键、一组值或一组键-值映射。映射的顺序被定义为映射集合视图上的迭代器返回元素的顺序。


Map接口抽象了一些通用操作。定义了一个大致的操作骨架。

Set<Map.Entry<K, V>> entrySet();

个人认为是Map的核心方法,因为后序的大部分操作都依赖此方法。
返回了一个Entry的Set集合,

2.1.1、Entry


Entry是Map的数据存储结构,存储Key和Value的映射关系,抽象了操作Entry的通用方法。其中有四个comparing方法,用于排序。

2.2、AbstractMap


提供了Map接口的骨架实现,以尽量减少实现此接口所需的工作量。

public abstract Set<Entry<K,V>> entrySet();

entrySet方法被定义为抽象的,意味着需要子类去实现它。

2.2.1、SimpleEntry


实现了Entry接口,保存Key和Value的数据结构,包含了Key和Value的成员属性.Key是final类型的。

2.2.2、SimpleImmutableEntry

实现了Entry接口,保存Key和Value的数据结构,包含了Key和Value的成员属性.Key和Value是final类型的。
所以这个类定义了一个不可变的键值映射关系。

2.3、HashMap


为了便于操作和存储数据,HashMap定义了很多内部类型。

2.3.1、内部类型

2.3.1.1、Node


Node是HashMap存储数据的数据结构,实现了Entry接口
Node含有四个成员属性

  • hash
    key的hash散列值
  • key
    存储键值映射关系的键
  • value
    存储键值映射关系的值
  • next
    处理hash冲突时,节点指向的下一条数据
2.3.1.2、KeySet、Values、EntrySet


继承了AbstractSet,实现了一个简单的Set集合功能。返回键值对中键的集合,实际操作的还是HashMap中的Node节点数据。由于同一个key不可能存储多份,所以使用Set。


继承了AbstractCollection,实现了一个简单的集合功能。返回了键值对中值的集合,由于Value是允许出现重复的,所以这里没有继承AbstractSet,而是AbstractCollection。


继承了AbstractSet,返回了一个键值对集合。返回了键值对映射关系的集合,

2.3.1.3、TreeNode


链表转红黑树的节点数据结构。

2.3.2、成员属性

// 默认初始容量,“必须是2的幂”
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;
// hash冲突时,链表转化为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树退化为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 链表转化红黑树时容器的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储数据的容器
transient Node<K,V>[] table;
// 节点数据缓存
transient Set<Map.Entry<K,V>> entrySet;
// 节点数据大小
transient int size;
// 结构变动修改数
transient int modCount;
// table发生扩容的size大小
int threshold;
// 加载因素
final float loadFactor;

2.3.3、构造方法

public HashMap() {
        // 设置加载因子为默认值
	this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity) {
        // 根据参数初始化容器长度
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
        // initialCapacity的合法性校验
	if (initialCapacity < 0)
		throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
	if (initialCapacity > MAXIMUM_CAPACITY)
		initialCapacity = MAXIMUM_CAPACITY;
        // loadFactor的合法性校验
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
		throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        // 设置加载因子
	this.loadFactor = loadFactor;
        // tableSizeFor会返回一个大于initialCapacity且是最小的2的幂
	this.threshold = tableSizeFor(initialCapacity);
}
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;
}
public HashMap(Map<? extends K, ? extends V> m) {
        // 设置默认加载因子
	this.loadFactor = DEFAULT_LOAD_FACTOR;
        // 插入数据
	putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
	int s = m.size();
	if (s > 0) {
		if (table == null) { // pre-size
                        // 根据加载因子,计算可以容纳s个元素的最小table.length
			float ft = ((float)s / loadFactor) + 1.0F;
			int t = ((ft < (float)MAXIMUM_CAPACITY) ?
					 (int)ft : MAXIMUM_CAPACITY);
			if (t > threshold)
                                // 取得最接近且大于t的2的幂
				threshold = tableSizeFor(t);
		}
                // 扩容判断,由于Map的加载因子是可通过初始化参数赋值的,要判断参数map的size是否已经大于扩容阈值
		else if (s > threshold)
			resize();
		for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
			K key = e.getKey();
			V value = e.getValue();
                        // 将数据插入到Map中
			putVal(hash(key), key, value, false, evict);
		}
	}
}

四个构造器都不会处理table的初始化,table的初始化被延迟到首次插入数据进行。

构造方法 描述
HashMap()无参构造器 使用默认参数的HashMap
HashMap(int) 根据参数初始化容量,调用HashMap(int, float)构造器初始化。
HashMap(int, float) 根据参数初始化容量和加载因子,这里实际初始化的是threshold,再首次插入数据时会根据threshold的大小进行table的初始化。
HashMap(Map<? extends K, ? extends V>) 初始化HashMap,并将参数Map中的数据插入到Map中。

2.3.4、常用方法

2.3.3.1、hash
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

获取key的hash值,这个值与右面计算获取table[]下标值有关。
在java中,int占4个byte,32个bit,而“h >>> 16”,取h的高16位,然后在与h进行异或操作。为什么这么处理,与计算table[]下标值有关。

2.3.3.2、V put(K key, V value)

将键值对关联,插入到HashMap中,如果键 已存在,则替换值,并返回旧值

    // 对外的数据插入接口
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    // HashMap内部插入数据实现
    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为空,调用resize()初始化table
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // (n - 1) & hash 计算table下标值,若table下标数据为空,则创建节点插入数据,否则修改数据
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 创建节点信息,插入table指定下标
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 判断下标位节点key与参数key是否一致,条件是hash值一致且key相同或key的内容值相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果节点是树节点,则调用树的putTreeVal写入数据
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 无限循环,统计当前循环节点个数,binCount用于判断是否链表转红黑树
                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比较
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 节点后移,进入下次循环,重新进行比较
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 扩容判断
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
2.3.3.3、Node<K,V>[] resize()

resize会初始化table或对table进行扩容,如果table为空,则根据threshold对table进行初始化,否则按当前table的长度进行2的幂次扩容
扩容时,会将链表节点对半拆分,分别挂在到newTab[j]和newTab[oldCap + j]

final Node<K,V>[] resize() {
    // 扩容前的table
    Node<K,V>[] oldTab = table;
    // 扩容前的table容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 扩容前的阈值
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 如果旧容量大于0,则进行2的幂次扩容
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 旧容量为0,旧阈值大于0,则按旧阈值初始化table,这里会在除了无参构造器的其他其他构造器初始化HashMap时执行
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 使用无参构造器初始化HashMap时执行,设置默认容量和阈值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    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"})
    // 根据newCap对table进行扩容
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 判断旧容器是否为空,不为空则需要将旧容器的节点信息插入到扩容后的新容器
    if (oldTab != null) {
        // 从0开始遍历旧容器
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    // 后继节点不存在,则根据hash值与新容器容量取模得到下标,插入到新容器
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // 后继节点存在,且是红黑树节点,则对数进行切割,挂到新容器中
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 节点有后继节点,且是链表节点,遍历链表,对节点进行再分配,插入到新容器。
                    // 这里定义了两个头结点和尾结点,代表两个链表,hash & oldCap 为0的,为一个链表,否则为另一个链表,hash & oldCap 为0的链表插入到newTab[j],否则插入到newTab[j + oldCap]
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 将链表分割为两个链表的条件,这里我的理解是,忽略余数,如果hash值是oldCap的奇数倍,则一个队列,偶数倍为另一个队列
                        if ((e.hash & oldCap) == 0) {
                            // 尾结点为空,则说明头结点为空,设置头结点
                            if (loTail == null)
                                loHead = e;
                            else
                                // 当前循环节点挂在尾结点后
                                loTail.next = e;
                            // 重置尾结点为当前节点,由于初始状态head和tail都为空,插入第一个元素时,head和tail都指向同一个节点,从第二个节点开始,tail的实际意义变为上一个节点信息,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;
}
2.3.3.4、void treeifyBin(Node<K,V>[] tab, int hash)

链表树形化,如果采用链表处理hash冲突,当链表长度越来越长,hashmap的效率会越来越慢,因为链表需要从头开始遍历。时间复杂度为O(n)
链表树形化后,时间复杂度O(logn)

// 链表树形化,树化是对某个节点链表进行树化,通过入参hash值可以计算得出下标,然后对下标出的链表进行树化
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 树形化条件,tab.length 必须大于等于 64,否则进行扩容处理
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 树的首尾节点,hd为head, tl为tail
        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记录的是尾结点,即上一次循环的节点
            tl = p;
        } while ((e = e.next) != null);      // 循环结束条件是 节点不存在后继
        // 上面的循环结束后,实际只是从单向链表转成了一个双向链表,如果hd不为空,则还要将双向链表转为数
        if ((tab[index] = hd) != null)
            // 树化双向链表
            hd.treeify(tab);
    }
}
// 双向链建树形化
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    // 遍历链表,x是当前节点,next是后继节点
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (root == null) {
            // 设置红黑树根节点即基本属性
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            // 记录节点key
            K k = x.key;
            // 记录节点hash
            int h = x.hash;
            // 记录key类型
            Class<?> kc = null;
            // 遍历树
            for (TreeNode<K,V> p = root;;) {
                // dir 标识节点在左侧还是右侧,ph为树节点hash值
                int dir, ph;
                // 记录树节点key
                K pk = p.key;
                // 比较树节点hash和链表节点hash值,树节点hash大于链表节点hash,则再左侧,小于则在右侧,
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                // 处理hash值相等的情况
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode<K,V> xp = p;
                // 根据dir判断左边还是右边
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    // 对树进行平衡操作
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    // 将根节点设置到tab中
    moveRootToFront(tab, root);
}
2.3.3.3、get(Object key)
// 根据键获取值
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;
    // 根据(n - 1) & hash获取下标值
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 判断下标节点key与参数key是否相等
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 如果后继节点是树,则获取树节点信息,否则遍历链表,比较key和hash值
            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);
        }
    }
    return null;
}
2.3.3.4、
posted @ 2020-11-30 06:58  Simple°  阅读(106)  评论(0编辑  收藏  举报