HashMap源码学习(一)

参考:https://juejin.im/post/599652796fb9a0249975a318#heading-5

 

HashMap 简介

HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。

 

底层数据结构

HashMap 是一个关联数组、哈希表,它是线程不安全的,允许key为null(只允许一个key为空),value为null。遍历时无序。

其底层数据结构是数组称之为哈希桶,每个桶里面放的是链表,链表中的每个节点,就是哈希表中的每个元素。

在JDK8中,当链表长度达到8,会转化成红黑树,以提升它的查询、插入效率,它实现了Map<K,V>, Cloneable, Serializable接口。 

 

链表中的节点Node

 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//哈希值
        final K key;//key
        V value;//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; }

        //每一个节点的hash值,是将key的hashCode 和 value的hashCode 亦或得到的。
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        //设置新的value 同时返回旧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;
        }
    }

树节点源码:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // 父
        TreeNode<K,V> left;    // 左
        TreeNode<K,V> right;   // 右
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;           // 判断颜色
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
        // 返回根节点
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
       }

因其底层哈希桶的数据结构是数组,所以也会涉及到扩容的问题。

当HashMap的容量达到threshold域值时,就会触发扩容。扩容前后,哈希桶的长度一定会是2的次方(如果不是2的次方,优化的 hash & (table.length-1) 会发生索引越界)。

这样在根据key的hash值寻找对应的哈希桶时,可以用位运算替代取余操作( hash & (table.length-1) 替代 hash % (table.length)),更加高效。

当然如果不考虑效率用hash % (table.length)就不要求是2的次方

而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数的扰动,以使hash值更加均衡。

因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。

但就算原本的hashCode()取得很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过 & 操作完成的,会忽略hash值的高位。因此只有hashCode()的低位参加运算,发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。 即,碰撞率会增大。

扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作。

JDK1.7 hash方法 

static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

JDK1.8 hash方法

    static final int hash(Object key) {
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^ :按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

扩容操作时,会new一个新的Node数组作为哈希桶,然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大,可想而知,在哈希表的容量越大时,性能消耗越明显。

扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。

因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位 = low位 + 原哈希桶容量 。    如果追加节点后,链表数量>=8,则转化为红黑树

构造函数(4种)

  //最大容量 2的30次方
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认的加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //哈希桶,存放链表。 长度是2的N次方,或者初始化时为0.
    transient Node<K,V>[] table;

    //加载因子,用于计算哈希表元素数量的阈值。
    final float loadFactor;
    //哈希表内元素数量的阈值,当哈希表内元素数量超过阈值时,会发生扩容resize()。
    int threshold;   // threshold = 哈希桶.length * loadFactor;

 

第一种:默认构造函数,赋值加载因子为默认的0.75f

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

第二种 :指定初始化容量的构造函数

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

第三种:同时指定初始化容量 以及 加载因子,用的很少,一般不会修改loadFactor

public HashMap(int initialCapacity, float loadFactor) {
    //边界处理
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //初始容量最大不能超过2的30次方
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //显然加载因子不能为负数
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    //设置阈值为  >=初始化容量的 2的n次方的值
    this.threshold = tableSizeFor(initialCapacity);
}

 

//根据期望容量cap,返回2的n次方形式的 哈希桶的实际容量 length。 返回值一般会>=cap 
static final int tableSizeFor(int cap) {
//经过下面的 或 和位移 运算, n最终各位都是1。
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    //判断n是否越界,返回 2的n次方作为 table(哈希桶)的阈值
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

 

第四种:新建一个哈希表,同时将另一个map m 里的所有元素加入表中

 

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
//将另一个Map的所有元素加入表中,参数evict初始化时为false,其他情况为true
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //拿到m的元素数量
    int s = m.size();
    //如果数量大于0
    if (s > 0) {
        //如果当前表是空的
        if (table == null) { // pre-size
            //根据m的元素数量和当前表的加载因子,计算出阈值
            float ft = ((float)s / loadFactor) + 1.0F;
            //修正阈值的边界 不能超过MAXIMUM_CAPACITY
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            //如果新的阈值大于当前阈值
            if (t > threshold)
                //返回一个 >=新的阈值的 满足2的n次方的阈值
                threshold = tableSizeFor(t);
        }
        //如果当前元素表不是空的,但是 m的元素数量大于阈值,说明一定要扩容。
        else if (s > threshold)
            resize();
        //遍历 m 依次将元素加入当前表中。
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

 

put方法

HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。

1.如果定位到的数组位置没有元素 就直接插入。

2.如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用

e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。

(图片来源网络,侵删)

public V put(K key, V value) {
    //先根据key,取得hash值。 再调用putVal插入节点
    return putVal(hash(key), key, value, false, true);
}

如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value。如果evict是false。那么表示是在初始化时调用的

  final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
}

 

putVal方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //tab存放 当前的哈希桶, p用作临时链表节点  
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果当前哈希表是空的,代表是初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        //那么直接去扩容哈希表,并且将扩容后的哈希桶长度赋值给n
        n = (tab = resize()).length;
    //如果当前index的节点是空的,表示没有发生哈希碰撞。 直接构建一个新节点Node,挂载在index处即可。
    //这里再啰嗦一下,index 是利用 哈希值 & 哈希桶的长度-1,替代模运算
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {//否则 发生了哈希冲突。
        //e
        Node<K,V> e; K k;
        //如果哈希值相等,key也相等,则是覆盖value操作
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;//将当前节点引用赋值给e
        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);
                    //如果追加节点后,链表数量》=8,则转化为红黑树
                    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;
            }
        }
        //如果e不是null,说明有需要覆盖的节点,
        if (e != null) { // existing mapping for key
            //则覆盖节点值,并返回原oldValue
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //这是一个空实现的函数,用作LinkedHashMap重写使用。
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //如果执行到了这里,说明插入了一个新的节点,所以会修改modCount,以及返回null。

    //修改modCount
    ++modCount;
    //更新size,并判断是否需要扩容。
    if (++size > threshold)
        resize();
    //这是一个空实现的函数,用作LinkedHashMap重写使用。
    afterNodeInsertion(evict);
    return null;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值