jdk1.8的HashMap源码解析

文章目录

  • 前言
  • 一、HashMap是什么?
  • 二、HashMap源码解析
    • 1.主要属性
    • 2.构造方法及底层数据结构(数组+单向链表)
    • 3.重要方法
      • put(key, value)
      • get(key)
      • resize()
    • 4.HashMap遍历
      • entrySet()
      • keySet()
      • values()
  • 总结


前言

HashMap在工作中是最常用的一种数据结构,那么深入了解它的工作原理也成了必不可少的一环,而且大部分面试都会问到HashMap底层原理,学习它益处多多。


一、HashMap是什么?

HashMap是基于数组+单向链表+红黑树等基本数据结构组成的一种存储key-value键值对的数据结构。基于hash操作查找速度非常快。

HashMap源码重点关注以下内容:

  • HashMap底层数据结构
  • hash碰撞解决方案
  • 扩容机制
  • 遍历原理以及遍历方式

使用的话大家应该都非常熟练,我就直接跳过直奔源码。

二、HashMap源码解析

提示:本次源码解析不包含链表转换成红黑树过程,红黑树的增删改查操作。如有兴趣可以自己深入学习。

1.主要属性

主要属性如下:

/**
* 默认初始化大小,如果你直接new HashMap()那初始大小就是16了
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
* 一个HashMap最大的存储容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
* 默认的加载因子,即存储数据达到了当前容量0.75就开始扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
* 链表变成红黑树最小的长度,即如果链表长度达到8就会变成红黑树数据结构,主要是优化查询效率
*/
static final int TREEIFY_THRESHOLD = 8;

/**
* 红黑树标称链表最少元素,红黑树中只有6个元素就会变成链表结构
*/
static final int UNTREEIFY_THRESHOLD = 6;

/**
* 红黑树的最小值,在链表转换成红黑树的时候会判断,如果HashMap大小小于64则进行扩容操作
*/
static final int MIN_TREEIFY_CAPACITY = 64;

/**
* 哈希表
*/
transient Node<K,V>[] table;

/**
* 缓存entrySet(),用于keySet()和values()
*/
transient Set<Map.Entry<K,V>> entrySet;

/**
* 当前哈希表的容量
*/
transient int size;

/**
* 修改次数,用于哈希表的快速失败机制
*/
transient int modCount;

/**
* 下一次扩容所需要达到的数量
*/
int threshold;

/**
* 加载因子
*/
final float loadFactor;

2.构造方法及底层数据结构(数组+单向链表)

提示:省略了HashMap(Map map)的构造方法

// 没有任何参数的构造方法,这里仅仅是加载因子设置为默认的0.75
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

public HashMap(int initialCapacity) {
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// tableSizeFor()方法是将你输入的初始化容量变成大于等于你输入的值的最小2的次幂。
// 例如:你输入的是11,tableSizeFor()方法计算后得到16(大于11且为2的4次方)
// 再如:23->tableSizeFor()->32(2的5次方)
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;
    // 这明明是计算后得到的初始化容量,为啥要赋值给下一次扩容所需要达到的数量,put方法中扩容可以找到答案
    this.threshold = tableSizeFor(initialCapacity);
}

来看下tableSizeFor()方法,看似简洁,实则深奥
这个方法的目的是返回大于等于参数值且最小的2的次幂,例如
输入11,返回16(2的4次方)。输入16,返回还是16。输入125,返回128(2的7次方)

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;
}

先普及一下位运算:
and & :相同位的两个数字都为1,则为1;若有一个不为1,则为0。
or | :相同位只要一个为1即为1。
>>> :无符号右移, 右移之后, 无论该数是正数还是负数, 右移之后左边都是补上0。

现在我们来看看这个函数:
我们输入参数11
int n = cap -1; 11 -> 10 二进制表示:1010
n |= n >>> 1; 1010右移一位101,然后1010 | 101 = 1111(15)
n |= n >>> 2; 1111右移一位111,然后1111 | 111 = 1111
后面操作一直是1111,即为15,然后返回n+1 = 16
我们输入参数125
int n = cap -1; 125 -> 124 二进制表示:111 1100
n |= n >>> 1; 111 1100 右移一位111 110,然后111 1100 | 111 110 = 111 1110
n |= n >>> 2; 111 1110右移两位111 11,然后111 1110 | 111 11 = 111 1111(127)
后面操作一直是111 1111,即为127,然后返回n+1 = 128
我们再来分析一下:
int n = cap -1; 这个其实是保证输入的是2的次幂返回也是2的次幂。
n |= n >>> 1; 这个保证前2个高位都是1
n |= n >>> 2; 这个保证前4个高位都是1
n |= n >>> 4; 这个保证前8个高位都是1
n |= n >>> 8; 这个保证前16个高位都是1
n |= n >>> 16; 这个保证前32个高位都是1
而int类型最多只有32位,所以保证是2的n次幂减一,最后返回的时候n+1就保证了一定是2的n次幂。

接下来我们看看HashMap最核心的东西–底层数据结构
数组+单向链表,如下图所示:
jdk1.8的HashMap源码解析_第1张图片

static class Node<K,V> implements Map.Entry<K,V> {
	// node节点存储了哈希值,key,value,和下一个的指针。说明是单向链表	
    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; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    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;
    }
}

3.重要方法

最重要也是使用的最多的方法就是put和get方法了。

put(key, value)

先看看put方法。put方法的逻辑看起来比较复杂,我简单的说一下。
1、如果Node[]数组还没初始化,先初始化。
2、如果key值经过hash之后没有产生hash碰撞,则新建一个node放入。
3、如果产生了hash碰撞,那就先用hash值比较如果相等再用equals方法比较。(这是重点,这就是为啥在java中约定如果重写equals方法就必须重写hashCode方法)
4、如果相等,则说明是同一个key,然后就直接用value替换。
5、如果没有相等的,则新建一个node放在链表最后。
6、如果达到了转换红黑树的条件就将链表转换为红黑树。
7、插入完成后还会调用afterNodeInsertion或afterNodeAccess方法,这两个方法用在LinkedHashMap中,以后介绍LinkedHashMap时详细介绍。
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;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;// 如果数组还没初始化则进行初始化
    if ((p = tab[i = (n - 1) & hash]) == null)	// (n-1)&hash可以说是等价于hash%n,但是前者性能更好
        tab[i] = newNode(hash, key, value, null);// 如果通过hash函数之后没有hash冲突,则就放在数组下标
    else {
        Node<K,V> e; K k;
        if (p.hash == hash && // 先比较hashCode然后再equals方法比较。
            ((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) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // 表示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;
}

get(key)

get方法比较简单,就是先通过 hash函数找到在数组的哪个下标,然后再通过hashCode和equals找到相等的key,返回value就好了,如果没找到,则返回null。

public V get(Object key) {
  Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return 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) {// 不相等的话就要考虑是链表结构还是数组结构
            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;
}

resize()

主要说一下小数组扩容成乘2的大数组的逻辑:
1、如果没有hash冲突,就再重新hash一下确定位置,但是要么在原来位置j上,要么在(oldCap+j)上
2、如果有哈希冲突,那么就确定hash&oldCap是为0还是1,0的话就在原来位置,1的话就在(oldCap+j)上
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&// 扩容就是乘以2
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // 走到这里说明是使用的有初始化容量的构造方法,例如HashMap(12)
        newCap = oldThr;
    else {               // 使用HashMap(),默认16,,扩容临界值16*0.75=12
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {// 如果临界值为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 = newTab;
    if (oldTab != null) { // 表示小数组要扩容成乘2的大数组,那么位置也要变
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null) // 没有hash冲突则重新hash一下
                    newTab[e.hash & (newCap - 1)] = e;// 可以尝试一下hash&(oldCap*2-1)和hash&(oldCap-1)有啥区别
                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;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {// 只看hash值的那一位是0还是1,如果是0那就还是在原来的链表中,这里称之为low链表
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {// 表示那一位是1,则表示high链表,即j+oldCap
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {// low链表在原来位置
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {// high链表在(oldCap+j)位置
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

其他方法有兴趣可以自己去阅读源码,这里就不一一展开介绍。

4.HashMap遍历

entrySet()

该方法返回entry的一个set集合,而Node就是实现了Entry,实际上就是node节点的集合。

public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
   public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<Map.Entry<K,V>> iterator() {
        return new EntryIterator();
    }
}

从源码中可以看出,这个entrySet是空的,那增强for循环的时候为啥还能遍历出数据呢?增强for循环语法糖其实就是通过迭代器遍历的。而EntrySet重写了iterator()方法,返回的是EntryIterator对象,那么遍历操作就是在这里面做的。

final class EntryIterator extends HashIterator
    implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}

abstract class HashIterator {
  Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot

    HashIterator() {
        expectedModCount = modCount;// 后面如果这两个不相等则表示有线程改了数据,就会抛ConcurrentModificationException,所谓的"fail-fast"
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
            do {} while (index < t.length && (next = t[index++]) == null);//找到第一个不为null的数组下标,赋值给next
        }
    }

    public final boolean hasNext() {
        return next != null;
    }
	// next()就是调用的这个方法
    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)// 快速失败
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        if ((next = (current = e).next) == null && (t = table) != null) {// 表示当前数组下标没有hash冲突
            do {} while (index < t.length && (next = t[index++]) == null);// 那么就找下一个有值的数组下标
        }
        return e;
    }

    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}

EntryIterator 继承了HashIterator,最终遍历还是通过HashIterator。

keySet()

这个方法返回key的set集合。源码跟entrySet()差不多,还是通过HashIterator遍历,只是next()方法不一样,它获取的是key,这里就不展开介绍了。

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}
final class KeySet extends AbstractSet<K> {
	public final Iterator<K> iterator()     { return new KeyIterator(); }
}
final class KeyIterator extends HashIterator
    implements Iterator<K> {
    public final K next() { return nextNode().key; } // 关键就是这里
}

values()

这个方法返回value的Collection集合。前两个都是set集合,为啥不一样?因为value在不同的key可能有重复的。而entry和key肯定是不会重复的。
这个源码也跟entrySet()差不多,next()方法是返回的value。

 public Collection<V> values() {
    Collection<V> vs = values;
    if (vs == null) {
        vs = new Values();
        values = vs;
    }
    return vs;
}
final class Values extends AbstractCollection<V> {
    public final Iterator<V> iterator()     { return new ValueIterator(); }
}
final class ValueIterator extends HashIterator
    implements Iterator<V> {
    public final V next() { return nextNode().value; }// 这里不一样
}

总结

以上就是今天要讲的内容,本文简单介绍了HashMap的底层数据结构(数组+单向链表),put/get方法和扩容操作,还有HashMap的遍历方式entrySet、keySet、values三种不同的遍历方式。红黑树的相关内容读者有兴趣的话可以自行阅读相关源码。


第一次写自己的理解,希望多多指正!

你可能感兴趣的:(JDK源码系列,java)