[java]JDK1.7版本HashMap源码分析

JDK1.7版本HashMap源码分析

前言

注:观看本文章前要对hash表有一定的了解
HashMap一个Java中一个非线程安全、用于存储键值对的容器,JDK1.7版本和JDK1.8版本在实现方式上有着一定的不同,我们先简单介绍下JDK1.7版本的HashMap的原理:通过key计算出一个hash值,在通过hash值计算出一个下标index。将键、值、hash值,下一个节点的引用next封装到一个Entry对象中,将该Entry对象放入到数组下标为index的位置。不同的key计算出的下标可能会相同,这就是所谓的hash冲突,回了解决这种冲突,HashMap用了单项链表,如果发生Hash冲突,会以头插法的方式将新节点加入到对应的的链表中。上述原理只是简单说明。详情内容请大家接着往下看

数据结构

存储结构

JDK.1.7版本的HashMap可以理解成哈希表,数组,单项链表的集合,如下图所示,每一个方格就代表一个节点或元素。
在这里插入图片描述

Entry对象

Entry类是HashMap中的的一个静态内部类,Entry对象就代表存储的一个节点,包含了一对键值,还有key的hash值以及下一个节点的引用
在这里插入图片描述

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;//键
        V value;//值
        Entry<K,V> next;//下一个节点的引用
        int hash;//键得hash值

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        //键和值都相等返回true
        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }
        //键和值得hashcode异或
        public final int hashCode() {
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }
		//该方法在节点的值被更改时调用
        void recordAccess(HashMap<K,V> m) {
        }

        /**
         * 在节点被删除时调用
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }

主要属性

	//hashMap默认容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    //HashMap的最大容量为1<<30,即2的30次方
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     *负载因子,是用来计算是否要进行扩容操作的阀值,
     *比如HashMap的容量为16,阀值 = 16 * 0.75 = 12;
     *也就是说当HashMap被使用12个元素空间时,就要进行扩容,以保证HashMap的效率

     *有的人估计会纠结为什么是0.75,我在这里作一个简单的解释
     *假设负载因子是1,HashMap的容量是16,那么随着HashMap中元素的增加,哈希冲突会越来越严重
     *Jdk1.8中的解决冲突的数据结构是红黑树,大量的哈希冲突会导致红黑树高度会越来越高。大大减小了查询速率
     *假设负载因子是0.5,也就是当HashMap的容量用到一半的时候,就进行的扩容,虽然这样可以减少红黑树的高度,提高查询效率
     *但是以前需要1M的存储空间,现在就需要2M。换句话说就是,我们虽然获得了时间,但是我们牺牲了空间

     *所以0.75是编辑HashMap的那些高手们中和时间与空间得到的一个比较合适的值
     *
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //空表
    static final Entry<?,?>[] EMPTY_TABLE = {};
     */
    //HashMap用于储存键值对的表
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

    //表中元素的数量
    transient int size;

    //进行扩容判断的阈值
    int threshold;
    
    //负载因子
    final float loadFactor;


    //每次对HashMap的操作的会使modCount++;可以用来检查线程安全
    transient int modCount;

    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

构造方法

//双参构造,传入初始容量和负载因子
public HashMap(int initialCapacity, float loadFactor) {
		//容量小于零,报异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
       	//initialCapacity 不能超过最大容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        /*
        负载因子不可能小于零,也不能是NaN,读者可以自己输出一下System.out.println(0.0f/0.0f);
        就可以知道NaN是什么了,实际上就是如果loadFactor是通过除法计算得来的,当分母为零时,计算的结果就是NaN
        NaN的全称为Not a Number,显然是不能作为负载因子的*/
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
		//给成员赋值
        this.loadFactor = loadFactor;
        //扩容阈值初始为初始容量
        threshold = initialCapacity;
        init();
    }

	//单参构造,指定容量initialCapacity,使用默认负载因子0.75f
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

	//使用默认容量 16, 使用默认负载因子0.75f
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

	//将一个Map容器中的节点赋值到当前table数组中
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        inflateTable(threshold);

        putAllForCreate(m);
    }

核心方法

roundUpToPowerOf2方法

该方法中使用到了 highestOneBit方法,关于这个方法的解释请看到我的另一博文

 //table数组必须是2的幂次方,详情请看indexFor()方法,h & (length-1);只有当length的长度为2的幂次方时
    //我们才能得到一个0~length-1的下标,比如length = 8,length - 1 = 7,7(D) = 111(B),通过与运算就能得出一个0~7的数字
    //刚好满足数组的下标,这个方法就是将一个参数变为接近2的幂次方的数作为数组的容量
    //其中 highestOneBit()方法请看我的另一篇博文
    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        //highestOneBit方法是得到比参数小并且最接近参数的2的幂次方
        //(number - 1) << 1 相当于将参数放大,使我们能得到一个大于number,并且最接近number的2的幂次方
        //比如number = 15 Integer.highestOneBit((15 - 1) << 1) = 16
        //    number = 16 Integer.highestOneBit((16 - 1) << 1) = 16  
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

   

inflateTable方法

	//并不是想你想toSize设定多大就多大
    //当一个HashMap对象被创建时,table数组是一个容量为0的空数组
    private void inflateTable(int toSize) {
        //得到一个大于toSize的2的幂次方,因为table数组的容量必须是2的幂次方
        int capacity = roundUpToPowerOf2(toSize);
        //得到扩容阈值
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //给table数组申请空间
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

hash方法及indexFor方法

关于hash方法

  */
    //将指定参数对象,返回一个用来计算table数组下标的值,具体细节请查看,另一篇博文
    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

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


    //其实就是取余运算,算出一个table数据的下标
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

get方法及相关方法

    /*
    这算是一个常用的核心方法:得到HashMap中对应键值的值
    */
    public V get(Object key) {
    	//如果参数为null,返回键为null对应的值,并返回
        if (key == null)
            return getForNullKey();
        //得到键值对应的entry对象
        Entry<K,V> entry = getEntry(key);
        //如果键不存在得到的entry对像为null
        return null == entry ? null : entry.getValue();
    }
     //得到table数组中键为null的值,没有对应的值返回null,如果数组的length为0,返回0,默认从下标为0的链表中进行查找
    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    
    //判断是否存在某一个键
    public boolean containsKey(Object key) {
    	//能通过对应键找到对应的entry对象就表示存在
        return getEntry(key) != null;
    }
    
	/*
    *通过指定的键值key找到对应的Entry对象,如果参数为null,就是从数组中下标为0的链表开始找起
    */
    final Entry<K,V> getEntry(Object key) {
    	//table表中没有元素,返回null
        if (size == 0) {
            return null;
        }
        //通过hash()方法得到键得哈希值
        int hash = (key == null) ? 0 : hash(key);
        //哈希表肯定会存在哈希冲突,JDK1.7版本的HashMap解决哈希冲突的方式是单项链表,所以查找时要遍历链表
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
        //找到键和参数相等的Entry对象,并返回
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

put方法及相关方法

   public V put(K key, V value) {
        //构造方法执行完之后threshold要么等于默认容量16,要么是一个自定义的大小 
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        //得到键得hash值
        int hash = hash(key);
        //通过hash值得到table数组的下标
        int i = indexFor(hash, table.length);
        //遍历当前下标所对应的链表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果链表中已经存在哈希值并别key相等的节点,替换节点中的值,并返回以前的值
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                //这个方法当一个节点中的值被替换时会被调用
                e.recordAccess(this);
                return oldValue;
            }
        }
        //对table数组的操作都要使modCount++
        modCount++;
        //给table[i]对应的链表增加一个Entry成员为hash,key,value的节点
        addEntry(hash, key, value, i);
        //替换已有节点的值时,返回的是被替换的值,添加新的节点时,返回的是null
        return null;
    }

    /**
     * 给键为null的节点赋值,默认是table[0]的链表
     */
    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

	//这个方法论功能来说和普通方法有些相似,但是它不考虑扩容和初始化table数组的问题
    private void putForCreate(K key, V value) {
        //key == null =》 hash = 0
        int hash = null == key ? 0 : hash(key);
        int i = indexFor(hash, table.length);

        /**
         * Look for preexisting entry for key.  This will never happen for
         * clone or deserialize.  It will only happen for construction if the
         * input Map is a sorted map whose ordering is inconsistent w/ equals.
         */
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                e.value = value;
                return;
            }
        }
        //创建一个节点加入table中
        createEntry(hash, key, value, i);
    }
    //将一个Map容器中的键值对生成entry对象加入到table数组中
    private void putAllForCreate(Map<? extends K, ? extends V> m) {
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            putForCreate(e.getKey(), e.getValue());
    }

resize方法及相关方法

	//扩容,在addEntry方法中使用
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        //得到当前数组的容量,如果已经是最大容量,就不执行扩容操作
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //申请一个新的容量的数组
        Entry[] newTable = new Entry[newCapacity];
        //transfer顾名思义,就是将老容器的所有元素搬运到引得数组中
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        //数组的容量变了,要重定义阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * 搬运老数组的所有entry对象,将他们复制到newTable中,注意这里给链表增加一个新元素时,是从头插入

     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                //是否从新键得hash值,可通过参数来决定
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //求出每一个元素在新数组中的下标
                int i = indexFor(e.hash, newCapacity);
                //这里链表的递增是头插法插入,因为这样不用寻找链表的尾节点
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

因为将数组中的节点放入到新数组中使用的是头插法,所以新数组中的链表和老数组中的链表顺序是反的。
在这里插入图片描述

remove方法和相关方法

	//删除table数组中指定键的Entry对象
	public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }
    //通过一个键值删除一个Entry对象
    final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        //得到键得hash值,并求出下标
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        //从数组中得到entry对象,prev当街节点的前一个节点
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            //找到要删除的节点
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                //如果删除的节点是头节点
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;

                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
        //此时说明没有找到要删除的节点,e = null;
        return e;
    }
    
    //清空整个table数组
 	public void clear() {
        modCount++;
        //将table数组中的元素全部赋值为null
        Arrays.fill(table, null);
        size = 0;
    }

containsValue方法和相关方法

 //判断是否包含一个指定值的节点
    public boolean containsValue(Object value) {
        if (value == null)
            return containsNullValue();

        Entry[] tab = table;
        //遍历整个table数组,找到值为vaule的节点返回true
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (value.equals(e.value))
                    return true;
        return false;
    }

    //
    private boolean containsNullValue() {
        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (e.value == null)
                    return true;
        return false;
    }

clone方法

public Object clone() {
        HashMap<K,V> result = null;
        try {
            result = (HashMap<K,V>)super.clone();
        } catch (CloneNotSupportedException e) {
            // assert false;
        }
        if (result.table != EMPTY_TABLE) {
            result.inflateTable(Math.min(
                (int) Math.min(
                    //得数组的总容量,因为负载可以设定,但是这里将负载因子规定最小为0.25f
                    size * Math.min(1 / loadFactor, 4.0f),
                    // we have limits...
                    HashMap.MAXIMUM_CAPACITY),
               table.length));
        }
        result.entrySet = null;
        result.modCount = 0;
        result.size = 0;
        result.init();
        //将本对象中的table数组中的所有元素放入到result中去
        result.putAllForCreate(this);

        return result;
    }

addEntry方法

	//给table[bucketIndex]对应的链表添加一个新节点
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //添加一个Entry对象时,如果table数组中的元素数大于扩容阈值时,需要扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //因为table数组的容量必须是2的幂次方所以扩容两倍
            resize(2 * table.length);
            //扩容后数组容量改变,所以要重新计算hash值下标
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        //构成entry对象,加入到数组中
        createEntry(hash, key, value, bucketIndex);
    }

createEntry方法

	//创建一个entry对象,将它加到table[bucketIndex]中
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

迭代器

private abstract class HashIterator<E> implements Iterator<E> {
		//下一个节点的引用
        Entry<K,V> next;        // next entry to return
        //与modCount进行比较来判断
        int expectedModCount;   //fast-fail
        //下一个节点的下标
        int index;
        //当前节点              // current slot
        Entry<K,V> current;     // current entry

        HashIterator() {
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                //找到table数组中第一个不是null的元素
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }
		//是否存在下一个节点
        public final boolean hasNext() {
            return next != null;
        }
		//返回下一个节点,返回的顺序是将一条链表遍历完,在去遍历下一跳
        final Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();

            if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
        		       ;
            }
            current = e;
            return e;
        }
		//删除一个节点
        public void remove() {
            if (current == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            //得到当前节点的键
            Object k = current.key;
            current = null;
            //删除对应的Entry对象
            HashMap.this.removeEntryForKey(k);
            //更新modCont的值
            expectedModCount = modCount;
        }
    }

KeySet方法

	//这也是我们经常用的方法了,返回值是得到HashMap的键集
 	public Set<K> keySet() {
 		//keySet是HashMap父类的AbstractSet<K>的一个成员,初始化后keySet=null
        Set<K> ks = keySet;
        return (ks != null ? ks : (keySet = new KeySet()));
    }

	private final class KeySet extends AbstractSet<K> {
        public Iterator<K> iterator() {
            return newKeyIterator();
        }
        public int size() {
            return size;
        }
        public boolean contains(Object o) {
            return containsKey(o);
        }
        public boolean remove(Object o) {
            return HashMap.this.removeEntryForKey(o) != null;
        }
        public void clear() {
            HashMap.this.clear();
        }
    }
    
	Iterator<K> newKeyIterator()   {
        return new KeyIterator();
    }
    
    //这个继承继承的非常巧妙,此类继承了HashIterator<K>,所有拥有hasNest()方法
    private final class KeyIterator extends HashIterator<K> {
	      public K next() {
	      	  //nextEntry()返回当前Entry对象
	          return nextEntry().getKey();
	      }
    }

关于在多线程条件下头插法引起的链表成环的问题

这个方法的功能是将将老数组的节点放入到扩容后的新数组中

1 void transfer(Entry[] newTable, boolean rehash) {
2       int newCapacity = newTable.length;
3       for (Entry<K,V> e : table) {
4          while(null != e) {
5            Entry<K,V> next = e.next;
6            //是否从新键得hash值,可通过参数来决定
7            if (rehash) {
8               e.hash = null == e.key ? 0 : hash(e.key);
9            }
10           //求出每一个元素在新数组中的下标
11           int i = indexFor(e.hash, newCapacity);
12           //这里链表的递增是头插法插入,因为这样不用寻找链表的尾节点
13           e.next = newTable[i];
14           newTable[i] = e;
15           e = next;
           }
        }
    }

假设两个线程同时执行transfer()方法,为了方便说明我们规定两个线程分别为线程A和线程B
老数组为下图:
在这里插入图片描述
当线程A第一次进入for循环和while循环,执行完第5行的代码时,该线程的时间片段到了,紧接着执行B线程竞争到了CPU,开始执行。此时对于A线程来说,e = 节点1的地址(或者叫引用)。
B线程在它所在的时间片段内完成了扩容操作,为了方便讲解,我们假设假设扩容完毕后,变成了下模样。
在这里插入图片描述
这时B线程的时间片段也到了,A线成竞争到CPU开始执行,此时 e 是节点1的地址,紧接着执行完这几行代码时
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
我们的新数组变成了如下模样
在这里插入图片描述
我们发现我们的单项链表成环了,一旦成环,就代表我们遍历某一个链表就成了死循环。
这从根本上证明了HashMap是多线程不安全的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值