HashMap源码解析

HashMap存储结构

HashMap源码解析_第1张图片
image

image

HashMap根据hash算法计算出index,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap只允许一条记录的key为null,允许多条value为null。

HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

源码解析

成员变量

bin(是hashmap专用术语,约定桶后面存放的每一个数据称为bin )

/**
 * 默认的初始容量,必须是2的幂。16
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

/**
 * 最大的容量。1073741824 必须是2的幂
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 没有指定的时候默认加载因子 默认的加载因子0.75是对空间和时间效率的一个平衡选择 
 * 如果内存空间很多而又对时间效率要求很高,可以降低加载因子Loadfactor的值;
 * 如果内存空间紧张而对时间效率要求不高,可以增加加载因子loadFactor的值,这个值可以大于1。
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * bin转为红黑树判断条件之一 bin数量大于8
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 由树转换成链表的阈值UNTREEIFY_THRESHOLD当执行resize操作时
 * 当桶中bin的数量少于UNTREEIFY_THRESHOLD时使用链表来代替树。默认值是6
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 如果bin中的数量大于TREEIFY_THRESHOLD,但是capacity小于MIN_TREEIFY_CAPACITY,依然使用链表存储。
 * 此时会进行resize操作,如果capacity大于MIN_TREEIFY_CAPACITY进行树化
 */
static final int MIN_TREEIFY_CAPACITY = 64;
/**
 * 存放KV数据的数组。第一次使用的时候被初始化,根据需要可以重新resize。分配的长度总是2的幂。
 */
transient Node[] table;

/**
 * 当被调用entrySet时被赋值。通过keySet()方法可以得到map key的集合,通过values方法可以得到map *value的集合。
 */
transient Set> entrySet;

/**
 * map的大小
 */
transient int size;

/**
 * 结构修改的次数
 */
transient int modCount;

/**
 * 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
 */
int threshold;

/**
 * 填充因子
 */
final float loadFactor;

构造函数

/**
 * @param initialCapacity 初始容量
 * @param loadFactor 填充因子
 */
public HashMap(int initialCapacity, float loadFactor) {
    //容量小于0抛出illega异常
    if (initialCapacity < 0) {
        throw new IllegalArgumentException("Illegal initial capacity: " +
                initialCapacity);
    }
    //如果初始容量大于1<<30 那么容量大小就等于1<<30
    if (initialCapacity > MAXIMUM_CAPACITY) {
        initialCapacity = MAXIMUM_CAPACITY;
    }
    //如果加载因子小于等于0或者不是float类型
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
        throw new IllegalArgumentException("Illegal load factor: " +
                loadFactor);
    }
    //赋值填充因子
    this.loadFactor = loadFactor;
    //临界值 刚开始以为这样写是一个Bug,
    //觉得应该这样写this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
    //此处带着疑问去看put方法,就会明白此处为啥这么设计。这里是懒加载。并没有new的时候直接加载
    //找到大于等于initialCapacity的最小的2的幂
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * @param initialCapacity 初始容量 使用默认的加载因子
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * 默认容量16,默认加载因子0.75
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 * 传入一个map,使用默认加载因子
 */
public HashMap(Map m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    //这个方法下文putAll的时候会详细介绍
    putMapEntries(m, false);
}

put()解析

/**
 * @param key
 * @param value
 */
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

/**
 * 这里就是低16位和高16位异或。
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

/**
 * @param key hash以后的值
 * @param key
 * @param value 
 * @param onlyIfAbsent true:不改变存在的值;false:改变存在的值
 * @param evict if false, the table is in creation mode.
 * @return 返回老的值或者空
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
        boolean evict) {
    Node[] tab;
    Node p;
    int n, i;
    //判断节点数组是否为null,是null进行扩容 ;不等于null,把节点长度赋值给n,节点长度为0扩容
    if ((tab = table) == null || (n = tab.length) == 0)
    //进行扩容操作,并把扩容后的长度赋值给n
    {
        n = (tab = resize()).length;
    }
    // (tab.length-1)&hash 得到index 。注意,同一个元素在扩容前后可能得到的index不一样
    if ((p = tab[i = (n - 1) & hash]) == null)
    //如果等于null,直接newNode,放入tab;不等于null,p=当前下标得到的第一个bin
    {
        tab[i] = newNode(hash, key, value, null);
    } else {
        Node e;
        K k;
        //判断第一个节点是否等于当前元素,如果等于赋值给e。
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
            e = p;
        }
        //判断p是否是treeNode,如果是红黑树,则添加数据。红黑树的crud有文章介绍
        else if (p instanceof TreeNode) {
            e = ((TreeNode) p).putTreeVal(this, tab, hash, key, value);
        } else {
            //循环查找元素
            for (int binCount = 0; ; ++binCount) {
                //获取p的下一个bin
                if ((e = p.next) == null) {
                    //构建一个新的节点,把地址赋值给上个记得点的next指针域
                    p.next = newNode(hash, key, value, null);
                    //判断bin数量是否大于8。这里减1因为binCount是从0开始的
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    //转为红黑树。
                    //如果容量小于MIN_TREEIFY_CAPACITY,不会进行树化,会进行扩容
                    {
                        treeifyBin(tab, hash);
                    }
                    break;
                }
                //判断元素是否等于当前元素。和上面同理
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                    break;
                }
                p = e;
            }
        }
        //e不等于空,说明之前已经存在这个key
        if (e != null) { // existing mapping for key
            //获取老的值
            V oldValue = e.value;
            //onlyIfAbsent=fals 或者oldValue=null 改变存在的值。返回老的值
            if (!onlyIfAbsent || oldValue == null) {
                e.value = value;
            }
            //这个方法是为了LinkedHashMap服务的
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //改变了结构,modCount加1
    ++modCount;
    //判断size+1 是否大于临界值,大于扩容
    if (++size > threshold) {
        resize();
    }
    //这个方法是为了LinkedHashMap服务的
    afterNodeInsertion(evict);
    return null;
}

resize()解析

借用网上图片表述


HashMap源码解析_第2张图片
image
/**
 * 初始化table,或者table的大小扩大两倍。
 * 如果是table空,根据tableSizeFor方法计算出threshold的值,然后赋值给capacity,然后重新计算threshold=capacit*loadFactor。
 * 如果table不为空。根据2的幂来扩容,原来bin的元素要么在原来的位置或者从原来的位置移动到(原索引+oldCap)。
 * 扩容以后看新增hash值的bit位是1,index=(原索引+oldCap),否则就是原来的index位置
 */
final Node[] resize() {
    //当前table赋值给oldTab
    Node[] oldTab = table;
    //获取老的table长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //当前的threshold赋值给oldThr
    int oldThr = threshold;
    //定义新的容量,新的临界值
    int newCap, newThr = 0;
    //如果老的容量大于0
    if (oldCap > 0) {
        //老的容量大于最大容量
        if (oldCap >= MAXIMUM_CAPACITY) {
            //临界值等于Integer.MAX_VALUE
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //oldCap扩大2倍赋值给newCap,比较newCap是否小于最大容量 并且oldCap大于等于默认容量
        //如果满足,oldThr扩大两倍赋值给newThr
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY) {
            newThr = oldThr << 1; 
        }
    }
    //如果oldThr大于0,说明初始化map的传入了初始容量
    else if (oldThr > 0) 
    //oldThr赋值给newCap。这里需要结合this.threshold = tableSizeFor(initialCapacity);这里计算出来的threshold是。最小且大于initialCapacity的2次幂
    {
        newCap = oldThr;
    } else {              
        //进入此处,说明new HashMap的时候没有指定容量。
        //赋值默认容量,临界值等于默认容量*默认加载因子
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //如果newThr等于0,说明oldThr大于0;
    if (newThr == 0) {
        //新的容量*加载因子得到新的临界值
        float ft = (float) newCap * loadFactor;
        //赋值newThr
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
                (int) ft : Integer.MAX_VALUE);
    }
    //临界值赋值给threshold
    threshold = newThr;
    @SuppressWarnings({"rawtypes", "unchecked"})
    //定义新的table数组,指定长度为newCap
            Node[] newTab = (Node[]) new Node[newCap];
    //newTab赋值给table
    table = newTab;
    //判断oldTab是否等于null
    if (oldTab != null) {
        //循环oldCap,把老的元素重新放入newCap
        for (int j = 0; j < oldCap; ++j) {
            //定义e元素
            Node e;
            //获取j位置的索引赋值给e,判断oldTab是否等于空
            if ((e = oldTab[j]) != null) {
                //把下标为j的元素置为空
                oldTab[j] = null;
                //判断e后面是否还有元素
                if (e.next == null)
                //把e放入newTab 新的index处
                {
                    newTab[e.hash & (newCap - 1)] = e;
                }
                //判断e是否是红黑树,此处不细讲。后面会讲
                else if (e instanceof TreeNode) {
                    ((TreeNode) e).split(this, newTab, j, oldCap);
                } else { // preserve order
                    //以下代码作用保护排序和元素移动。
                    //元素移动:把oldTab中的元素挂到newTab中。这里需要注意的是,元素的位置可能不变,                             也有可能变为(原索引+oldCap)
                    //定义节点元素。lohead代表index不变的元素
                    Node loHead = null, loTail = null;
                    Node hiHead = null, hiTail = null;
                    Node next;
                    do {
                        //e的下一个元素赋值给next
                        next = e.next;
                        //判断bin中元素的index是否变化,如果==0说明index没变。
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null) {
                                loHead = e;
                            } else {
                                loTail.next = e;
                            }
                            loTail = e;
                        }
                        //不等于0说明index=index+oldCap
                        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;
                    }
                    //将节点元素放入j+oldCap
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

get()解析

/**
 * 传入key
 */
public V get(Object key) {
    Node e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
 * @param key the key
 * @return the node, or null if none
 */
final Node getNode(int hash, Object key) {
    //定义Node数组;定义一个节点first,e;定义n,定义一个K类型的k
    Node[] tab;
    Node first, e;
    int n;
    K k;
    //判断,如果table不等于null,table的长度大于0,根据计算出来的index获取table元素不等于null
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
        //这里总是检查第一个元素,我也不知道为啥这么检查。有知道的朋友可以分享一下
        //判断第一个元素的hash和传入key的hash是否相等并且判断传入的key等于first的key
        if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
        //返回第一个节点元素
        {
            return first;
        }
        //判断first节点下是否还挂着其它节点
        if ((e = first.next) != null) {
            //判断是否树化
            if (first instanceof TreeNode)
            //从红黑树中查找
            {
                return ((TreeNode) first).getTreeNode(hash, key);
            }
            do {
                //循环查找,没有找到返回null
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                    return e;
                }
            } while ((e = e.next) != null);
        }
    }
    return null;
}

putAll()解析

/**
 * 拷贝传入的元素到map
 */
public void putAll(Map m) {
    putMapEntries(m, true);
}

/**
 * @param m the map
 * @param evict 是false代表是构造方法的时候调用这个,如果是true代表是其它方法
 */
final void putMapEntries(Map m, boolean evict) {
    //获取map的大小
    int s = m.size();
    //如果size大于0
    if (s > 0) {
        //判断table是否为null
        if (table == null) { // pre-size
            //使用s/loadFactor+1.0 其实是为了获得table的容量
            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();
        }
        //循环添加
        for (Map.Entry e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

remove()解析

public boolean remove(Object key, Object value) {
    return removeNode(hash(key), key, value, true, true) != null;
}

/**
 * @param hash hash以后的值
 * @param key the key
 * @param value 如果matchValue是true,才使用value,其它忽略
 * @param matchValue 如果true必须value,key相等才能删除 only remove if value is equal
 * @param movable 如果false删除的时候不移动节点
 * @return the node, or null if none
 */
final Node removeNode(int hash, Object key, Object value,
        boolean matchValue, boolean movable) {
    //定义一个tab,定义一个Node 
    Node[] tab;
    Node p;
    int n, index;
    //table不等于null并且table长度大于0,根据index获取table元素不等于null
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
        //定义变量
        Node node = null, e;
        K k;
        V v;
        //判断第一个节点是否key是否相等,如果相等
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
        //赋值node
        {
            node = p;
        }
        //判断下一个节点元素是否
        else if ((e = p.next) != null) {
            //判断p是否是红黑树
            if (p instanceof TreeNode) {
                node = ((TreeNode) p).getTreeNode(hash, key);
            } else {
                //循环bin判断key是否存在
                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不等于null,说明根据这个key找到了这个元素。判断value是否相等
        if (node != null && (!matchValue || (v = node.value) == value ||
                (value != null && value.equals(v)))) {
            //判断是否是红黑树
            if (node instanceof TreeNode) {
                ((TreeNode) node).removeTreeNode(this, tab, movable);
            }
            //判断node是否是第一个元素。如果是获取node的下一个元素赋值给tab[index]
            else if (node == p) {
                tab[index] = node.next;
            } else
            //不是第一个元素。从p元素口面删除node元素
            {
                p.next = node.next;
            }
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

keySet()和Values()

// 返回此映射中包含的键的map视图
// 解惑:最开始在看这段代码的时候并没有看懂。
// 1:此处直接返回了一个包含key的Set集合,但是又没有对map的key进行处理,所以一直在纠结这里是怎么处理的。最后在同事的指点下,幡然大悟。
// 2:这里只是返回了一个包含map的视图,对这个集合进行遍历的时候会调用iterator方法。iteraotr方法会调用new KeyIterator();KeyIterator这个类继承了hashIterator。
//3:遍历set集合的时候会调用next方法。next方法会调用HashIterator的NextNode方法。下面对nextNode方法做了介绍
public Set keySet() {
    Set ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

final class KeySet extends AbstractSet {

    public final int size() {
        return size;
    }

    public final void clear() {
        HashMap.this.clear();
    }

    public final Iterator iterator() {
        return new KeyIterator();
    }

    public final boolean contains(Object o) {
        return containsKey(o);
    }

    public final boolean remove(Object key) {
        return removeNode(hash(key), key, null, false, true) != null;
    }

    public final Spliterator spliterator() {
        return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }

    public final void forEach(Consumer action) {
        Node[] tab;
        if (action == null) {
            throw new NullPointerException();
        }
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node e = tab[i]; e != null; e = e.next) {
                    action.accept(e.key);
                }
            }
            if (modCount != mc) {
                throw new ConcurrentModificationException();
            }
        }
    }
}

final class KeyIterator extends HashIterator
        implements Iterator {

    public final K next() {
        return nextNode().key;
    }
}


abstract class HashIterator {

    Node next;        // next entry to return
    Node current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot

    //调用iterator的方法的时候会初始化HashIterator
    HashIterator() {
        //把map中的modCount赋值给expectedModCount
        expectedModCount = modCount;
        //table赋值给t
        Node[] t = table;
        current = next = null;
        index = 0;
        //找到一个bin元素不为空就返回
        if (t != null && size > 0) { // advance to first entry
            do {
            } while (index < t.length && (next = t[index++]) == null);
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    //nextNode方法 此处就是循环查找next元素
    final Node nextNode() {
        //定义t
        Node[] t;
        //把next元素赋值给e
        Node e = next;
        //判断线性结构的修改
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        if (e == null) {
            throw new NoSuchElementException();
        }
        //如果bin上还有节点直接返回下一个节点。如果没有循环则循环table查找下一个index的node元素
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {
            } while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }

    public final void remove() {
        Node 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;
    }
}

HashMap面试总结

1:HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,使用ConcurrentHashMap。

2:hashMap采用数组加链表的形式。如果链表的长度大于8并且capacity大于MIN_TREEIFY_CAPACITY=64才会变成红黑树。

3:hashMap默认的容量是16,默认容量并不是第一次new的时候容量就是16,而是put的时候resize()的扩容的。

4:hashMap默认的加载因子loadFactor=0.75,这个数值是对空间和时间效率的一个平衡选择。如果内存空间很多而又对时间效率要求很高,可以降低负载因子Loadfactor的值;如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。

5:hash()算法原理跟获取元素index有关。(n-1) & hash

put()方法简介:

1:根据key获取hash值,hash算法大致是key的hashCode的高16位和低16位异或。得到key的hash&(table.length-1)得到index

2:判断tab元素是否为null,如果null进行扩容操作。扩容完成以后根据(n-1)&hash(key)获取到元素下标。

3:根据index获取tab中的元素e,如果获取到e为空,说明该index没元素,直接赋值即可。如果获取到的e不为空,判断e.key和key是否相等,如果相等直接覆盖原来的值即可。如果不相等,判断e是否是TreeNode,如果是,根据e从红黑树查询元素。如果不是TreeNode,循环index下标的元素,判断是否有元素相等,如果有key相等直接返回。没有元素相等链表的尾节点插入元素即可,此处还会判断节点元素是否大于TREEIFY_THRESHOLD,如果大于TREEIFY_THRESHOLD转为红黑树。转为红黑树需要链表长度大于等于8,容量大于等于64

4:如果key已经存在,把value直接覆盖,不需要修改modCount直接返回即可。

5:如果不存在,需要modCount++;判断size++>threshold,大于扩容即可。

get()方法简介:

1:根据key获取hash。

2:根据(n-1)&hash获取index

3:判断index的第一个节点元素e是否等于key

4:判断e.next是否等于空,如果不等于空,循环bin查找元素

resize()方法简介:

1:判断oldTab.length>0

2:oldTab.length>0,判断oldTab是否大于最大容量MAXIMUM_CAPACITY,如果大于则threshold=Integer.MAX_VALUE。不大于就把容量扩大2倍并且比较是否大于MAXIMUM_CAPACITY,不大于则临界值扩大2倍。

3:如果oldTab.length<=0,临界值大于0,说明这是第一次初始化map并且初始化map的时候指定了初始容量。这个时候会调用tableSizeFor()计算临界值。扩容的时候会把newCap=threshold。重新计算临界值=新的容量*加载因子

4:如果oldTab.length<=0,临界值不大于0。容量等于默认值,临界值=默认容量*加载因子

5:循环之前的数组,判断同一个index的bin上是否有多个元素,如果只有一个元素,根据这个元素重新计算index,赋值即可。如果存在多个元素,判断元素是否是红黑树,如果是进行红黑树扩容。如果不是则进行元素移动,元素可能在原来的index。也有可能变为oldCap+index。此处移动元素的原理,就是循环bin上的元素。

9:HashMap中为什么table,entrySet要使用transient是来修饰。(transient修饰表示不可被序列化)
stackoverflow 查了一下,大概有两个原因。

1:transient 是表明该数据不参与序列化。因为 HashMap 中的存储数据的数组数据成员中,数组还有很多的空间没有被使用,没有被使用到的空间被序列化没有意义。所以需要手动使用 writeObject() 方法,只序列化实际存储元素的数组。

2:由于不同的虚拟机对于相同 hashCode 产生的 Code 值可能是不一样的,如果你使用默认的序列化,那么反序列化后,元素的位置和之前的是保持一致的,可是由于 hashCode 的值不一样了,那么定位函数 indexOf()返回的元素下标就会不同,这样不是我们所想要的结果.

阅读更多文章

HashMap源码解析_第3张图片
image.png

你可能感兴趣的:(HashMap源码解析)