android HashMap 源码分析

776 阅读7分钟

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
这样在数组同一个位置就形成了一条链式结构。

这里写图片描述
具体的存储如上图所示。

当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方法都以解析完成,如果文章中有什么不正确的地方还请各位看官指出。