HashMap源码及原理解析

1、HashMap简介

HashMap提供所有可选的Map操作,并允许使用 null 值和 null 键,是线程不安全的。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

HashMap的实例有两个参数影响其性能:初始容量 和加载因子。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目(或者说元素)数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。

默认的加载因子是0.75,是寻求时间复杂度和空间复杂度的平衡。加载因子过高,空间利用率高,但是会增加查询成本,在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点(后面会详细解释)。

另外,HashMap实现了实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。

2、核心源码分析

以下是JDK1.6的源代码:

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{
     //默认的初始容量(初始数组的长度)是16,且必须是2的整数次幂,
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    //数组的长度范围是[0,2的30次方], 小于0会抛异常,大于MAXIMUM_CAPACITY会被它覆盖。
    static final int MAXIMUM_CAPACITY = 1 << 30;

    //默认的加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //存放数据的Entry数组,实际容量必须是2的幂
     //每个Entry元素其实是一个链表
    transient Entry[] table;

    //HashMap中已经存放数据的个数,并非一定是数组的长度
    transient int size;

     //HashMap的阈值,如果size>threshold(threshold = 容量 * 加载因子)则HashMap需要rehash
    int threshold;

    //加载因子
    final float loadFactor;

    //HashMap被改变的次数
    transient volatile int modCount;

}

我们在看看Entry的结构,只截取了重要的部分

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

        /**
         * 创建新的Entry,并让其next指针指向n
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
          //获得key
        public final K getKey() {
            return key;
        }
          //获得value
        public final V getValue() {
            return value;
        }
          //设置value,并返回原来得value
        public final V setValue(V newValue) {
         V oldValue = value;
            value = newValue;
            return oldValue;
        }

          //比较两个Entry是否相等
          //首先,判断其key是否相等,然后再判断value是否相等,
          //只有key和value都相等,两个entry才相等
        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 (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }

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

        //没做任何事情
        void recordAccess(HashMap<K,V> m) {
        }

        //没做任何事情
        void recordRemoval(HashMap<K,V> m) {
        }
    }

下图是HashMap的数据结构图,当两个entry的key经过算法获得的index相同,但value不同时,就采用链表解决冲突。

2.1 put方法

现在我们来看看put方法的源码:

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<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;
    }

private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

首先,我们看看put的流程图(建议与文字说明对照阅读):

详细的流程如下:

1、检测key是否为null,如果是null,会被存放到以table[0]为头节点的链表中(null的hash值总是0),因为存在冲突,table[0]处可能已经有了别的元素。

2、调用key的hashCode()得到哈希值,然后再调用hash()重新计算哈希值,再通过indexFor()得到该key所对应的index,下面来看下hash()和indexFor()的源码:

    //求hash值的方法,重新计算hash值
    static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }   

    // 返回h在数组中的索引值,这里用&代替取模,旨在提升效率
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

hash()就不讨论了,这里我们仔细分析下indexFor(),细心的同学会发现传递过来得length是容量的大小,而容量的大小是强制要求为2的幂次方。这样就保证length-1是2^n-1,其二进制表示为1111(N个1),任何一个数字与length-1进行与运算(&)的结果必定是分散在0到length-1之间。如此,我们才能保证已保存的entry是分布均匀的,空间是足够被利用的。例如


h


length – 1


二进制


h&length-1


0(0000)


1111


0000


0


1(0001)


1111


0001


1


12(1100)


1111


1100


12


16(10000)


1111


0000


0


20(10100)


1111


0100


4

在length=2^n时,通过逻辑与运算(&),代替取模运算,有公式h & (length-1) == h % (length-1),等值而不等效。如果length不为2的幂,那么length-1的二进制表示中至少有1个0(假如在个位上),与运算的最后一位永远是0,不可能为1,“index =奇数”的空间永远得不到利用。这就是容量大小必须为2的整数次幂的原因。

3、 如果发现key已经存在,则覆盖原来的值,返回旧值。

注意:HashMap中全部是通过equels()来判断两个对象(key和value)是否相等。在判断key是否存在时,这里是有加速优化的,首先判断key的hash值是否相等,在相等的前提下再去判断key是否相等。原因:enrty中已经保存了hash,而key的hash值只用计算一次,并且hash值不相等的两个对象的值一定不相等,所以先判断hash值是否相等,比直接判断key.equals(k)来的快。

4、如果没有就调用addEntry将新元素插入所在链表的头节点处。另外,addEntry也还有很多需要注意的地方,其源码如下:

//添加新Entry元素
void addEntry(int hash, K key, V value, int bucketIndex) {
     Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold) //如果size>阈值,则将数组长度扩大成原来的两倍
            resize(2 * table.length);
    }

//调整大小
void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

//转移Entry[]数组
void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

我们发现当size>threshold(阈值)时,需要将数组长度扩大成原来的两倍,是调用resize()来实现的,而resize()又是通过调用transfer()来实现。transfer()中做了两件事情:一件是重新计算元素的index,一件是将其拷贝至新数组,显然这是非常耗时的。所以,我们在使用HashMap时,最好预先估算待存储元素的个数,避免resize的发生,这样有助于提高HashMap的性能。

2.2 get()

当我们了解了put的原理后,再来看看get的源码及其原理:

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

  private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

1、首先判断key是否为null,如果是则遍历以table[0]为头节点的链表,如果e.key==null,则返回其value;

2、再次通过hash()重复计算key的哈希值,并通过indexFor来计算index值,然后遍历table[index]为头节点的链表,找到value;

好了,到此我们已经了解了HashMap的put和get,但是还有一个问题我们没有解释,为什加载因子过高,虽然减少了空间开销,但同时也增加了查询成本?因为,无论Hash函数如何设计,所有的Hash表理论上都不可能避免冲突。当table中快填满时(加载因子大,已存在的元素多),再填入新的元素,冲突的概率会增大。当put时产生冲突(用一个链表解决冲突),get和下一次put就有可能需要去遍历链表,查询成本自然就高了。理想情况是没有链表,所有的元素均存在table上,因此,要减少冲突的机会。

时间: 2024-10-15 12:50:56

HashMap源码及原理解析的相关文章

【转】Java HashMap 源码解析(好文章)

- .fluid-width-video-wrapper { width: 100%; position: relative; padding: 0; } .fluid-width-video-wrapper iframe, .fluid-width-video-wrapper object, .fluid-width-video-wrapper embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } [

Java中的容器(集合)之HashMap源码解析

1.HashMap源码解析(JDK8) 基础原理: 对比上一篇<Java中的容器(集合)之ArrayList源码解析>而言,本篇只解析HashMap常用的核心方法的源码. HashMap是一个以键值对存储的容器. hashMap底层实现为数组+链表+红黑树(链表超过8时转为红黑树,JDK7为数组+链表). HashMap会根据key的hashCode得到对应的hash值,再去数组中找寻对应的数组位置(下标). hash方法如下: static final int hash(Object key

HashMap 源码详细解析 (JDK1.8)

概要 HashMap 最早出现在 JDK 1.2 中,底层基于散列算法实现.HashMap 允许 null 键和 null 值,在计算哈键的哈希值时,null 键哈希值为 0.HashMap 并不保证键值对的顺序,这意味着在进行某些操作后,键值对的顺序可能会发生变化.另外,需要注意的是,HashMap 是非线程安全类,在多线程环境下可能会存在问题. HashMap 底层是基于散列算法实现,散列算法分为散列再探测和拉链式.HashMap 则使用了拉链式的散列算法,并在 JDK 1.8 中引入了红黑

HashMap 源码解析

HashMap简介: HashMap在日常的开发中应用的非常之广泛,它是基于Hash表,实现了Map接口,以键值对(key-value)形式进行数据存储,HashMap在数据结构上使用的是数组+链表.允许null键和null值,不保证键值对的顺序. HashMap检索数据的大致流程: 当我们使用HashMap搜索key所对应的value时,HashMap会根据Hash算法对key进行计算,得到一个key的hash值,再根据hash值算出该key在数组中存储的位置index,然后获取数组在inde

【转】Android 带你从源码的角度解析Scroller的滚动实现原理

今天给大家讲解的是Scroller类的滚动实现原理,可能很多朋友不太了解该类是用来干嘛的,但是研究Launcher的朋友应该对他很熟悉,Scroller类是滚动的一个封装类,可以实现View的平滑滚动效果,什么是实现View的平滑滚动效果呢,举个简单的例子,一个View从在我们指定的时间内从一个位置滚动到另外一个位置,我们利用Scroller类可以实现匀速滚动,可以先加速后减速,可以先减速后加速等等效果,而不是瞬间的移动的效果,所以Scroller可以帮我们实现很多滑动的效果. 在介绍Scrol

[java源码解析]对HashMap源码的分析(二)

上文我们讲了HashMap那骚骚的逻辑结构,这一篇我们来吹吹它的实现思想,也就是算法层面.有兴趣看下或者回顾上一篇HashMap逻辑层面的,可以看下HashMap源码解析(一).使用了哈希表得"拉链法". 我打算按这个顺序来讲HashMap:几个关键属性 -> 构造方法-> 存取元素方法 ->解决hash冲突方法->HashMap扩容问题. 4个关键属性: /** *HashMap的存储大小 */ transient int size; /** * HashMa

【Java深入研究】9、HashMap源码解析(jdk 1.8)

一.HashMap概述 HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现.与HashTable主要区别为不支持同步和允许null作为key和value.由于HashMap不是线程安全的,如果想要线程安全,可以使用ConcurrentHashMap代替. 二.HashMap数据结构 HashMap的底层是哈希数组,数组元素为Entry.HashMap通过key的hashCode来计算hash值,当hashCode相同时,通过"拉链法"解决冲突 相比于之前的版本,jd

HashMap源码分析(JDK1.8)- 你该知道的都在这里了

我的csdn博客地址: http://blog.csdn.net/brycegao321 HashMap是Java和Android程序员的基本功, JDK1.8对HashMap进行了优化, 你真正理解它了吗? 考虑如下问题: 1.哈希基本原理?(答:散列表.hash碰撞.链表.红黑树) 2.hashmap查询的时间复杂度, 影响因素和原理? (答:最好O(1),最差O(n), 如果是红黑O(logN)) 3.resize如何实现的, 记住已经没有rehash了!!!(答:拉链entry根据高位b

Java集合---HashMap源码剖析

无论是在平时的练习还是项目当中,HashMap用的是非常的广,真可谓无处不在.平时用的时候只知道HashMap是用来存储键值对的,却不知道它的底层是如何实现的. 一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 值得注意的是HashMap不是线程安全的,如果