HashMap源码解析

来源:互联网 发布:淘宝卖家服务市场 编辑:程序博客网 时间:2024/05/02 00:58

一起来看下


定义:

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

0 0
原创粉丝点击