HashMap源码解析

一起来看下


定义:

	public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

集成AbstractMap类,实现了Map、Cloneable/


常量定义:

   <span style="white-space:pre">		</span>   <pre name="code" class="html">//存储数据的Entry数组,它的大小必须是2的幂
			transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

	    	//map中保存的键值对的数量
	    	transient int size;
	    	
	    	//需要调整大小的极限值
	    	int threshold;

	        //装载因子
	        final float loadFactor;

	        //map修改的次数
	        transient int modCount;
	        
	        //默认的map大小
	        static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
	        
	        //哈希因子
	        transient int hashSeed = 0;

//默认初始大小 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //默认的装载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // static final Entry<?,?>[] EMPTY_TABLE = {};

 在HashMap中,使用Entry这一对象来存储元素结构,它在Map接口中定义: 

<pre name="code" class="html">   <span style="white-space:pre">	</span>    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;
	            threshold = initialCapacity;
	            init();
	        }
	        
	        public HashMap(int initialCapacity) {
	            this(initialCapacity, DEFAULT_LOAD_FACTOR);
	        }
	        
	        public HashMap() {
	            this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
	        }
	        //构造方法很好理解,其中init()函数为空
	        
	        
	        //使用一个Map来构造新的map的构造函数
	        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);
	            //将老的map中的元素全部放入到新的map之中
	            putAllForCreate(m);
	        }


 

对构造方法分析下:构造方法一共四个,第一个也就是主要用的,它的参数传入了两个参数,初始容量和负载因子;并且将扩展阈值的大小变为初始容量;最后一个构造函数,使用一个Map对象作为参数,来构建一个新的Map;

这个函数里面有两个新的函数,分别是inflateTable和putAllForCreate,下来看看实现:

 private void inflateTable(int toSize) {
	            // 前面提到了,table的长度一定是2的幂,这个函数是计算大于且最接近toSize的数的;这里是将容量扩大到大于toSize的最小的2的幂
	            int capacity = roundUpToPowerOf2(toSize);

	            threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
	            table = new Entry[capacity];
	            //初始化哈希的掩码值
	            initHashSeedAsNeeded(capacity);
	        }
在inflateTable里面,我们看见一个roundUpToPowerOf2()的函数,它的作用我在上面已经谢了,看下具体实现:

 //此函数返回大于等于最接近number的2的冪数:如果number>MAXIMUM_CAPACITY,返回MAXIMUM_CAPACITY;Integer.highestOneBit(number)返回小于等于最接近number的2的冪数,比如5是101,对5调用次函数,返回1000
//Integer.bitCount()是返回number中2进制中1的个数;因为number的最高位为1,所以当二进制中1的个数多余1,就说明最number大鱼Integer.highestOneBit(number),小于这个数字的2倍;因此让他扩大一倍,就是最接近大于等于number的数字了
	        private static int roundUpToPowerOf2(int number) {
	            // assert number >= 0 : "number must be non-negative";
	            int rounded = number >= MAXIMUM_CAPACITY
	                    ? MAXIMUM_CAPACITY
	                    : (rounded = Integer.highestOneBit(number)) != 0
	                        ? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
	                        : 1;

	            return rounded;
	        }


下来在看看如何旧的Map中的元素全部放入到新的Map中去~

    private void putAllForCreate(Map<? extends K, ? extends V> m) {
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            putForCreate(e.getKey(), e.getValue());
    }

对旧map中的每一个元素进行putForCreate()的操作,

  private void putForCreate(K key, V value) {
        int hash = null == key ? 0 : hash(key);
        int i = indexFor(hash, table.length);

        /**
         * <span style="font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 13.9200000762939px; line-height: 20.8800010681152px;">该方法先计算需要添加的元素的hash值和在table数组中的索引i。接着遍历table[i]的链表,若有元素的key值与传入key值相等,则替换value,结束方法。若不存在key值相同的元素,则调用createEntry创建并添加元素。</span>
        
         */
        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;
            }
        }

        createEntry(hash, key, value, i);
    }

第一步显示计算hash的值,如果为返回0,否则根据hash函数返回一个值;看下Hash()函数:

  // 这个方法的主要作用是防止质量较差的哈希函数带来过多的冲突(碰撞)问题。对hashCode再次哈希的原因是减少哈希冲突
	        final int hash(Object k) {
	            int h = hashSeed;
	            if (0 != h && k instanceof String) {
	                return sun.misc.Hashing.stringHash32((String) k);
	            }

	            h ^= k.hashCode();

	            h ^= (h >>> 20) ^ (h >>> 12);
	            return h ^ (h >>> 7) ^ (h >>> 4);
	        }

下来,根据hash的值,找到在table中的位置:indexFor()函数:

 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);
	        }
这里, 它通过  h & (table.length -1)  来得到该对象的保存位,而 HashMap 底层数组的长度总是  2  次方,这是 HashMap 在速度上的优化。

length总是 2 n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&%具有更高的效率。

  这看上去很简单,其实比较有玄机的,我们举个例子来说明:

  假设数组长度分别为1516,优化后的hash码分别为89,那么&运算后的结果如下:

       h & (table.length-1)                     hash                             table.length-1

       8 & (15-1)                                 0100                                1110                                  0100

       9 & (15-1)                                 0101                   &              1110                   =                0100

       -----------------------------------------------------------------------------------------------------------------------

       8 & (16-1)                                 0100                                1111                   =                0100

       9 & (16-1)                                 0101                                1111                   =                0101

  

  从上面的例子中可以看出:当它们和15-11110的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,89会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hash值会与15-11110)进行,那么 最后一位永远是0,而0001001101011001101101111101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!而当数组长度为16时,即为2n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对keyhashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。

   所以说,当数组长度为2n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。



最后的创建新的Entry对象函数:

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

到此,与HashMap相关的方法就已经分析完毕,下来看下HashMap常用的几个方法。


常用方法:

首先,看下HashMap中的put方法:

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> 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;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

put函数先判断table是否为空表,如果是空表则先扩张整个表,inflateTable上面已经写过~~;然后判断key的值是不是为null,如果为null,存key为null的entry元素;否则,找出其hash值和在table中的下标,然后判断将存的元素的key值时候已经在map中有,如果存在,需改value值,返回此entry~,如果没有,添加新的,返回null;


      private V putForNullKey(V value) {
 <span style="white-space:pre">		</span>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,如果table中有entry的key为null,则修改它的value,否则,创建新的entry添加到table中~~其中e.recordAccess方法的作用记录当调用put函数时,所存的entry元素的key已经存在,覆盖value的时间,这个函数是个空函数~~。

 void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

这个函数是添加新的entry的,再添加的时候,我们肯定会遇到这样一个情况,如果table的大小已经满了,且带添加的这个key需要新的table空间,则需要扩展原有的table了;这里判断如果大小大于或者等于阈值且当前添加的元素部位null,扩充table,调用resize()函数;

   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(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
resize函数先判断原来table的大小,如果达到最大值,不再扩充,阈值设为最大值;否则,将table的长度翻倍,再讲以前的元素全部放入到新的table中:
<pre name="code" class="html">   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;
                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;//这里又将链表倒序了一次。
            }
        }
    }


 
<span style="font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 13.9200000762939px; line-height: 20.8800010681152px;">  从上面的代码可以看出,HashMap之所以不能保持元素的顺序有以下几点原因:第一,插入元素的时候对元素进行哈希处理,不同元素分配到table的不同位置;第二,容量拓展的时候又进行了hash处理;第三,复制原表内容的时候链表被倒置。</span>




在来看看get方法:

  public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

首先判断key是不是为null,如果为null,去找到key为null的entry,否则,根据key的值去找,函数很好理解~


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

  final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

再来看看删除remove();

 public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }


其中的removeEntryForKey():

    final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        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;
        }

        return e;
    }
上面的这个过程就是先找到table数组中对应的索引,接着就类似于一般的链表的删除操作,而且是单向链表删除节点,很简单。在C语言中就是修改指针,这个例子中就是将要删除节点的前一节点的next指向删除被删除节点的next即可。

在看看clear方法:

 public void clear() {
        modCount++;
        Arrays.fill(table, null);
        size = 0;
    }

直接将所有的元素变为null;


在看看两个新增的函数(相比起hashtable)

containskey()

public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }
直接利用getEntry函数进行判断


containsValue()

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

没什么好说的~~


其余的基本上都是很少用的api了,大家可以自己分析分析;

我们都知道hatshable与hashmap的区别一个是同步的,另一个是不同步的,但是hashmap还有一个ConcurrentHashMap是同步的,他和hashtable有什么区别呢?下来分析hashtable与ConcurrentHashMap

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值