22.源码阅读(jdk1.6 HashMap源码和原理分析)

HashMap 底层采用数组 + 链表的的实现方式来降低数据插入和查询的时间复杂度,理想状态下可以实现时间复杂度位O(1),今天就从源码的角度看一下它是如何实现的。我们从它的两个关键方法put和get入手。

put方法
    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        //数组中i位置存在Entry对象
        for (Entry e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        //不存在则创建一个新的Entry加入数组,并将计数器加1
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

这里有两个关键方法hash和indexFor,hash()方法的作用是对key的hashCode值进行一系列的位运算,看下边的源码

/**
     * Applies a supplemental hash function to a given hashCode, which
     * defends against poor quality hash functions.  This is critical
     * because HashMap uses power-of-two length hash tables, that
     * otherwise encounter collisions for hashCodes that do not differ
     * in lower bits. Note: Null keys always map to hash 0, thus index 0.
     * 注释的意思大概是说下边代码是对给定的哈希值应用补充散列函数
     * 我们只需要知道一点,经过这些位运算之后,hash值的散列效果会
     * 得到优化即可
     */
    static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

再看indexFor方法,将得到的hash值与hashMap中数组的长度-1的值进行与运算,得到的就是这个元素将会被放置在数组中的哪个位置的index,并且绝不用担心发生数组越界之类的问题,原因请自行发现

   /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

这其实就是mod取余的一种替换方式,相当于h%(lenght-1),其中h为hash值,length为HashMap的当前长度。而&是位运算,效率要高于%。至于为什么是跟length-1进行&的位运算,是因为length为2的幂次方,即一定是偶数,偶数减1,即是奇数,这样保证了(length-1)在二进制中最低位是1,而&运算结果的最低位是1还是0完全取决于hash值二进制的最低位。如果length为奇数,则length-1则为偶数,则length-1二进制的最低位横为0,则&位运算的结果最低位恒为0,即恒为偶数。这样table数组就只可能在偶数下标的位置存储了数据,浪费了所有奇数下标的位置,这样也更容易产生hash冲突。这也是HashMap的容量为什么总是2的平方数的原因

我们再回到上边的put方法中,获取到当前元素将要插入的index后,会从当前的这个内置的数组中查找这个index位置是否已经存放了Entry,如果有则变量这个index位置的链表,判断是否有相同key值的元素存在(当前的这个数组指的就是在new HashMap的时候默认创建出来的一个Entry数组,它有一个默认长度,此时在没有进行put操作的时候,数组中元素都是null)如果此时是第一次put,那么数组中是空的,直接绕过这个for循环往下执行。如果进行put操作的时候,数组中存这个index,那么会进入这个for循环,拿到i这个位置的单链表,遍历链表中所有的Entry,如果有相同key的元素,则找到它,用新的value值替换旧value,并返回旧value

for (Entry e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

继续往下,如果不存在,那么创建新的Entry对象
addEntry(hash, key, value, i)
hash:key值计算得到的hash值
i: 经过hash和indexFor方法计算得到的被put的key,value即将被放入数组中的位置

   /**
     * Adds a new entry with the specified key, value and hash code to
     * the specified bucket.  It is the responsibility of this
     * method to resize the table if appropriate.
     *
     * Subclass overrides this to alter the behavior of put method.
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //拿到数组中bucketIndex位置的Entry对象,第一次执行,他是null
    Entry e = table[bucketIndex];
        //new了一个Entry对象放在这个bucketIndex位置上
        table[bucketIndex] = new Entry(hash, key, value, e);
        //判断是否已经达到了需要扩容的条件,如果是则对HashMap进行扩容
        if (size++ >= threshold)
            resize(2 * table.length);
    }

Entry构造方法中第四个参数很重要,我们知道HashMap是基于数组和单链表的,数组就是指的Entry数组,而单链表呢?就在Entry对象的这个next属性上

static class Entry implements Map.Entry {
        final K key;
        V value;
        Entry next;
        final int hash;
}

添加一个Entry到数组中某一位置后,会同时给它指定它的next,这个next就是它所指向的下一个Entry元素,这样,每次添加一个Entry都会把这个Entry加入到这个链表的第一个位置,它的next就是原来在第一个位置的Entry对象,这样就连成了一个链子

添加了第一个Entry到数组中某一index后,它的next指向的是一个null,因为在没有添加的时候,这个index的位置存放的就是null值

此时我们的数组中添加了第一个Entry,此时会判断当前数组的长度是否已经达到了极值(每次添加都会进行一次判断),如果是则对数组进行扩容,执行 resize(2 * table.length)可以看到,每次会扩容一倍的长度

    /**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        //判断当前数组的长度是否已经达到了设置的MAXIMUM_CAPACITY
        //如果是则将threshold (达到这个值就满足扩容的条件),设置为int的最大值
        //那么此时hashMap停止扩容,可见,hashMap容量是有极限的
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //创建一个新的Entry数组
        Entry[] newTable = new Entry[newCapacity];
        //将旧的数组中的值经过转换放入新的数组中
        transfer(newTable);
        //使用扩容后的数组
        table = newTable;
        //扩容阈值更新
        threshold = (int)(newCapacity * loadFactor);
    }

HashMap扩容的原理也很简单,因为它是基于数组实现的,所以所谓的扩容并不是将原有的数组长度扩大,而是创建了一个新的数组,这个数组的长度等于扩容后的长度,然后将旧数组中的元素转移到新数组中,旧的数组废弃,这个转移的过程仍然是经过了一系列的变换的,我们知道之前的数组中每一个put进来的元素的index都是经过一系列的位运算得到的,那么这里在进行转移的时候同样要再次进行运算得到新的index,因为这个index是基于hash值和数组长度生成的,hash值不会改变但是数组长度经过扩容后是不同了,所以要重新计算,否则会导致查询数据失败

   /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        //遍历旧的数组中所有的Entry对象
        for (int j = 0; j < src.length; j++) {
            Entry e = src[j];
            //拿到index位置的Entry后,如果这个Entry不为null,则循环遍历这个链表
            if (e != null) {
                src[j] = null;
                do {
                    //拿到链表上每一个Entry
                    Entry next = e.next;
                    //计算这个Entry在扩容后数组中的index值
                    int i = indexFor(e.hash, newCapacity);
                    //设置这个Entry的next值
                    e.next = newTable[i];
                    //把这个Entry放在计算得到的index位置上
                    newTable[i] = e;
                    //赋值获取下一个Entry,直到这个链表遍历完成
                    e = next;
                } while (e != null);
            }
        }
    }

源码是这样的先后顺序放置的,如果你把顺序调换一下会更好理解。这里我们先调整一下代码的顺序如下(不影响结果,但是更易理解)。大概过程就是假设我第一个找到了index=1的位置的这个单链表(这个链表可能只有一个元素,也可能有多个),我会获取到这个位置的第一个Entry,计算得到它在新数组中的index,然后把这个Entry放在这个数组的index位置上,将它的next指定为原本这个位置的值,其实就是null,这是第一步,第二步就是获取到第一个Entry的next值,也就是链表中第二个位置的Entry,把这个Entry重新指定为e,重复上边的操作,这样一整个过程下来,旧的数组中的所有元素都被正确的放置在了新数组中的正确位置上,达到了扩容的效果

  do {
        //计算这个Entry在扩容后数组中的index值
        int i = indexFor(e.hash, newCapacity);
        //把这个Entry放在计算得到的index位置上
        newTable[i] = e;
       //设置这个Entry的next值
        e.next = newTable[i];

        //拿到链表上每一个Entry
        Entry next = e.next;
        //赋值获取下一个Entry,直到这个链表遍历完成
        e = next;
  } while (e != null);
get方法

put看完之后get就简单多了。根据传入的key值,进行hash和indexFor运算,得到key在数组中的index,然后拿到数组中index位置的entry对象,再从这个位置的单链表中查找key值相同的value返回

    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * 

More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise * it returns {@code null}. (There can be at most one such mapping.) * *

A return value of {@code null} does not necessarily * indicate that the map contains no mapping for the key; it's also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases. * * @see #put(Object, Object) */ public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }

总结一下:
1.HashMap底层基于数组和单链表
2.hash()和indexFor()方法作用很关键
3.如果你去看看native曾hashCode值获取的方式,你会得到两个结论(hashCode值并不是简单的地址值,而是地址值进行右移16位之后的一个值,参阅ndk底层c++代码实现):

两个不同的对象 hashCode 值可能会相等,hashCode 值不相等的两个对象肯定不是同一对象。

附上手写的HashMap代码


public class MyHashMap {
    
    public MapEntry[] table;
    
    int size;
    
    /**
     * 扩容阈值,达到这个值时就要扩容
     */
    int threshold = 8;
    
    /**
     * 扩容因子
     */
    final float loadFactor = 0.75f; 
    
    public class MapEntry{
        private K key;
        private V value;
        MapEntry next;
        int hash;
        
        public MapEntry(int hash,K key,V value,MapEntry next) {
            this.key = key;
            this.value = value;
            this.hash =hash;
            this.next = next;
        }
    }

    public V put(K key,V value) {
        if(table == null) {
            table = new MapEntry[8];
        }
        
        //判断key为null
        if(key == null) {
            return null;
        }
        
        int hash = hash(key);
        int index = getIndex(hash,table.length);
        System.out.println("key = "+key+" hash="+hash+" index="+ index);
        //判断这个key是否存在
        for (MapEntry e = table[index]; e!=null;e = e.next) {
            Object k = null;
            if(e.hash == hash && ((k = e.key) == key) || (key.equals(k))){
                V oldV = e.value;
                e.value = value;
                return oldV;
            }
        }
        
        //添加新的
        addEntry(hash,key,value,index);
        return null;
    }
    
    private void addEntry(int hash, K key, V value, int index) {
        //hash值相等,两个对象不一定相等,两个对象不相等,hash值有可能相等
        //hashCode值 从native看,mask_bits(value()>> hash_shift,hash_mask))
        //是地址右移16位的结果
        if(size >= threshold && table[index] != null) {
            //右移一位,扩容一倍
            resize(size << 1);
            //重新计算index
            index = getIndex(hash,table.length);
        }
        //添加
        createEntry(hash,key,value,index);
        size++;
    }

    private void createEntry(int hash, K key, V value, int index) {
        MapEntry newEntry = new MapEntry(hash, key, value, table[index]);
        table[index] = newEntry;
    }

    private void resize(int newCapacity) {
        MapEntry [] newTable = new MapEntry[newCapacity];
        //将原来数组中的内容经过转换之后放入新数组
        transform(newTable);
        table = newTable;
        threshold = (int) (newCapacity*loadFactor);
        System.out.println("扩容之后:newCapacity="+newCapacity+" threshold="+threshold);
    }

    /**
     * 重新计算散列
     * @param newTable
     */
    private void transform(MyHashMap.MapEntry[] newTable) {
        int newCapacity = newTable.length;
        for(MapEntry e:table) {
            while(null != e) {
                MapEntry next = e.next;
                int index = getIndex(e.hash,newCapacity);
                //把原来数组中的e放在新的数组中index位置
                newTable[index] = e;
                //将这个e插入到index位置的第一个,将原来index位置的e指定给e的next
                e.next =  newTable[index];
                //把原来数组中某index位置这条链表上的每一个元素都重新归位
                e = next;
            }
        }
    }

    /**
     * 通过hash值找到table的index
     * @param hash
     * @return
     * 
     * & : 两位同时为“1”,结果才为“1”,否则为0
     */ 
    private int getIndex(int hash,int length) {
        return hash & length-1;
    }

    /**
     * ^ : 就是相同为0不同为1
     * @param key
     * @return
     */
    private int hash(K key) {
        int h = 0;
        h = key.hashCode();
        int h16 = h>>>16;
        return (key == null)?0:(h^(h16));
    }

    public V get(K key) {
        if(key == null) {
            return null;
        }
        MapEntry entry = getEntry(key);
        return entry == null?null:entry.value;
    }

    private MyHashMap.MapEntry getEntry(K key) {
        int hash = hash(key);
        int index = getIndex(hash,table.length);
        
        for (MapEntry e = table[index]; e!=null;e = e.next) {
            Object k = null;
            if(e.hash == hash && ((k = e.key) == key) || (key.equals(k))){
                return e;
            }
        }
        return null;
    }

    public int getSize() {
        // TODO Auto-generated method stub
        return size;
    }
}

红黑树

红黑树其实就是一种自平衡的二叉查找树。他这个自平衡的特性就是对HashMap中链表可能会很长做出的优化。红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶节点(NIL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

HashMap在里面就是链表加上红黑树的一种结构,这样利用了链表对内存的使用率以及红黑树的高效检索。在jdk1.8版本后,java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度

红黑树相比avl树,在检索的时候效率其实差不多,都是通过平衡来二分查找。但对于插入删除等操作效率提高很多。红黑树不像avl树一样追求绝对的平衡,他允许局部很少的不完全平衡,这样对于效率影响不大,但省去了很多没有必要的调平衡操作,avl树调平衡有时候代价较大,所以效率不如红黑树

你可能感兴趣的:(22.源码阅读(jdk1.6 HashMap源码和原理分析))