首页 > 编程语言 >HashMap源码解析

HashMap源码解析

时间:2023-09-25 12:08:16浏览次数:45  
标签:key Node 解析 hash HashMap 源码 数组 null


HashMap简介

HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键,但最多只允许一条记录的键为null。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。

HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的ConcurrentHashMap。

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

接下来,我们从源码开始分析,HashMap的继承结构:

public class HashMap<K,V>  
    extends AbstractMap<K,V>  
    implements Map<K,V>, Cloneable, Serializable

HashMap继承了AbstractMap及实现了Map、Cloneable和Serializable接口。

这里我们还可以回顾下设计模式的精华,AbstractMap也实现了Map接口,为什么HashMap既继承AbstractMap抽象类还需要实现Map接口吗?

从功能上来说:HashMap实现Map是没有任何作用的。

从结构上来说:由于我们一般是针对接口编程,为了维护结构清晰和完整,是需要实现Map接口的。

而HashMap继承AbstractMap的作用为:AbstractMap 提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作。 

HashMap的构造函数

/**
   *  构造HashMap对象,其它的构造函数都是调用此构造函数来实现的
   *  参数说明:
   *  initialCapacity:分配数组的大小,默认大小为16,且只能是2的幂次方
   *  loadFactor:加载因子,作用为:当数组中存储的数据大于了分配空间的总长度*loadFactor之后就进行扩容
    */
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

HashMap共有四个构造方法。构造方法中提到了两个很重要的参数:初始容量和加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中槽的数量(即哈希数组的长度),初始容量是创建哈希表时的容量(从构造函数中可以看出,如果不指明,则默认为16),加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 resize 操作(即扩容)。

下面说下加载因子,如果加载因子越大,对空间的利用更充分,但是查找效率会降低(链表长度会越来越长);如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费。如果我们在构造方法中不指定,则系统默认加载因子为0.75f,这是一个比较理想的值,一般情况下我们是无需修改的。

另外,无论我们指定的容量为多少,构造方法都会将实际容量设为不小于指定容量的2的次方的一个数,且最大值不能超过2的30次方

HashMap的数据结构

在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

想搞清楚HashMap,首先需要知道HashMap是什么,即它的存储结构-字段;其次弄明白它能干什么,即它的功能实现-方法。下面我们针对这两个方面详细展开讲解。

存储结构-字段

从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下如所示。

 

HashMap源码解析_链表

图中,左边一列即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。

这里需要弄明白两个问题:数据底层具体存储的是什么?这样的存储方式有什么优点?看一下源码

/** Node是单向链表。  
 * 它是 “HashMap链式存储法”对应的链表。  
 * 它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o),   				     * hashCode()这些函数 
 */
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;    //用来定位数组索引位置
        final K key;
        V value;
        Node<K,V> next;   //链表的下一个node

        Node(int hash, K key, V value, Node<K,V> next) { ... }
        public final K getKey(){ ... }
        public final V getValue() { ... }
        public final String toString() { ... }
        public final int hashCode() { ... }
        public final V setValue(V newValue) { ... }
        public final boolean equals(Object o) { ... }
}

从源码可知,HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个Node的数组。

Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。上图中的每个黑色圆点就是一个Node对象。HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决问题,Java中HashMap采用了链地址法。链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。

功能实现-方法

HashMap的内部功能实现很多,下面从根据key获取哈希桶数组索引位置、put方法的详细执行、扩容过程三个具有代表性的点深入展开讲解。

1. 确定哈希桶数组索引位置

不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。先看看源码的实现:

方法一:

static final int hash(Object key) {   //jdk1.8 & jdk1.7
     int h;
     // h = key.hashCode() 第一步 取hashCode值
     // h ^ (h >>> 16)  第二步 高位参与运算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

方法二:

static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
     return h & (length-1);  //第三步 取模运算
}

这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。

对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用方法一所计算得到的Hash码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的消耗还是比较大的,在HashMap中是这样做的:调用方法二来计算该对象应该保存在table数组的哪个索引处。

这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。一般我们利用hash码, 计算出在一个数组的索引, 常用方式是”h % length”, 也就是求余的方式。但这种方式效率不高, SUN大师们发现,当length总是2的n次方时,h& (length-1)运算等价于h%length,但是&比%具有更高的效率。

在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

put方法

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
put方法直接调用putVal方法。因此,我们直接看putVal方法即可。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
//tab为空则创建
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//重新开辟一个Node<K,V>的数组
        /*
        根据key的hash值找到要存储的位置,
        如果该位置还没有存储元素,则直接在该位置保存值即可
        */
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            /*
            检查在位置的链表中是否有了该key,
            在下面的代码中,是先检查头结点是否为该key,如果不等于,则在剩余的节点中寻找
            */
            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 {
                /*
                在剩余的节点中寻找key的位置
                将节点(key,value)加到链表的末尾
                */
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
  //链表长度大于8转换为红黑树进行处理
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
 // key已经存在直接覆盖value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果e为空,则说明是添加的新节点,如果e不为空,则说明该key已经存在,只需要更新value
            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;
    }

此方法的思想为:首先根据key得到hashcode,然后根据hashcode得到要存储的位置i=hash&(n-1),其中n为数组的长度。得到存储位置i之后,检查此位置是否已经有元素,如果没有,则直接存储在该位置即可,如果有,则在位置的所有节点中遍历是否含有该key,如果已经有了该key,则更新其value即可,如果没有该key,则在该链表的末尾加入该新节点即可。

扩容机制

上面调用了resize方法来进行扩容,前面提到,在HashMap所有的构造函数中,都没有对数组table分配存储空间。而是将这一步放入到了在put方法中进行table检测,如果为空,则调用resize方法进行扩容(或者说是为了给其开辟空间)。

扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

我们分析下resize的源码

 

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        /**
         *   如果数组table是有长度的,即不是第一次使用,则会进行扩容处理
        */
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
// 没超过最大值,就扩充为原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        /*
        下面的就是table第一次使用
        (第一种是指定了threshold,第二种是什么都没事指定,这个使用哪个构造函数得到HashMap对象有关)
        */
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
// 计算新的resize上限
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //进行拷贝
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

 

 

标签:key,Node,解析,hash,HashMap,源码,数组,null
From: https://blog.51cto.com/u_6947107/7594165

相关文章

  • ArrayList源码解析
    ArrayList是基于List接口,大小可变数组的实现。实现了所有可选列表操作,并允许包括null在内的所有元素。除了实现List接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。下面我们来分析ArrayList的源代码,ArrayList继承体系结构如下:publicclassArrayList<E>extend......
  • Java 8 Lambda 表达式解析
    Lambda表达式,也可称为闭包,它是推动Java8发布的最重要新特性。使用Lambda表达式可以使代码变的更加简洁紧凑。坦白的说,初次看见Lambda表达式瞬间头就大了,为了更好的理解,我们可以把Lambda表达式当作是一种匿名函数(对Java而言这并不完全正确,但现在姑且这么认为),简单地说,就是......
  • HashMap的实现原理
    HashMap的数据结构:底层使用hash表数据结构,即数组和链表或红黑树当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象元素在数组中的下标存储时,如果出现hash值相同的key,此时有两种情况如果key相同,则覆盖原始值如果key不同(出现冲突),则将当前的key-value放入链......
  • 理解并掌握C#的Channel:从使用案例到源码解读(一)
    引言在C#的并发编程中,Channel是一种非常强大的数据结构,用于在生产者和消费者之间进行通信。本文将首先通过一个实际的使用案例,介绍如何在C#中使用Channel,然后深入到Channel的源码中,解析其内部的实现机制。使用案例一:文件遍历和过滤在我们的使用案例中,我们需要遍历一个文件夹及......
  • Java LinkedList与ArrayList源码解析:根本区别和表面区别的详解
    在Java中,LinkedList和ArrayList是两个常见的集合类。它们都实现了List接口,但它们在实现方式上有很大的区别。本篇博客将详细解析LinkedList和ArrayList的源码,解释它们的根本区别和表面区别,并提供详细的代码解释。LinkedList与ArrayList的根本区别:数据结构:LinkedList是基于链表......
  • threejs源码
    剖分管道形状面地表文字水体相交光线三维实体材质运动svg动画输出......
  • HashMap常见面试题
    简介HashMap最早出现在JDK1.2中,底层基于散列算法实现。HashMap允许null键和null值,是非线程安全类,在多线程环境下可能会存在问题。1.8版本的HashMap数据结构:为什么有的是链表有的是红黑树?默认链表长度大于8时转为树结构Node是HhaspMap中的一个静态内部类://Node是单向链表,实现......
  • 新版绿豆视频APP视频免授权源码 V6.6插件版
    简介:新版绿豆视频APP视频免授权源码插件版后端插件开源,可直接反编译修改方便对接苹果cms,自定义DIY页面布局!绿豆影视APP对接苹果cms所有页面皆可通过后端自由定制此版本后端源码+前端是壳(反编译版本)五款个人中心主题自由切换个人中心背景图后台可控后台控制幻灯片背景虚幻支持信......
  • 无法在web.xml或使用此应用程序部署的jar文件中解析绝对uri:[http://java.sun.com/jsp/
    今天解决了一个很早之前的问题!!!无法在web.xml或使用此应用程序部署的jar文件中解析绝对uri:[http://java.sun.com/jsp/jstl/core]之前一直以为是jar包不匹配,但是改了jar包之后连uri都分辨不出来了后来在网上查到是tomcat的问题,将tomcat的conf目录下的catalina.properties的tomc......
  • HashMap常见面试题
    简介HashMap最早出现在JDK1.2中,底层基于散列算法实现。HashMap允许null键和null值,是非线程安全类,在多线程环境下可能会存在问题。1.8版本的HashMap数据结构:为什么有的是链表有的是红黑树?默认链表长度大于8时转为树结构Node是HhaspMap中的一个静态内部类://Node......