android HashMap 源码分析

HashMap 数据结构

数据结构中有数组与链表两种模式,但是这两种模式都存在一定的缺陷

数组

数组:数组的存储区域是连续的,占用内存比较严重,空间复杂度比较高,但是数组在查找时是比较简单的。数组特点:查找容易,删除、插入比较复杂

链表

链表:链表的存储区域是不连续的,占用内存比较宽松,但是当在链表中查找某一个元素时是比较复杂的。链表特点:删除、插入比较简单,查找比较复杂。

HashMap是一种数据存储的容器,并且很好的将数组与链表结合使用
到这里读者不免有些疑惑,HashMap在存储过程中是如何将数组与链表结合使用的?

HashMap存取实现

在看HashMap存储室如何实现之前,我们需要先看一下HashMap的一个内部类:HashMapEntry

HashMapEntry


     static class HashMapEntry
   
     implements Entry
    
      { final K key; V value; final int hash; HashMapEntry
     
       next; HashMapEntry(K key, V value, int hash, HashMapEntry
      
        next) { this.key = key; this.value = value; this.hash = hash; this.next = next; } //获取HashMao存储的key值 public final K getKey() { return key; } //获取HashMap存储的value值 public final V getValue() { return value; } //设置 value 值 并将 oldValue值返回 public final V setValue(V value) { V oldValue = this.value; this.value = value; return oldValue; } ... } 
      
     
    
   

HashMapEntry内部含有四个变量分别是:
K key:HashMap数据存储时 key值
V value:HashMap数据存储时value值
int hash:key的hashCode通过位运算生成的int值
HashMapEntry

put

@Override public V put(K key, V value) {
        if (key == null) {
            return putValueForNullKey(value);
        }
        int hash = Collections.secondaryHash(key);
        HashMapEntry
   
    [] tab = table; int index = hash & (tab.length - 1); for (HashMapEntry
    
      e = tab[index]; e != null; e = e.next) { if (e.hash == hash && key.equals(e.key)) { preModify(e); V oldValue = e.value; e.value = value; return oldValue; } } modCount++; if (size++ > threshold) { tab = doubleCapacity(); index = hash & (tab.length - 1); } addNewEntry(key, value, hash, index); return null; }
    
   

在put时首先判断key是否为null
当key值为null时,直接执行 putValueForNullKey(value);

 private V putValueForNullKey(V value) {
        HashMapEntry
   
     entry = entryForNullKey; if (entry == null) { addNewEntryForNullKey(value); size++; modCount++; return null; } else { preModify(entry); V oldValue = entry.value; entry.value = value; return oldValue; } }
   

直接将 entryForNullKey 赋值给 entry ,对于 entryForNullKey

transient HashMapEntry
   
     entryForNullKey;
   

赋值是在 addNewEntryForNullKey(value) 方法内进行赋值,所以第一次执行 putValueForNullKey 方法时,entry 为null,直接执行addNewEntryForNullKey 方法,在改方法内;

void addNewEntryForNullKey(V value) {
        entryForNullKey = new HashMapEntry
   
    (null, value, 0, null); }
   

初始化 entryForNullKey ,此时 传入的key、HashMapEntry 为null,hash值为 0;同时 size++、modCount++;

当entry不为null时,执行 preModify(entry) 方法,将value值赋值给当前entry 并 获取oldValue 将oldValue返回。

关于preModify(entry) 方法内是如何执行,稍后再看。

当key不为null时:
直接从put方法的第四行代码开始看,先来关注一下三行代码:

        int hash = Collections.secondaryHash(key);
        HashMapEntry
   
    [] tab = table; int index = hash & (tab.length - 1);
   

第一行 将key值通过secondaryHash 得到一个hash值,该值是如何计算:

public static int secondaryHash(Object key) {
        return secondaryHash(key.hashCode());
    }

private static int secondaryHash(int h) {
        // Spread bits to regularize both segment and index locations,
        // using variant of single-word Wang/Jenkins hash.
        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
    }

第二行 将 table值赋值给tab数据,table值具体是多少,在HashMap初始化时,

private static final Entry[] EMPTY_TABLE
            = new HashMapEntry[MINIMUM_CAPACITY >>> 1];
private static final int MINIMUM_CAPACITY = 4;
public HashMap() {
        table = (HashMapEntry
   
    []) EMPTY_TABLE; threshold = -1; // Forces first put invocation to replace EMPTY_TABLE }
   

通过 上述代码得到tab时一个长度为2 的数组

第三行代码 通过 hash & (tab.length – 1) 获得index 值,本人通过测试得到 index = 0;

接下来分析put方法 之后执行的代码:

for (HashMapEntry
   
     e = tab[index]; e != null; e = e.next) { if (e.hash == hash && key.equals(e.key)) { preModify(e); V oldValue = e.value; e.value = value; return oldValue; } }
   

在for循环中获取数组中的每一个 HashMapEntry对象,当该对象不为null的情况下,获取该HashMapEntry对象中的存储的下一个HashMapEntry对象。在循环内部存在一个判断,当HashMapEntry对象与存储的HashMapEntry对象 hash值以及key值想等的情况下 执行preModify(e); 方法,该方法稍后分析,在执行完 preModify(e); 方法之后,赋值value值,并返回oldValue

当for循环完成或者 e为null之后 执行下面代码:

        modCount++;
        if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        addNewEntry(key, value, hash, index);
        return null;

第六行 通过addNewEntry方法将 HashMapEntry放到 数组 table[index]位置 具体实现:

void addNewEntry(K key, V value, int hash, int index) {
        table[index] = new HashMapEntry
   
    (key, value, hash, table[index]); }
   

在addNewEntry 方法内有两种情况:

1、当该数组位置为null情况下,直接将HashMapEntry存放在该位置,同时在该HashMapEntry中存放一个null HashMapEntry;
2、当该数组位置不为null情况下,将该HashMapEntry存放在该位置,同时在该HashMapEntry中存放之前该位置的HashMapEntry
这样在数组同一个位置就形成了一条链式结构。

《android HashMap 源码分析》
具体的存储如上图所示。

当e.hash == hash && key.equals(e.key) 或者entry != null时
代码中执行了preModify(entry);方法,
HashMap中该方法是一个空实现:

void preModify(HashMapEntry
   
     e) { }
   

具体实现是在 LinkedHashMap

 @Override void preModify(HashMapEntry
   
     e) { if (accessOrder) { makeTail((LinkedEntry
    
     ) e); } }
    
   

只有当accessOrder 为true的情况下 执行 makeTail 该方法
在LinkedHashMap中 当 accessOrder false: 基于插入顺序 为 true: 基于访问顺序
而accessOrder 在默认情况下为false,这也就导致默认情况下preModify 该方法中并没有执行makeTail 方法。所有在HashMap put方法中并不需要太关注 preModify 方法。
那么当e.hash == hash && key.equals(e.key) 或者entry != null 时HashMap是如何存放数据的,则需要重点关注一下三行代码:

                V oldValue = e.value;
                e.value = value;
                return oldValue;

着重看第二行,当出现hash以及key相等情况下,则用新的value值覆盖oldValue值,并将oldValue值返回。

到这里可以看到HashMap的存储是将HashMapEntry存放到数组中,存放位置与HashMapEntry中key值的hashCode值相关,当两个HashMapEntry的hashCode值相同时,会将该两个HashMaEntry以链表形式存储。

那么现在出现一个问题:数组的长度是固定的,HashMap在存储时是不知道多少HashMaoEntry需要存储的。

针对上述问题,可以有两种方式:
1、定义一个最大长度的数组
2、随着HashMapEntry的数量动态改变数组长度

针对第一种方案显然是不合适的,因为定义最大长度数组需要占用很大的内存,google 显然不会这么做。
通过源代码可以看出google 显然是采取的第二种动态改变数组的方案解决存储数组大小问题的。

private transient int threshold;
transient int size;
 public HashMap() {
        table = (HashMapEntry
   
    []) EMPTY_TABLE; threshold = -1; } if (size++ > threshold) { tab = doubleCapacity(); index = hash & (tab.length - 1); }
   

在构造HashMap时,threshold 默认为 -1;
当调用put方法时,第七行 size > threshold 为true,接下来执行
doubleCapacity方法。

private HashMapEntry
   
    [] doubleCapacity() { HashMapEntry
    
     [] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { return oldTable; } int newCapacity = oldCapacity * 2; HashMapEntry
     
      [] newTable = makeTable(newCapacity); if (size == 0) { return newTable; } for (int j = 0; j < oldCapacity; j++) { HashMapEntry
      
        e = oldTable[j]; if (e == null) { continue; } int highBit = e.hash & oldCapacity; HashMapEntry
       
         broken = null; newTable[j | highBit] = e; for (HashMapEntry
        
          n = e.next; n != null; e = n, n = n.next) { int nextHighBit = n.hash & oldCapacity; if (nextHighBit != highBit) { if (broken == null) newTable[j | nextHighBit] = n; else broken.next = n; broken = e; highBit = nextHighBit; } } if (broken != null) broken.next = null; } return newTable; } 
        
       
      
     
    
   

第六行可以看出数组的长度变化每次增加时 进行翻倍。且数组长度有一个最大值:MAXIMUM_CAPACITY 该值为:1 << 30

第八行 执行了一个方法 makeTable 在该方法中可以看出:

private HashMapEntry
   
    [] makeTable(int newCapacity) { @SuppressWarnings("unchecked") HashMapEntry
    
     [] newTable = (HashMapEntry
     
      []) new HashMapEntry[newCapacity]; table = newTable; threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity return newTable; }
     
    
   

内部new出一个新的数组,并对threshold 进行赋值。经测试当 newCapacity = 4、8、16、32…. 时, 该threshold值为 newCapacity *3/4;
到这里结合 HashMap put方法中 当 size++ > threshold 时对数组进行扩充,可以想到,HashMap在存储时并非是当此时数组已经存储满之后在扩充,而是当数组中存储的数据达到当前数组的3/4时 进行数组扩充。

在这里的到一个 3/4值,该值在HashMap中实质是默认加载因子;
在HashMap变量中其实已经提到:

/**
     * The default load factor. Note that this implementation ignores the
     * load factor, but cannot do away with it entirely because it's
     * mentioned in the API.
     *
     * 

Note that this constant has no impact on the behavior of the program, * but it is emitted as part of the serialized form. The load factor of * .75 is hardwired into the program, which uses cheap shifts in place of * expensive division. */ static final float DEFAULT_LOAD_FACTOR = .75F;

ok 到这里 HashMap是如何存储数据的相信大家已经明白了,接下来看一下HashMap中是如何get数据的。

get

  public V get(Object key) {
        if (key == null) {
            HashMapEntry
   
     e = entryForNullKey; return e == null ? null : e.value; } int hash = Collections.secondaryHash(key); HashMapEntry
    
     [] tab = table; for (HashMapEntry
     
       e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { K eKey = e.key; if (eKey == key || (e.hash == hash && key.equals(eKey))) { return e.value; } } return null; }
     
    
   

get方法很简单
当传入key为null时,获取到entryForNullKey 该对象,并判断该对象是否为null 如果为null 则直接返回null,否则 返回该 HashMaEntry的value值。

当key不为null时,可以看到与put方法中一样,通过secondaryHash 方法得到hash值,可以看到该值与put时该hash值是一样的,得到该值之后,然后遍历数组table 当eKey == key || (e.hash == hash && key.equals(eKey))时,将该key值对应的value值返回,否则返回一个null。

到这里 HashMap中常用的get以及put方法都以解析完成,如果文章中有什么不正确的地方还请各位看官指出。

    原文作者:HashMap源码分析
    原文地址: https://juejin.im/entry/584e420e128fe10058b10545
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞