深入分析HashMap

て烟熏妆下的殇ゞ 提交于 2019-11-30 01:47:28

每个学生都有学号、姓名、年龄、身高等信息,其中学号是学生的唯一标识,要把学生存储起来,并可以通过学号快速查找。

我们很自然的想到数组( 数组支持按照下标随机访问,时间复杂度O(1) ),将这些学生的信息放到数组里,但是我们又怎么通过学号从数组里查找到该学生的信息呢?

如果可以把学号映射到数组里的一个索引位置,这样我们就可以根据学号找到索引位置,进而查到学生的信息。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

 

散列函数

下面的hash函数用key的哈希码值对数组大小取余,来锁定数组的位置。

    // 哈希函数必须把关键字的范围压缩到数组的范围,用取余操作来完成
    public int hashFunc(String key) {
        return key.hashCode() % arraySize; // hash function
    }

 

散列冲突

如果不同的key经过hash函数,命中了数组的同一个索引位置,那怎么办?

再好的hash函数也无法避免散列冲突,常用的散列冲突解决方案有开放地址法和链地址法。

开放地址法

开放地址法的思想就是当前索引有冲突了,那么我就根据一定的规则找到其他空闲的索引位置。最简单的方式就是,每次当前索引+步长(比如1),向下查找,直到找到空闲的位置,当然也可以每次hash的时候改变步长。查找的时候也是每次当前索引+步长(比如1),向下查找,直到遇到空闲位置。这里值得注意的地方是,删除的时候,不能直接将该索引位置变为空闲位置,否则会导致查找算法失效。所以删除的时候给该索引一个标志,比如deleted。

除了采用步长的方式,还可以采用多次hash的方式,比如hash1(key),hash2(key),hash3(key)..... 直到找到空闲的位置。

开放地址法这种方式,不适合存储大量的数据,因为数据多了,造成hash冲突的概率就大,就会很容易填满数组,影响效率。

jdk内部ThreadLocal.ThreadLocalMap采用了这种方式解决hash冲突。

链地址法

链地址法的思想是如果有冲突了,就将当前的元素转换为1条链表。如果链表达到一定的长度,比如8,这时候链表查询效率会下降,此时可以将链表转为其他比较合适的数据结构,比如红黑树。HashMap就是采用的这种方式解决hash冲突。

 

装载因子

当数组的空闲位置不多的时候,散列冲突就会频繁发生,为了保证散列表的效率,当占用的索引位置达到一定的比例后,我们就需要对数组进行扩容。这个比例就是装载因子。

散列表的装载因子 = 填入表中的元素个数 / 散列表的长度

装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。

HashMap

HashMap 是java 集合中实现的工业级散列表。

重要字段

负载因子,默认为 0.75(默认值是空间和时间效率的一个平衡选择,建议不要修改),threshold  = length(数组容量) *  loadFactor,当数组占用的索引位达到threshold时,就会进行扩容。

static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认的负载因子
final float loadFactor; //负载因子
​​​​​​​int threshold; //负载因子阈值

默认的初始化容量,new HashMap<>()如果不指定容量默认就是16 。如果事先知道HashMap里存储的元素数量,最好指定大小,元素数量要小于threshold,否则可能会导致频繁扩容,影响性能。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

当我们通过构造函数指定了一个容量,HashMap会通过tableSizeFor方法计算一个比initialCapacity大的一个最小的2^n。 比如传入10,容量会是16

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > 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);
}

存储到HashMap中元素的数量

transient int size;

HashMap中的数组,存储的元素是Node,Node是静态内部类,hash用来定位当前索引位置,next指向下一个节点(如果有Hash冲突,变成链表)。

 transient Node<K,V>[] table;   

 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;
        }

如果索引位置的链表长度超过8,就会转为红黑树,节点就会变成TreeNode,TreeNode是Node的子类。

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        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);
        }

 

hash函数

在hashCode值的基础上又进行了一步运算,获取一个hash值。如果key为null,则返回0,否则返回(h = key.hashCode()) ^ (h >>> 16)。

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

然后根据 hash函数计算的值对数组的长度取模,但是这里并没有用%这种方式,而是采用位与运算的方式。下面的n为数组的长度。

(n - 1) & hash

位运算是直接在内存中进行,有更好的性能,但是位运算只能用于除数是2的n次方的数的求余。

X % 2^n  = X & (2^n - 1)

简单提一下:X / 2^n 是 X >> n,那么 X & (2^n - 1) 就是取被移掉的后 n 位,也就是 X % 2^n。

那么(h = key.hashCode()) ^ (h >>> 16) 这个操作是为了什么呢?

h = key.hashCode(): 11110000 00100000  10111010 10011111
              异或: ^
          h >>> 16: 00000000 00000000  11110000 00100000
                    =
            hash值: 11110000 00100000  01001010 10111111

该函数的做法就是将hash值的高16位与低16位进行异或操作,使新值的低16位混合了高位信息和低位信息。在数组长度不大的情况下(比如16),高位信息也可以影响落到数组索引的位置。如果不进行这种扰动操作的话,只有低16位才会影响落到数组索引的位置。

hash值: 11110000 00100000  01001010 10111111
                  & 00000000 00000000  00000000 00001111
                  = 00000000 00000000  00000000 00001111

put方法

从put方法可以看出,HashMap通过i = (n - 1) & hash的方式计算在table数组中的索引位置,然后如果当前位置存在元素,则转为链表,如果链表超过了TREEIFY_THRESHOLD,则将链表转为红黑树。

public V put(K key, V value) {
    //hash(key)计算key的hash值
    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 如果为false 新值替换旧值
 * @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;
​​​​​​​    // tab为空则创建 ,resize()初始化和扩容的方法
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
​​​​    //计算在索引的位置​​​,并且当前索引的元素为null  
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null); //创建节点
    else { //当前索引的位置不为null
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))//如果索引位的key相同,则表示同一个元素
            e = p;
        else if (p instanceof TreeNode)//如果是 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 如果链表的长度超过TREEIFY_THRESHOLD ,则转为红黑树。
                        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方法

从下面的扩容方法,可以看出,HashMap的table的长度始终是2^n。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
     //table不为空,表明已经初始化过了
        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
        }
     //table未被初始化,将threshold 的值赋值给 newCap
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
     //table未被初始化,设置容量为默认容量,阈值为容量和负载因子的乘积
        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;
    }

 

HashMap实战

从put方法的源码 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) 可以看出HashMap是根据key的hashCode方法和equals方法来判断新添加的元素和原有位置的元素是否是同一个元素。所以如果是对象作为key的话,要重写hashCode和equals方法。

HashMap hm = new HashMap(); 
hm.put(new People("zs", 22), 22);
public class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
    @Override
    public int hashCode() {
        return name.hashCode();
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Person other = (Person) obj;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }
}

 

HashMap并不是线程安全

从上面put方法的源码可以看出,如果当多个线程同时读取到数组的某个索引位置为null,然后给该位置赋值,一个线程给该位置赋值了一个Node对象,后面的线程就会覆盖掉前面的线程赋值的数据。同样在链表插入的过程和红黑树插入的过程,都会存在并发问题。

这时候就需要使HashMap的方法变成同步的map

Collections.synchronizedMap(map);//使map的所有方法都加上synchronized,调用的时候都要先获取同一把锁。

或者使用ConcurrentHashMap(采用Node + CAS + Synchronized来保证并发安全,性能好)。

除了上面两种方式,还有一种写时复制的思想,读的时候不加锁,写的时候加锁。

public class CopyOnWriteMap<K, V> implements Cloneable {
    private volatile Map<K, V> internalMap;

    public CopyOnWriteMap() {
        internalMap = new HashMap<K, V>();
    }

    public V put(K key, V value) {
        synchronized (this) {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            V val = newMap.put(key, value);
            internalMap = newMap;
            return val;
        }
    }

    public V get(Object key) {
        return internalMap.get(key);
    }

    public void putAll(Map<? extends K, ? extends V> newData) {
        synchronized (this) {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            newMap.putAll(newData);
            internalMap = newMap;
        }
    }

    public void replace(Map<? extends K, ? extends V> newData) {
        synchronized (this) {
            Map<K, V> newMap = new HashMap<K, V>(newData);
            internalMap = newMap;
        }
    }
}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!