HashMap的源码实现

1 HashMap概述

本文描述的是JDK1.7

在了解HashMap之前,我们来了简单的了解一下其他数据结构。

我们知道,数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中。

  1. 数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)
  2. 线性链表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)
  3. 二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。
  4. 哈希表:相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1),接下来我们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的。

大概了解了上述知识后,我们来讲解HashMap。

HashMap是我们最常用的Map集合类,底层实现是哈希表,检索数据可达到O(1)时间复杂度。

1.1哈希寻址

哈希表是一种逻辑数据结构的底层是通过数组实现的。

看下图:

由上图可知,每一个存入HashMap的key-velue,key都会经过哈希算法,计算出一个数字index,然后将value存放在数组arr[index]的元素上。由此可见,每次取值也只需要通过key算出index,去arr[index]处读取值即可。这样的存值和读取每一次的步骤几乎是一个差不多的常数,所以时间复杂度为0(1)。

观察上面的实现步骤我们可以发现,哈希算法才是整个取值和读值的重点,我们无法保证不同的key不会算出一样的index,如果算出一样的index,一个数组元素里如何存放另两个值?这种现像,我们叫做哈希碰撞,也叫哈希冲突

1.2哈希冲突

解决哈希冲突的方法很多: 1 开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),2 再散列函数法,3 链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

其实HashMap底层的数组每个元素是一个链表,如果index冲突后,直接逐个存放到链表后。

如下图:

以上就是HashMap的大概实现逻辑。

2 HashMap底层解析

2.1 HashMap当中的常量和变量

HashMap中有几个常量和变量还有概念要先理解一下。

MAXIMUM_CAPACITY = 1<<30=2^30:hashMap允许创建的最大长度,即底层数组的长度。

EFAULT_LOAD_FACTOR = 0.75:默认加载因子。

DEFAULT_INITIAL_CAPACITY = 16:默认HashMap的初始化长度。

size:HashMap中存放的key-value的数目。

modCount:修改HashMap的次数,每次增删改的时候都会+1。

threshold:容量上限 ,用容量*负载因子求得,当size>threadshold时,HashMap会扩容。

2.2 HashMap的构造方法

1 public HashMap(){this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);}

2 public HashMap(int initialCapacity){this(initialCapacity, DEFAULT_LOAD_FACTOR);}

3 public HashMap(Map<? extends K, ? extends V> m) {

    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    putAllForCreate(m);
}

4 public HashMap(int initialCapacity, float loadFactor) {......}

由上代码可知,HashMap提供四个构造方法。1 2 3 构造函数最后都是调用了4的构造函数,实例化HashMap'主要细节全部都在4构造函数里,接下来让我们来详细看看。

public HashMap(int initialCapacity, float loadFactor) {
        /*容量不能小于0*/
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        /*输入的容量大于最大容量,只用最大容量当创建HashMap的容量*/
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        /*负载因子不能小于0,且不能为NaN,
        NaN是浮点型一个无法表达的值,例如负数的平方根,0数除以0的时候都得NaN,非0除以0是得到无限大Infinity*/
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        /*算出最接近initialCapacity的2的倍数的容量*/
        int capacity = 1; //创建的真实容量
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        /*在最大容量和 指定容量*负载因子 里挑最小值*/
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//容量上限
        /*创建指定容量的底层数组*/
        table = new Entry[capacity];
        /*这个参数的作用是在算哈希时,决定字符串是否使用另外一种hash算法*/
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();//子类会重写该方法,给子类留的入口,这里不研究这个
    }

流程图如下:

上诉代码需要注意的是下面的代码

int capacity = 1; //创建的真实容量
        while (capacity < initialCapacity)
            capacity <<= 1;

这里决定了不管我们在调用构造方法时传入什么initialCapacity,都会被转换成2^n值capacity ,并求出最接近initialCapacity的2^n,且大于initialCapacity的2^n。

这中种做法是为了让HashMap'底层数组的长度总是2^n,为后面的求数组小标做准备。

负载因子的作用

由上可知上限容量threshold=真实容量*负载因子;

HashMap的哈希表中的哈希碰撞经常发生时,会造成链表被拉长,检索会变慢。当底层数组越大时,哈希碰撞发生的可能性越小,反之越大,即常说的用空间去换时间。

当我们往HashMap中存的值size>threshold时,HashMap底层数组会扩容,变成原来的两倍长度,并将旧数组中存的值重新哈希,放到新的数组中。

负载因子就是控制用空间换时间的程度。负载因子越大,即扩容发生可能性越低,底层数组会用教少的数组存值,哈希碰撞的可能性会更高,检索速度肯定更慢;

反之负载因子越小,扩容发生的可能性越高,底层的数组会用较多的数组存值,哈希碰撞的可能性越低,检索速度自然快。

举例:负载因子a=0.5,b=0.8; 数组长度capacity=16.存30个值,

capacity=16 上限容量 扩容次数 扩容后的数组长度
a=0.5 8 2次 64
b=1 16 1次 32

由上表可以看到,最终负载因子高的HashMap会用更小的数组存更多的数据,从而造成检索变慢。

2.3 HashMap的put()方法

HashMap通过put(Object key,Object value)方法插入key-value。

public V put(K key, V value) {
        /*键如果为空*/
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//求出hash值
        int i = indexFor(hash, table.length);//算出的数组下标
        /*使用数组下标找到链表,遍历链表,求出是否有一样key的结点,有就替换值,没有就往下走,去添加结点*/
        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;
    }

看代码可知,HashMap通过哈希算法求得数组下表分为两步:

第一步:使用int hash = hash(key);通过key求哈希值

第二步:使用int index = indexFor(hash,table.length);用hash值求数组小标index

hash()算法会进行一堆移位运算。代码如下

final int hash(Object k) {
        int h = 0;
        /*该值为true,且key类型为字符串,key采用指定的方法求哈希,据说这样可以减少哈希碰撞*/
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }
        h ^= k.hashCode();
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

为什么算出的数组小标不会超出数组的长度呢?

看indexFor()方法代码:

static int indexFor(int h, int length) {
    return h & (length-1);//长度-1是为了,让二进制表示时,最后一位为1,这样进行与运算的时候减少哈希冲突。
}

通过indexFor()方法代码可以知道,通过与(&)运算,保证求得的值永远小于length。

例子:用1010和1101进行与运算和或运算。

  第3位 第2位 第1位 第0位
  1 0 1 0
  1 1 0 1
与(&) 1 0 0 0
或(|) 1 1 1 1

看结果可知,与运算使得每一位值都不会大于当前值,所以总数最后也不会大于当前值。而或运算是可能会大于当前值的!

因为数组扩容都是偶数,即长度也为偶数,length-1后,肯定会变成奇数。强制使用奇数的目的是将length-1换算成二进制时,最后一位会为1,1做与(&)运算时,可以降低哈希冲突的概率。

 

做与运算 0 1
0001 0000 0001
0000 0000 0000

看上表可以知道,末尾为1时出现相同数据的概率比末尾为0时高!
 

看一下总的put()的流程图

由上图可知,当HashMap存放key-value的数量size > 上限容量threadShold时,会发生扩容,在扩容时发现此时的HashMap底层的数组长度capacity已达到最大值时,将不会发生扩容操作,只是将上限容量值设置为capacity的最大值,然后直接返回。所以HashMap的底层数组长度永远都不会超过规定的最大值2^30。只要你的内存够大,可以存放无限的key-value。

HashMap的get(Object key);基本逻辑就没什么特殊的地方了,只是简单的那key算出hash,用hash算出index,去底层数组arr[index],读取该元素中存的链表,遍历链表。找到链表的中key与之相等的结点,返回结点的value。

最后我们来看一下底层链表结点的数据结构:

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

存的是key,value,算出来的hash值以及下一个对象next。

hash值是用来当扩容时,再调用indexFor()求得数组下标!

2.4 Holder

HashMap中还有一个内部静态类Holder,该类的作用是保存在VM(虚拟机)加载后才能初始化的值。具体各种作用,没有深入研究。

private static class Holder {

            // Unsafe mechanics
        /**
         * 不安全的工具
         */
        static final sun.misc.Unsafe UNSAFE;

        /**
         * 我们必须在readObject()方法中设置“final”散种子字段的偏移量。
         */
        static final long HASHSEED_OFFSET;

        /**
         * 表容量超过此值可切换使用其他散列
         */
        static final int ALTERNATIVE_HASHING_THRESHOLD;

        static {
            //获取系统变量jdk.map.althashing.threshold
            String altThreshold = java.security.AccessController.doPrivileged(
                new sun.security.action.GetPropertyAction(
                    "jdk.map.althashing.threshold"));

            int threshold;

            try {
                threshold = (null != altThreshold)
                        ? Integer.parseInt(altThreshold)
                        : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;

                // jdk.map.althashing.threshold系统变量默认为-1,如果为-1,则将阈值设为Integer.MAX_VALUE
                // disable alternative hashing if -1
                /*禁用替代哈希*/
                if (threshold == -1) {
                    threshold = Integer.MAX_VALUE;
                }
                /*阀值要大于0*/
                if (threshold < 0) {
                    throw new IllegalArgumentException("value must be positive integer.");
                }
            } catch(IllegalArgumentException failed) {
                throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
            }
            ALTERNATIVE_HASHING_THRESHOLD = threshold;

            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                HASHSEED_OFFSET = UNSAFE.objectFieldOffset(
                    HashMap.class.getDeclaredField("hashSeed"));
            } catch (NoSuchFieldException | SecurityException e) {
                throw new Error("Failed to record hashSeed offset", e);
            }
        }
    }

第一次完整的开一份源码,描述不当或理解有误之处请提出,让我多学习学习。

参考博客:https://www.cnblogs.com/chengxiao/p/6059914.html

猜你喜欢

转载自blog.csdn.net/qq_32844875/article/details/82702293