详解HashMap

详解HashMap

    一、数据结构

    HashMap是由Hash表(散列表)维护的一个数据结构模型,什么是Hash表呢?
    哈希表,是根据Key-value直接进行访问的数据结构,也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表:记录的存储位置=f(关键字)。

    首先我们来看看HashMap源码中的“静态类”Entry:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
}

    源码过多,我们只展示其数据结构,是一个典型的链式数据结构,完全可以推测出HashMap解决Hash冲突的方式可能为链地址法。

    二、HashMap中的主要方法

    1.put()方法

    依旧根据源码进行分析,对其数据结构进行更深入的验证和分析:

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
//当table为空时,传入一个临界值,构造一个新的table,table为一个数组,存放多个Entry,该方法只允许最终数组大小为2的幂
        }
        if (key == null)
            return putForNullKey(value);
//允许加入一个key为空的value
        int hash = hash(key);
//获取key的hash值,通过hash函数对key的HashCode进行处理得到的值
        int i = indexFor(hash, table.length);
//根据Key的HashCode找到其在数组中的index:return h & (length-1);也就是使用数组长度求余。
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//循环遍历数组中指定index中的Entry链表
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//当key的HashCode值相同,且两者equals为true,开始覆盖原来的key
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
//否则表示key为空,插入新值
        modCount++;
        addEntry(hash, key, value, i);//加入一个新的Entry
        return null;
    }

    2.get()方法:

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

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

    get方法相对简单,如果key为null则调用getForNullKey方法,否则getEntry,获取指定entry。

    3.总结

    由以上源码我们可以得出,HashMap解决冲突的方式是链地址法。Hash函数,为key的HashCode值,与Hash表表长求余,余数为插入表的index,若有冲突,则在该表的Entry后面链式插入。同时,可以根据源码细节,看出HashMap允许key为null。

    三、addEntry()细节和HashMap的扩容

    addEntry()源码:
    //在table指定位置新增Entry, 这个方法很重要      
    void addEntry(int hash, K key, V value, int bucketIndex) {  
        if ((size >= threshold) && (null != table[bucketIndex])) {  
        //table容量不够, 该扩容了(两倍table),重点来了,下面将会详细分析  
            resize(2 * table.length);  
        //计算hash, null为0  
            hash = (null != key) ? hash(key) : 0;  
        //找出指定hash在table中的位置  
            bucketIndex = indexFor(hash, table.length);  
        }  
  
        createEntry(hash, key, value, bucketIndex);  
    }  
      
    //扩容方法 (newCapacity * loadFactor)  
    void resize(int newCapacity) {  
        Entry[] oldTable = table;  
        int oldCapacity = oldTable.length;  
    //如果之前的HashMap已经扩充打最大了,那么就将临界值threshold设置为最大的int值  
        if (oldCapacity == MAXIMUM_CAPACITY) {  
            threshold = Integer.MAX_VALUE;  
            return;  
        }  
      
    //根据新传入的capacity创建新Entry数组,将table引用指向这个新创建的数组,此时即完成扩容  
        Entry[] newTable = new Entry[newCapacity];  
        transfer(newTable, initHashSeedAsNeeded(newCapacity));  
        table = newTable;  
    //扩容公式在这儿(newCapacity * loadFactor)  
    //通过这个公式也可看出,loadFactor设置得越小,遇到hash冲突的几率就越小  
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);  
    }  
  
    //扩容之后,重新计算hash,然后再重新根据hash分配位置,  
    //由此可见,为了保证效率,如果能指定合适的HashMap的容量,会更合适  
    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;  
            }  
        }  
    } 

    在addEntry时,会判断是否到了当前HashMap的容量临界值,如果到了,则进行扩容:

    扩容方式是直接将HashMap的数组长度翻倍,默认数组的长度为16,负载因子为0.75f,临界值为负载因子乘以数组长度。扩容时机为key-value键值对也就是Entry的数量大于临界值时,进行扩容,并在扩容后将原来的元素重写排版。



    

猜你喜欢

转载自blog.csdn.net/that_is_cool/article/details/80271411