java集合系列(二)——HashMap

今天我看了一下HashMap,作为java用的最多的集合,我们有必要去了解一下其底层的实现原理。

1.hashMap概述

hashMap底层是数组+ 链表的形式,因此其具有查询快,插入删除也快的特点。它是基于哈希表的 Map 接口的实现。其是以key-value的形式存在,也就是键值对

 public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable

由其定义来看,实现了Cloneable,Serializable接口,继承了AbstractMap接口
因此它是可以克隆,可以被序列化的

2.hashMap构造函数

这里构造具有一个默认初始容量(16)和负载因子(0.75)

  • 初始容量:其中容量表示哈希表中的数量,初始容量是创建哈希表时的容量
  • 负载因子:衡量其容量自动增加之前可以达到多满的一种尺度,即散列表的空间的使用程度
  • 负载因子越大表示散列表的装填程度越高,反之愈小
  • 负载因子越大,空间利用率越高,同时查找效率就越低

3.HashMap的数据结构

HashMap底层实现是数组,只是数组的每一项都是一条链。其中参数initialCapacity就代表了该数组的长度。下面为HashMap构造函数的源码:

public HashMap(int initialCapacity, float loadFactor) {
        //初始容量不能<0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: "
                    + initialCapacity);
        //初始容量不能 > 最大容量值,HashMap的最大容量值为2^30
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //负载因子不能 < 0
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: "
                    + loadFactor);

        // 计算出大于 initialCapacity 的最小的 2 的 n 次方值。
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        //设置HashMap的容量极限,当HashMap的容量达到该极限时就会进行扩容操作
        threshold = (int) (capacity * loadFactor);

        //初始化table数组(关键)
        table = new Entry[capacity];
        init();
    }
}

总结:初始容量不能 < 0 不能 > 2^30,负载因子不能< 0

  • 可以看出初始化的话HashMap 会初始化一个Entry数组,其元素为Entry 下面我们看Entry这个内部类
static class Entry implements Map.Entry {
        final K key;
        V value;
        //指向下一个Entry
        Entry next;
        final int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        .......
    }

其中Entry为HashMap的内部类,它包含了键key、值value、下一个节点next,以及hash值,这是非常重要的,正是由于Entry才构成了table数组的项为链表。

4.HashMap的快速存储put(Key K,Value V)

public V put(K key, V value) {
        //当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因
        if (key == null)
            return putForNullKey(value);
        //计算key的hash值
        int hash = hash(key.hashCode());                  ------(1)
        //计算key hash 值在 table 数组中的位置
        int i = indexFor(hash, table.length);             ------(2)
        //从i出开始迭代 e,找到 key 保存的位置
        for (Entry e = table[i]; e != null; e = e.next) {
            Object k;
            //判断该条链上是否有hash值相同的(key相同)
            //若存在相同,则直接覆盖value,返回旧value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;    //旧值 = 新值
                e.value = value;
                e.recordAccess(this);
                return oldValue;     //返回旧值
            }
        }
        //修改次数增加1
        modCount++;
        //将key、value添加至i位置处
        addEntry(hash, key, value, i);
        return null;
    }

4.1分析

源码我们可以知道put方法的过程为:
1.判断是否为null,是则接调用putForNullKey方法,不是则计算hash
2.然后根据hash值搜索在table数组中的索引位置
3.如果table数组在该位置处有元素,则通过比较是否存在相同的key
4.若存在,则覆盖掉之前的value,否则将该元素保存在链表头

4.2迭代处

  • 先看迭代处。此处迭代原因就是为了防止存在相同的key值,若发现两个hash值(key)相同时,HashMap的处理方式是用新value替换旧value,这里并没有处理key,这就解释了HashMap中没有两个相同的key。
  • 在看(1)、(2)处。这里是HashMap的精华所在。首先是hash方法,该方法为一个纯粹的数学计算,就是计算h的hash值。
  • 以上目的就是为了减少Hash碰撞,尽可能均匀分布在hash表中。

4.3HashMap具体实现装key-value

void addEntry(int hash, K key, V value, int bucketIndex) {
        //获取bucketIndex处的Entry也就是hash计算出来的位置
        Entry e = table[bucketIndex];
        //将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry指向原来的 Entry,也就是Entry.next = e
        table[bucketIndex] = new Entry(hash, key, value, e);
        //若HashMap中元素的个数超过极限了,则容量扩大两倍
        if (size++ >= threshold)
            resize(2 * table.length);
    }
}
  • 链表的产生
  • 这是一个非常优雅的设计。系统总是将新的Entry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,形成一条Entry链,但是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。
  • 扩容

随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。

该临界点在当HashMap中元素的数量等于table数组长度*加载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

你可能感兴趣的:(java集合系列(二)——HashMap)