浅析hashmap原理

1,070 阅读5分钟

这是我参与更文挑战的第3天,活动详情查看:更文挑战

HashMap概念

HashMap是基于哈希表的Map接口的非同步实现。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。数组被分为一个个桶(bucket),每个桶存储有一个或多个Entry对象,每个Entry对象包含三部分key(键)、value(值),next(指向下一个Entry),通过哈希值决定了Entry对象在这个数组的寻址;哈希值相同的Entry对象(键值对),则以链表形式存储。如果链表大小超过树形转换的阈(TREEIFY_THRESHOLD= 8),链表就会被改造为树形结构。对于HashMap中的每个key,首先通过哈希算法计算出一个哈希值,这个哈希值就代表了在桶里里的编号,而桶本身实际上是用数组来实现的,所以把这个数值模上数组的长度得到它在数组的index,就这样把它放在了数组里。

再看一下数组、链表的优缺点。

数组:数组删除、插入性能不佳,寻址性能极优

链表:链表查询性能不佳,删除、插入性能极优 数组的优缺点取决于它在内存中存储的模式,也就是直接使用顺序存储或链式存储导致的。无论是数组还是链表,都有明显的缺点。

散列表:散列表是一个根据key来访问value的存储结构,HashMap中实现的散列表是一个链表类型的数组,即数组+链表,用来存储key-value数据对。

HashMap原理与存储

hashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。基本原理:先声明一个下标范围比较大的数组来存储元素。另外设计一个哈希函数(也叫做散列函数)来获得每一个元素的Key(关键字)的函数值(即数组下标,hash值)相对应,数组存储的元素是一个Entry类,这个类有三个数据域,key、value(键值对),next(指向下一个Entry)。

举例:第一个键值对A1进来。通过计算其key的hash得到的index=0。存储为:Entry[0] = A1。 第二个键值对A2,通过计算其index也等于0, HashMap会将A2.next =A1,Entry[0] =A2, 第三个键值对 A3,index也等于0,那么A3.next = A2,Entry[0] =A3;可以看到index=0的地方事实上存取了A1,A2,A3三个键值对,它们通过next这个属性链接在一起。这就是桶。对于不同的元素,可能计算出了相同的函数值,这样就产生了“冲突”,这就需要解决冲突,“直接定址”与“解决冲突”是哈希表的两大特点。而HashMap中的链表主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。对于性能的影响,HashMap中的链表出现越少越好。

负载因子与扩容

//实际存储的key-value键值对的个数
transient int size;
//阈值,当table == {}时,该值为初始容量(初始容量默认为16);
int threshold;
//负载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;

默认容量:HashMap中数组的长度如果不指定,则默认为16 2的n次幂:HashMap在new的时候可以指定数组长度,但不管如何指定,实际长度一定是2的n次幂(16、32、64、128等),举几个例子,指定长度为17或29,实际长度为32,指定为35、57或63,实际长度为64,以此类推。 扩容:随着散列表存储节点的不断增加,散列表中数组的长度也应该增加,为了保证2的n次幂特性,每次扩容都是当前长度*2, 扩容的方法是,新建一个两倍的数组,然后遍历散列表中的所有节点,重新计算下标放入新数组。 负载因子:由于散列存储下标具有不确定性,在数组即将被占满的时候,后续添加会发生大量冲突,为了避免,需要使数组在即将被占满前就扩容,而不是等待数组被占满。负载因子决定具体何时扩容。其默认值是0.75,可以在调用构造器的时候指定。0.75的意思是,在调用put方法时,算上被put的节点,如果当前数组被占用达到75%则进行扩容。在JDK 1.8进行了优化,红黑树的引入被用于替换链表,上文说到,如果冲突过多,会导致链表过长,降低查询性能,均匀的hash函数能有效的缓解冲突过多,但是并不能完全避免。所以在往链表后追加节点时,如果发现链表长度达到8,就会将链表转为红黑树。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }