hashMap的源码了解

转载自:https://blog.csdn.net/linhaosheng123456/article/details/90300339

在这里插入图片描述

1.hasmMap的定义

jdk1.8官方对hashmap的定义:基于map接口的实现,允许null键值,非同步,不保证有序,也不保证无序不随时间变化。
jdk1.8之前的版本实现采用的是数据+链表,jdk1.8采用的实现是:数据+链表+红黑树

2.put函数的实现

/**
     * 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;
        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 {
                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;
    }

主要思路是对Key得hashCode()做hash运算,然后再计算index,接着如果没有碰撞,则直接放入buckets,如果碰撞了,以链表的形式放入,如果是碰撞严重(size超过8)则把链表换成红黑树,接着如果节点已经存在就替换old value来确保key的唯一性,如果buckets满了,则重新resize。

3.get函数实现:

  /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    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;
    }

主要思路是如果是buckets里的第一个节点,则直接命中,如果有冲突,则通过key.equals去查找key,如果是数,则通过key.equals(k)查找,如果是链表,则通过key.equals(k)。
无论是put还是get数据,都是hash来确定下标,首先是是通过hashcode来进行hash操作,然后再通过hash来做进一步操作,具体如下:

/**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

当buckets占用程度超过Load Factor的比例时(0.75)就会发生resize,把size扩充为buckets的两倍,然后重新计算index,拷贝到新的bucket,实现如下:

/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    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 &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            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) {
                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 { // 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) {
                                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;
    }

扩充后的index计算,即bucket的拷贝,元素的位置要么是在原位置,要么是在原位置再以移动2次幂的位置,因此元素在重新hash之后,主要是看原来的hash新增的那个bit是0还是1,如果是0,下标没有变,如果是1,则原下标+oldcap

4.为什么用HashMap?

HashMap是一个散列桶,它存储的内筒是键值对key-value映射
HashMap采用了数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改
HashMap是非synchronized,所以HashMap很快
HashMap可以接收null键和值,而HashTbale则不能

HashTable,ConcurrentHashMap这都是用于多线程,并发的,如果map.get(key)得到了null,不能判断到底是映射的value是null,还是因为没有找到对应的key而为空,
而用于单线程的HashMap可以containKey(key)去判断到底是否包含这个null。
HashTable为什么不能containKey(key)一个线程先get(key)再containKey(key),这两个方法的中间时刻,其他线程怎么操作这个key都可能发生,例如删除key

5.HashMap的工作原理是什么?

HashMap是基于hashing的原理
我们使用put(key,value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先调用hashCode()方法,计算并返回的hashCode用于找到Map数组的bucket位置来储存Node对象。
这里关键在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Node。
在这里插入图片描述
以下是HashMap初始化
简化的模拟数据结构:

Node[] table = new Node[16]; // 散列桶初始化,table
class Node {
    hash; //hash值
    key; //键
    value; //值
    node next; //用于指向链表的下一层(产生冲突,用拉链法)
}

以下是具体的put过程(JDK1.8)

  1. 对Key求Hash值,然后再计算下标
  2. 如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中)
  3. 如果碰撞了,以链表的方式链接到后面
  4. 如果链表长度超过阈值(TREEIFY THRESHOLD == 8 ),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表
  5. 如果节点已经存在就替换旧值
  6. 如果桶满了(容量16*加载因子0.75),就需要resize(扩容2倍后重排)

以下是具体get过程:
考虑特殊情况:如果两个键的hashcode相同,你如何获取值对象?

当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
在这里插入图片描述

6.有什么方法可以减少碰撞?

扰动函数可以减少碰撞
原理是如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这就意味着存链表结构减小,这样取值的话就不会频繁调用equal方法,从而提高HashMap的性能(扰动即Hash方法内部的实现算法,目的是让不同对象返回不同hashcode)。
使用不可变的、声明作final对象,并且采用合适的equals()和hashCode()方法,将会减少碰撞的发生
不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String、Integer这样的wrapper类作为键是非常好的选择。
为什么String、Integer这样的wrapper类适合作为键?
因为String是final,而且已经重写了equals()和hashCode()方法了。不可变性是必要的,因为为了计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。

7.HashMap中hash函数是怎么实现的?

HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。
HashMap的数据结构是数组和链表的结合,所以这个HashMap里面的元素尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个。当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的。而不用再去遍历链表,所以,我们首先想到的就是把hashcode对数组长度取模运算。这样一来,元素的分布相对来说是比较均匀的。
但是“模”运算的消耗还是比较大的,有没有更快速、消耗更小的方式?

static final int hash(Object key) {
    if (key == null){
        return 0;
    }
    int h;
    h = key.hashCode();返回散列值也就是hashcode
    // ^ :按位异或
    // >>>:无符号右移,忽略符号位,空位都以0补齐
    //其中n是数组的长度,即Map的数组部分初始化长度
    return (n-1)&(h ^ (h >>> 16));
}

在这里插入图片描述
简单来说是:
高16bit不变,低16bit和高16bit做了一个异或(得到的hashcode转化成32位二进制,前16位和后16位低16bit和高16bit做了一个异或)
(n-1)&hash == ->得到下标

8.拉链发导致的链表过深,为什么不用二叉查找树代替而选择红黑树?为什么不一直使用红黑树?

之所以选择红黑树是为了解决二叉查找树的缺陷:二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成层次很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋,右旋,变色这些操作来保持平衡。引入红黑树就是为了查找数据快,解决链表查询深度的问题。我们知道红黑树属于平衡二叉树,为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历现行链表要少。所以当长度大于8的时候,会使用红黑树;如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

9.说说你对红黑树的见解?

在这里插入图片描述

每个节点非红即黑
根节点总是黑色的
如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
每个叶子节点都是黑色的空节点(null节点)
从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

10.解决hash碰撞还有哪些办法?

开放定址法
当冲突发生时,使用某种探查技术在散列表中形成一个探查序列。沿此序列逐个单元的查找,直到找到给定的地址,按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、双重散列法等。

发布了27 篇原创文章 · 获赞 3 · 访问量 705

猜你喜欢

转载自blog.csdn.net/weixin_43849280/article/details/102549629