HashMap源码分析

参考:JDK1.8源码

开场白:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了乐观锁(JDK1.7是分段锁)。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换

从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的

HashMap继承图

HashMap

###HashMap构造方法

  public HashMap()
  public HashMap(int initialCapacity)
  public HashMap(int initialCapacity, float loadFactor)
  public HashMap(Map<? extends K, ? extends V> m) 

一些成员变量的理解

int initialCapacity;//初始化容量  
final float loadFactorloadFactor;//负载因子(扩容时,用来与容量相乘)  
int threshold;//阀值(容量大于该值就会扩容,扩大为原来的两倍,但最大不超过MAXIMUM_CAPACITY)
transient int size://容量值
transient Node<K, V>[] table;//实际装载数据的数组,类型为Node<K, V>

构造方法源码:

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)//最大容量为MAXIMUM_CAPACITY
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);//保证容量数值是2的倍数
}
//无参构造方法只是设置了负载因子loadFactor
public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

tableSizeFor源码:

    /**
     * 返回大于等于cap的,最小的,同时是2的n次方的数,最大不超过MAXIMUM_CAPACITY
     */
    static final int tableSizeFor(int cap) {
		//假设n=5,那最后应该返回8
        int n = cap - 1; //n=4
        n |= n >>> 1; // n = 0100 | 0010 = 0110
        n |= n >>> 2; // n = 0110 | 0001 = 0111
        n |= n >>> 4; // n = 0111 | 0000 = 0111
        n |= n >>> 8; // n = 0111 | 0000 = 0111
        n |= n >>> 16;// ...
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

之所以要保证容量数值为2的倍数,是为了__优化对象根据hash值求其所在数组下标的速度__。例如容量size为4(二进制为100),当新增一个数据是,HashMap会根据key的hash值和(size-1)相与(即hash&011),那得到的结果肯定不会大于3,所以不会造成数组越界,而且&比%具有更高的效率。

####Node类结构

static class Node<K, V> implements Map.Entry<K, V> {
    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;
    }
...

Node类结构很简单,字段有4个,hash值、key值、value值、还有一个Node类型的next字段用于指向下一个节点,从而实现链表结构。

HashMap重要方法

HashMap如何添加数据,如何扩容的呢?答案在putVal和resize方法里。
先看添加数据**putVal()**方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K, V>[] tab;
    Node<K, V> p; //tab里的计算出的下标的Node数据
    int n, i;
    if ((tab = table) == null || (n = tab.length) == 0) {
        //如果调用的是无参构造方法,table为空,所以进行扩容
        n = (tab = resize()).length;
    }
    if ((p = tab[i = (n - 1) & hash]) == null) {
        //计算出的下标下没有数据,就新建一个Node加进去
        tab[i] = newNode(hash, key, value, null);
    }
    else {
        //这是HashMap里面已经有一个同样的key的情况
        Node<K, V> e;//存放最终找到的键值key的Node对象
        K k;
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))){
            //该key下只有一个数据的情况
            e = p;
        }
        //下面是hash冲突的情况,分两种,一种是用红黑树结构存,一种是用链表存。
        //从下面代码可以看到同一个下标下数据大于等于TREEIFY_THRESHOLD-1时会进行判断是要把链表变成红黑树结构还是扩容
        else if (p instanceof TreeNode) {
            //如果p是红黑树,就用红黑树的添加方法
            e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
        }
        else {
            
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    //找不到同hash值同key的数据,新建一个
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // 改为红黑树结构存
                        treeifyBin(tab, hash);
                    break;
                }
                //找到了同hash值同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;
}

上面可以看到容量超过阈值就会进行扩容,下面看扩容resize() 方法

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) {//当前map数据量大于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;
    }
    else if (oldThr > 0) { //HashMap创建后第一次扩容
        newCap = oldThr;
    }
    else {//HashMap创建后第一次扩容,调用无参构造方法,oldThr会为0,赋值新的容量为16  阀值为16*0.75=12
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float) newCap * loadFactor;
        //如果大于MAXIMUM_CAPACITY(60)就赋值为Integer.MAX_VALUE
        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) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K, V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;//把旧数组里的引用置为null,才能让虚拟机回收掉
                if (e.next == null) {// 该key值下只有一个数据,用旧的hash与新长度-1的值&,作为下标
                    newTab[e.hash & (newCap - 1)] = e;
                }
                else if (e instanceof TreeNode) {//红黑树的插入
                    ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                }
                else { // 链表结构,下面具体分析这段代码
                    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) {
                            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;
}

来分析下复制链表结构那段代码

那里把hash值与oldCap的值为0的分为一条链表,不等于0也分为一条链表,最后把等于0的链表放到原来的下标j里,不等于0的放在j+oldCap下标里。为什么这么做的?
我们知道求数据所在的下标,是通过key的hash值&(oldCap-1),而那里是hash&oldCap。
举个例子,原来容量为oldCap为4(二进制0100),有两个数据key的hash值分别为1,5,那么根据hash&(oldCap-1)求其下标,1&0011=1,101&011=1,那它们都会放在数据下标为1那。
当扩容了,新的容量newCap为8(为原来的2倍),根据hash&(newCap-1)求其下标,1 & 0111=1,101 & 0111=101=5,可以看到hash值1还是放在了下标为1那,hash值为5的放在了下标为5的地方,刚好是1+oldCap。

归根到底还是HashMap把容量设置成2的次方数,扩容策略为原来容量的2倍的前提条件,使得容量size会是001000…的形式,容量size-1低位会是1111这样的形式,扩容为2倍,实际就把数左移一位,新的容量值newCap-1,会比原来容量值oldCap-1多一位1,那么hash & oldCap是求hash值在多出的那一位是0还是1,如果是1,复制后的下标是原来下标+oldCap

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值