Android HashMap源码分析

本版本基于android-19 sdk源码分析
上面连篇文章已经分析了一下Arraylist和linkedList源码,那怎么能错过HashMap源码分析呢!
hashMap嘚吧嘚
HashMap可以说是Java中最常用的集合类框架之一,是Java语言中非常典型的数据结构,我们总会在不经意间用到它,很大程度上方便了我们日常开发。
HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。

HashMap 底层实现是基于数组+链表实现的。他之所以查询速度快是因为它是通过计算散列码来决定存储位置的。

原理图:

hashMap底层实现原理.png

变量分析

private static final int MINIMUM_CAPACITY = 4;
private static final int MAXIMUM_CAPACITY = 1 << 30;
private transient int threshold;

HashMap容量的初始值为4,如果没有指定大小的话!最大值为整型的最大值。
threshold 是阈值,当它的大小超过这个阈值时,表就被重新哈希,一般是0.75.

  HashMapEntry(K key, V value, int hash, HashMapEntry next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
        }

首先HashMap里面实现一个静态内部类HashMapEntry,其重要的属性有 key , value, hash,next,从属性key,value我们就能很明显的看出来HashMapEntry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是HashMapEntry[],Map里面的内容都保存在HashMapEntry[]里面。
构造方法

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

无参构造方法创建了一个大小为2的HashMapEntry数组对象。

  1. 创建指定大小容量hashMap
    public HashMap(int capacity) {
        if (capacity < 0) {
            throw new IllegalArgumentException("Capacity: " + capacity);
        }
        //如果传入值为0,则按照无参构造创建tab
        if (capacity == 0) {
            @SuppressWarnings("unchecked")
            HashMapEntry[] tab = (HashMapEntry[]) EMPTY_TABLE;
            table = tab;
            threshold = -1; // Forces first put() to replace EMPTY_TABLE
            return;
        }
        //如果传入值小于4,让其等于4,如果传入值大于int最大值,让其等于最大值
        if (capacity < MINIMUM_CAPACITY) {
            capacity = MINIMUM_CAPACITY;
        } else if (capacity > MAXIMUM_CAPACITY) {
            capacity = MAXIMUM_CAPACITY;
        } else {
            capacity = Collections.roundUpToPowerOfTwo(capacity);
        }
        makeTable(capacity);
    }

    private HashMapEntry[] makeTable(int newCapacity) {
        //创建指定大小HashMapEntry
        HashMapEntry[] newTable = (HashMapEntry[]) new HashMapEntry[newCapacity];
        table = newTable;
        //阈值大小为总大小的0.75
        threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
        return newTable;
    }
  1. 创建指定容量大小和阈值的构造方法
public HashMap(int capacity, float loadFactor) {
        this(capacity);

        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new IllegalArgumentException("Load factor: " + loadFactor);
        }

        /*
         * Note that this implementation ignores loadFactor; it always uses
         * a load factor of 3/4. This simplifies the code and generally
         * improves performance.
         */
    }

put方法

    public V put(K key, V value) {
        //如果key值为空
        if (key == null) {
            //给entryForNullKey 设置值
            return putValueForNullKey(value);
        }
        //计算key的hash值,
        int hash = secondaryHash(key);
        HashMapEntry[] tab = table;
        //通过hash计算本key在数组中的下标,方便快速查找
        int index = hash & (tab.length - 1);
        for (HashMapEntry e = tab[index]; e != null; e = e.next) {
            //判断链表中是否已经存在此key,如果存在,只需要替换value即可
            if (e.hash == hash && key.equals(e.key)) {
                preModify(e);
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }

        // No entry for (non-null) key is present; create one
        modCount++;
        //判断是否需要扩容
        if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        //把对象增加如链表的头部
        addNewEntry(key, value, hash, index);
        return null;
    }
    private V putValueForNullKey(V value) {
        //获取当前entryForNullKey
        HashMapEntry entry = entryForNullKey;
        //如果当前entry为空,则需要新加否则只替换value
        if (entry == null) {
            addNewEntryForNullKey(value);
            size++;
            modCount++;
            return null;
        } else {
            preModify(entry);
            V oldValue = entry.value;
            entry.value = value;
            return oldValue;
        }
    }
    void addNewEntry(K key, V value, int hash, int index) {
        table[index] = new HashMapEntry(key, value, hash, table[index]);
    }

原理图如下

链表插入原理图.png

因此得知。当key的secondaryHash 值相同时,都会被放到同一条链表中。并且链表中刚刚加入的元素都会被放在最前面,加入时间很早的元素会被放到头部。

链表扩容

    private HashMapEntry[] doubleCapacity() {
        HashMapEntry[] oldTable = table;
        //获取老表的长度
        int oldCapacity = oldTable.length;
        //如果扩容到最大了,不需要扩容
        if (oldCapacity == MAXIMUM_CAPACITY) {
            return oldTable;
        }
        //扩容为原来的两倍,为什么只扩容为2倍?
        int newCapacity = oldCapacity * 2;
        //创建新表
        HashMapEntry[] newTable = makeTable(newCapacity);
        if (size == 0) {
            return newTable;
        }
        //开始遍历老表中数据
        for (int j = 0; j < oldCapacity; j++) {
            HashMapEntry e = oldTable[j];
            //如果index个数据为空,不需要copy
            if (e == null) {
                continue;
            }
            //根据当前链表的hash和老数组大小计算随机值
            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;
    }

因此可以发现,每次扩容都是老数组的2倍,并且把老数组中的数据重新copy到了新的数组中,但并不是原封不动的copy,会重新计算index的位置。
get方法

    public V get(Object key) {
        //当获取key为空时,直接去entryForNullKey获取值
        if (key == null) {
            HashMapEntry e = entryForNullKey;
            return e == null ? null : e.value;
        }
        int hash = key.hashCode();
        hash ^= (hash >>> 20) ^ (hash >>> 12);
        hash ^= (hash >>> 7) ^ (hash >>> 4);

        HashMapEntry[] tab = table;
        //根据key的二次hash获取到下标位置,遍历链表
        for (HashMapEntry e = tab[hash & (tab.length - 1)];
                e != null; e = e.next) {
            K eKey = e.key;
            //如果俩表中有此key,就返回此元素的value
            if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                return e.value;
            }
        }
        return null;
    }

get方法比较简单,直接通过key的二次hash获取到链表位置,再遍历链表获取到value。

思考
  1. hashmap的哈希冲突解决方法:拉链法等。拉链法的优缺点。
  2. hashmap的参数及影响性能的关键参数:加载因子和初始容量。
  3. Resize操作的过程。
  4. hashmap容量为2次幂的原因。
  • https://jingyan.baidu.com/article/4ae03de3d52ac23eff9e6bb0.html
  • https://blog.csdn.net/qq_36523667/article/details/79657400

你可能感兴趣的:(Android HashMap源码分析)