HashMap源码分析

原创 2017年06月19日 16:43:56

前提概要:

HashMap算是Map的子类中用的最多的了,其相关的类也有很多,比如LinkedHashMap、HashMap、HashTable、ConcurrentHashMap。而HashMap在其中也扮演着一个很重要的角色,本文主要分析HashMap的源码,了解完后,其他的也自然触类旁通了。

概述

HashMap本身是数组+链表的存储结构(JDK1.8加上了红黑树部分,本篇文章不提及,仅仅在目前android所使用的HashMap版本上进行讲述)。

put的大致流程如下:
1、通过hashcode计算出key的hash值
2、通过hash%length计算出存储在table中的index(源码中是使用hash&(length-1),这样结果相同,但是更快)
3、如果此时table[index]的值为空,那么就直接存储,如果不为空那么就链接到这个数所在的链表的头部。(在JDK1.8中,如果链表长度大于8就转化成红黑树)

get的大致流程如下:
1、通过hashcode计算出key的hash值
2、通过hash%length计算出存储在table中的index(源码中是使用hash&(length-1),这样结果相同,但是更快)
3、遍历table[index]所在的链表,只有当key与该节点中的key的值相同时才取出。

主要变量

    static final float DEFAULT_LOAD_FACTOR = 0.75f;
        static final HashMapEntry<?,?>[] EMPTY_TABLE = {};
    transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;
    transient int size;
    int threshold;
    final float loadFactor = DEFAULT_LOAD_FACTOR;

table:是一个HashMapEntry的数组,同时也是表明HashMap本身的数据结构是数组。一开始是为空的。
size:整个HashMap中的键值对的个数。
loadFactor:负载因子,默认为0.75。表明如果使用到的容量达到了四分之三,那么就进行扩容操作。
threshold:HashMap所能容纳的最大数据量的HashMapEntry(键值对)个数。其值一般情况下等于loadFactor *table.length。如果size>threshold,那么就进行扩容操作。

HashMapEntry

HashMapEntry是存在HashMap的数组中所存的数据结构,也是HashMap的内部类,源码如下:

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

        HashMapEntry(int h, K k, V v, HashMapEntry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }


        void recordAccess(HashMap<K,V> m) {
        }


        void recordRemoval(HashMap<K,V> m) {
        }
    }

可以看到结构其实比较简单,其中存key,value,hash以及链接的下一个HashMapEntry。这也就是数组+链表的数据结构的真身。

构造函数

    static final int DEFAULT_INITIAL_CAPACITY = 4;
    static final int MAXIMUM_CAPACITY = 1 << 30;

   public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY) {
            initialCapacity = MAXIMUM_CAPACITY;
        } else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
            initialCapacity = DEFAULT_INITIAL_CAPACITY;
        }

        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        threshold = initialCapacity;
        init();
    }

    void init() {
    }

此处主要是初始化了threshold ,规定其范围在4~2^30之间。如果小于就等于最小值,如果大于就大于最大值。
此处没有初始化table数组,说明在put之前是一直为空的。

put操作

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);
        for (HashMapEntry<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;
    }
        void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
        void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
        size++;
    }

整个put的流程还是比较简单,首先遍历计算hash值和index,然后遍历该处链表,如果已存在该key就替换value。如果该key不存在,那么就直接用addEntry()在该处创建一个HashMapEntry,如果table[index]已经存在其他键值对,那么就把原来的键值对作为next链接到现在创建的HashMapEntry尾部。

此处比较关键的还是table为空的情况,主要是inflateTable函数:

private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);

        // Android-changed: Replace usage of Math.min() here because this method is
        // called from the <clinit> of runtime, at which point the native libraries
        // needed by Float.* might not be loaded.
        float thresholdFloat = capacity * loadFactor;
        if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
            thresholdFloat = MAXIMUM_CAPACITY + 1;
        }

        threshold = (int) thresholdFloat;
        table = new HashMapEntry[capacity];
    }

首先通过roundUpToPowerOf2(toSize)计算出容量,roundUpToPowerOf2(toSize)的作用是计算出小于等于toSize的2的幂次方。然后再根据容量新建table。
此处就有一个HashMap的关键点,为什么HashMap的容量要等于2的 幂次方?
此处还是要提到table的index的计算方式,计算的时候是用hash&(length-1)来替代hash%length,而只有在length为2的幂次方的时候两者才会相等。

get操作:

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

        return null == entry ? null : entry.getValue();
    }
   final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        for (HashMapEntry<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;
    }

get操作比较简单:遍历table[index]处的链表,如果key等于该处的HashMapEntry的key那么就取出该值。

resize()操作

一般操作中,resize()是在put操作的addEntry()中才会执行,也就是说在给table[index]新建实例的时候才会执行resize()操作。条件可以看下源码:

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

当size>threshold的时候就执行resize操作,新的容量是原来的两倍。

    void resize(int newCapacity) {
        HashMapEntry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        HashMapEntry[] newTable = new HashMapEntry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    void transfer(HashMapEntry[] newTable) {
        int newCapacity = newTable.length;
        for (HashMapEntry<K,V> e : table) {
            while(null != e) {
                HashMapEntry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

resize中首先根据新的容量创建一个HashMapEntry的数组,然后交给transfer()进行数据的转移。
transfer()中遍历旧的数组,根据hash值和新的容量计算出新的index,然后放入新的数组。

版权声明:本文为博主许佳佳原创文章,转载请务必注明出处。

OkHttp源码解析

同步请求用例//创建对象 OkHttpClient client = new OkHttpClient(); //创建请求 Request request = new Request.Builder(...

JDBC的数据库连接池的工作原理

  随着信息技术的高速发展与广泛应用,数据库技术在信息技术领域中的位置越来越重要,尤其是网络应用和电子商务的迅速发展,都需要数据库技术支持动态Web站点的运行,而传统的开发模式是:首先在主程序(如Se...
  • lzs109
  • lzs109
  • 2010年03月02日 23:09
  • 3890

HashMap源码分析

  • 2017年07月22日 16:55
  • 215KB
  • 下载

HashSet和HashMap源码实现分析

术语解释: 负载因子: 负载因子表示散表的装满程度,定义为散列表中节点的数目除以基本区域能容纳的节点数所得的商.DEFAULT_LOAD_FACTOR=0.75f,当数组中75%的空间都已被使...
  • fgh2009
  • fgh2009
  • 2011年11月26日 18:41
  • 792

Java中HashMap底层实现原理(JDK1.8)源码分析

这几天学习了HashMap的底层实现,但是发现好几个版本的,代码不一,而且看了Android包的HashMap和JDK中的HashMap的也不是一样,原来他们没有指定JDK版本,很多文章都是旧版本JD...

HashMap源码分析(一)

//预先了解HashMap中元素的是以Entry的形式存在的,他是一个key value的封装类HashMap中的逻辑结构:链表数组??可以把他看做是一个数组,数组的每一个元素,可以是空,可以是单个元...

HashMap源码分析(基于JDK8)

HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable...

《Java源码分析》:HashMap

《Java源码分析》:HashMap 看过很多次HashMap的源码了,但是,每次都没有做记录,因此,每次记忆都不太深,今天在看别人博客时提到Hashtable是线程安全的,Hashtable中...
  • Solar24
  • Solar24
  • 2017年12月01日 16:03
  • 6

java核心之集合框架——HashMap源码分析

——每天的寥寥几笔,加持下去,将会是一份沉甸甸的积累。

HashMap底层源码分析

值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。Map map = C...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:HashMap源码分析
举报原因:
原因补充:

(最多只允许输入30个字)