数据结构之HashMap 源码解析

参考博文:https://fangjian0423.github.io/2016/03/29/jdk_hashmap/ 

参考文章:  http://wiki.jikexueyuan.com/project/java-enhancement/java-twentythree.html

1. HashMap实现了 Map 接口,继承 AbstractMap。其中 Map 接口定义了键映射到值的规则,而 AbstractMap 类提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作,其实 AbstractMap 类已经实现了Map。

    public class HashMap<K,V>
        extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable

2.HashMap中几个变量

initialCapacity 和 loadFactor这两个变量决定了HashMap的性能,initialCapacity初始容量表示哈希表中桶的数量,loadFactor加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是 O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为 0.75,一般情况下我们是无需修改的。

其他的一些重要属性:

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

transient int size; // 键值对个数


/* 阀值。 值 = 容量 * 加载因子。
 * 默认值为12(16(默认容量) * 0.75(默认加载因子))。
 * 当哈希表中的键值对个数超过该值时,会进行扩容
 */

int threshold; 

3.HashMap 的基本原理

(一),hash算法:把数据的 key 转化为 hash 值,放到某 n 长度的数组中改 hash 对应的 position,比如 hash 为 0,则放在数组的第1个位置,如果 hash 为111,则放在数组的第 112 个位置。这样每次根据 hash 值就能直接知道放在什么位置,或者反过来,根据 hash 值就能直接知道从哪个位置取值。

针对上述核心原理的几个常见疑问:

一,hash值很大时,需要的数组也要很大?

这个问题好解决,hash按数组长度取模,这样无论多大的 hash 总能在数组的范围之内,随便一提,取模操作为:(n - 1) & hash

二,取模后,总会有两个不一样的hash值取模得到相同的值,这个时候,该怎么办?

这种问题就叫做碰撞冲突,HashMap的源码中使用冲突解决方法是使用单独链表法,如下图:

hashmap01.png (508Ã441)

(二),put操作:

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

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    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;  //hash表为空时,使用resize重新构建,进行扩容
         
        // 注意 i = (n - 1) & hash 就是取模定位数组的索引
        if ((p = tab[i = (n - 1) & hash]) == null)  
            tab[i] = newNode(hash, key, value, null);  //无冲突,直接插入
        else {  //存在冲突 一模一样的值或者碰撞冲突
            Node<K,V> e; K k;
            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 {
                // 碰撞冲突,顺着链表的next指针找到最后一个
                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) { // 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;
    }

(三),get操作

    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) {
             // 如果哈希表容量为0或者关键字没有命中,直接返回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;
    }

(四),hash过程和resize过程分析

hash函数如下:

    
static final int hash(Object key) {
    int h;
    // 使用hashCode的值和 hashCode的值无符号右移16位 做异或操作
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

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) { // 如果老容量大于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) // 根据thresold初始化数组
        newCap = oldThr;
    else {               // 使用默认配置
        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"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) { // 扩容之后进行rehash操作
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    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;
}

其他

1.巧妙的取模

加入数组长度是 n, 如果要对 hash 取模,大家可能想到的解法是: hash%n;而 HashMap 采用的方法是hash & (n - 1),这是为什么呢?

因为n 是 2 的次方,所以 n - 1 的而进制01111111111..,
hash “与” 01111111111实际上是取保留低位值,
结果在 n 的范围之内,类似于取模。

2.分析源码之后,我们很容易就发现了一个规律,那就是,HashMap的容量一直都是2的幂次方,但是,为什么要让HashMap的容量拥有这个特性呢?其实我们可以从性能角度去分析一下。

1. 取模快。
其实就是上面为什么快的原因:位与取模比 % 取模要快的多。
2. 分散平均,减少碰撞。 
这个是主要原因。
如果二进制某位包含 0,则此位置上的数据不同对应的 hash 却是相同,碰撞发生,
而 (2^x - 1) 的二进制是 0111111…,分散非常平均,碰撞也是最少的。

HashMap注意的地方:

HashMap底层是个哈希表,使用拉链法解决冲突
HashMap内部存储的数据是无序的,这是因为HashMap内部的数组的下表是根据hash值算出来的
HashMap允许key为null
HashMap不是一个线程安全的类

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值