hashmap的底层原理

HashMap

一、HashMap是什么?

hashmap是基于哈希表的map接口的实现,允许使用null值和null键

二、哈希表是什么?

在讨论哈希表之前,先要了解各种数据结构:数组、链表、二叉树、哈希表、红黑树、…

2.1 各种数据结构

2.1.1 数组

采用一段连续的存储单元来存储数据,对于指定下标的查找,时间复杂度(O是描述算法的运行时间与输入数据个数之间的关系)为O(1),通过给定的值来查找,就需要遍历整个数组,时间复杂度为O(n),其中无序数组的插入是直接插入到末尾,所以其时间复杂度为O(1);对于有序数组来说,可通过二分查找,将定值查找的时间复杂度降为O(logn),但删除和插入的时间复杂度为O(n),因为涉及了数组元素的移动,两者的时间复杂度相加为O(n)。

2.1.2 链表

链表中每个数据的存储都是一个节点,一个节点内包括数据域和指针域,数据域用来存储该节点的数据,而指针域用来存储指向下一个节点的地址。
链表的插入和删除(找到指定操作位置的时间复杂度为O(1)
其时间复杂度为O(n),但是链表的查询需要遍历整个链表,其时间复杂度为O(n)。

2.1.3 二叉树

对一棵相对平衡的二叉树(平衡树),其查找、删除和插入的时间复杂度都为O(logn),但最坏的情况下,二叉树所有的时间复杂度都为O(n)。

2.1.4 哈希表

哈希表的时间复杂度

哈希表的插入、删除和查找的时间复杂度都为O(1)。
相比以上几种数据结构来说,哈希表的效率很高,不考虑哈希冲突的情况下,时间复杂度都为O(1)。

哈希函数

数据结构的物理存储结构只有两种:顺序存储结构链式存储结构。在数组中通过下标查找某个元素时,只要通过一次定位就能达到,而哈希表的主干就是数组
我们要查找某个元素,先要通过关键字查询到对应的下标位置,
所以引入了哈希函数,根据不同的情况可以设计不同的哈希函数,不一定要拘泥于某种哈希函数。
i n d e x = f ( K e y w o r d s ) index = f(Keywords) index=f(Keywords)
其中index代表了数组的下标,而Keywords则是查询的关键字。
哈希函数设计的好坏直接影响了数据查找和插入的效率。
在这里插入图片描述

哈希冲突

哈希冲突就是对某个元素进行哈希运算时,得到的存储地址已经被其他元素所占用了。所以哈希函数的设计至关重要,好的哈希函数能够使计算简单和散列地址分布均匀,因为数组是一块固定长度的内存空间,哈希函数设计得再好也不可能没有哈希冲突,hashmap中采用了链地址法来解决哈希冲突,即数组+链表的方式。

三、HashMap的数据结构

在这里插入图片描述
你可能注意到:在HashMap数组的位置0存的数据的key为null(当然位置0的key值也可以不为null,它是由hash决定的),实际上当key=null,hash=0,所以key=null时put方法是存到数组的第0位的。
HashMap底层采用了数组+链表的数据结构,这样操作起来的效率高,速度快。链表是为了解决哈希冲突的问题,链表在插入数据时,先要遍历整个链表,如果链表存在该key值,就覆盖掉,否则就新增,插入的时间复杂度为O(n),在查找时,需要遍历整个链表,其时间复杂度为O(n)。因此,链表不宜过长,否则会影响查找速率。

存储数据的对象节点实际上是实现了Map.Entry对象接口,Node对象包括了四个属性。

// HashMap的主干是Entry<K,V>类型的数组,Node是HashMap的基本组成单元,每一个Node都包括一个键值对。
// 储存对象Node是HashMap中的一个静态内部类
static class Node<K,V> implements Map.Entry<K,V>{
	final int hash;    // 对key的hashcode进行hash运算得到的值
	final K key;
	V value;
	Node<K,V> next;    // 储存指向下一个Node对象的引用,出现哈希冲突时,该数组元素会出现链表结构,会使用next指向链表的下一个元素对象。
	// 全参构造函数
	Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
   	}
   	public final K getKey(){ return key; }
    public final V getValue(){ return value; }
    public final String toString(){ return key + "=" + value; }
	// Node对象的hashCode方法,返回的是键值得hashCode值得异或值
    public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
	// Value的setter方法,将新值赋值给value,并返回旧值
    public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
       }
	// 重写的equals方法
    public final boolean equals(Object o) {
      // 判断两个对象的引用是否相同		
      if (o == this)
      return true;  // 如果相同,返回true
      if (o instanceof Map.Entry) {    // 首先判断对象o是否是Map.Entry的类型
         Map.Entry<?,?> e = (Map.Entry<?,?>)o;   // 将对象o强制转换成Map.Entry类型
         if (Objects.equals(key, e.getKey()) &&  // 将对象o中的key和value和原来比较,如果两者都相同就返回true。
         Objects.equals(value, e.getValue()))
         return true;
      }
      return false;
    }
}

hash方法,如下图所示:

static final int hash(Object key){
	 int h;
	 // key的hashcode值异或key的hashcode值右移16位的值得到最终的hash值
	 // 这样是为了让高位也能参与hash运算,让hash值更加分布的更加均匀
	 return (key == null) ? 0 : (h = key.hashcode()) ^ (h >>> 16);
}

四、HashMap的初始化操作

4.1 为什么在jdk1.8中要引入红黑树?

原因:虽然有链表可以解决hash冲突,但是随着数据的增长,hash冲突越来越频繁,链表长度会变得很长,HashMap中的get方法在查找元素时是通过遍历链表查找对应的数据,所以链表数据过长会导致get方法的效率越来越低。
红黑树:在JDK1.8时,引入了红黑树的结构,当链表的长度大于等于8时,链表会自动转换为红黑树,此时查询链表的时间复杂度为O(logn),提高了效率。
注意:当红黑树中的元素个数 <= 6时,红黑树会转回链表结构,这个临界值为什么不是7呢?这是为了避免在使用时,在8这个临界值上一会删一个变成链表,一会加一个变成红黑树,浪费系统资源,损耗性能。所以让两个数之间隔了一个数,避免以上描述情况的发生。

4.2 HashMap中的各个参数

HashMap在通过new关键字进行实例化时,没有对HashMap进行初始化,仅仅是对各项数据进行了检查。在进行putVal操作时,才对数组进行了初始化操作。

4.2.1 构造方法

构造方法一:该构造方法能够指定初始容量和负载因子,负载因子越大,数组能够存放的元素越多,数据的散列性也就越差,负载因子是可以大于1的。

  public HashMap(int initialCapacity, float loadFactor) {
  		// initialCapacity  初始容量   默认值为16
  		// loadFactor  负载因子  默认值为0.75
        if (initialCapacity < 0)     // 当初始容量小于0时,抛出一个非法参数异常("非法的初始容量:"+initialCapacity)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)   // 当初始容量>最大容量 (1 << 30) ,那么初始容量就等于 1<<30 
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))   // 当loadFactor <= 0 时,抛出非法参数异常
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

无参构造方法:两个参数都采用默认值,initialCapacity = 16 , loadFactory = 0.75
一个参数的构造方法(initialCapacity) 可以指定数组的长度,负载因子默认为0.75.

4.2.2 tableSizeFor()方法:
// 该方法是对n最高位为1的部分,将其后所有的部分全部变成1.
static final int tableSizeFor(int cap) {
		// cap-1是为了返回一个大于等于cap的最小的2的整数次幂
		// 如果不减1,那么当cap=16时,最终返回的值为32,不符合
		// 原来的期望,直接扩大一倍
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        // 这里是令返回值为2的整数次幂
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

HashMap的各种属性:

transient Node<K,V>[] table;   // 由储存数据构成的数组,会将16的静态变量赋值给数组长度table.length
int threshold;						// 允许储存的最大的元素数量,通过table.length*loadFactor得到
final float loadFactor;       // 数据的增长因子,默认为0.75,在进行扩容时用到
int modCount;		// 记录内部结构发生变化的次数,key相同时,覆盖值得操作不会计算入内
int size;           // 实际储存的元素数量
4.2.3 为什么数组长度默认为16?

为了减少hash值得碰撞,需要实现一个尽量均匀分布的hash函数,在HashMap中通过利用key的hashcode值,来进行位运算得到hash值
公式
i n d e x = e . h a s h ( ) ∗ ( n e w C a p − 1 ) index = e.hash()*(newCap-1) index=e.hash()(newCap1)
当newCap=16时,newCap-1=1111;如果hash后4位的值时均匀的,那么位与运算的结果也肯定时均匀的,如果newCap!=2的整数次幂,那么newCap-1 的二进制就不会全为1,这样的话,存在多种后4位不同的数据对应了相同的index,所以就会使得每种index出现的几率不一样,不符合hash均匀分布的原则。所以数组长度必须为2的整数次幂

五、putVal()和get()方法的源码解读

5.1 putVal() 方法

HashMap中的put方法实际上是引用了putVal()方法。

// HashMap中的put方法
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

putVal()方法:

// putVal()方法的参数说明:
// 1.hash(Key):键值的hash值
// 2.key: 键值
// 3.value
// 4.onlyIfAbsent
// 5.evict
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 1 判断数组tab是否为空,如果是就进行扩容数组初始化
        if ((tab = table) == null || (n = tab.length) == 0)
        	// 1.1 将初始化长度16赋值给n
            n = (tab = resize()).length;   
        // 2.1 通过hash算法获取的节点为null, 就新建一个节点对象赋值给当前的位置
        if ((p = tab[i = (n - 1) & hash]) == null)    // 2.1.1 p节点是通过hash算出来的在数组中对应的元素
        	// 2.1.2 创建一个新节点(node的next为null)
            tab[i] = newNode(hash, key, value, null);
        // 2.2 p节点不为null
        else {
        	// 定义一个e节点,和一个键值k
            Node<K,V> e; K k;
            // 2.2.1 判断put的key值与p节点的key值是否想等
            if (p.hash == hash &&  // 注意:先判断hash值是否想等,如果hash值不想等,那么key值必然不相等
                ((k = p.key) == key || (key != null && key.equals(k))))
            // 2.2.1.1 如果key值相等======>就将p节点赋值给定义好的节点e
                e = p;    
            // 2.2.2 p节点如果是红黑树节点的话,就进行红黑树插入
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 2.2.3 p节点是普通的链表节点(且p节点的key与put的key不等)
            else {  
            	// 遍历单链表
                for (int binCount = 0; ; ++binCount) {
                	// 2.2.3.1 将p节点的下一个节点赋值给e,判断下一个节点是否为null
                    if ((e = p.next) == null) {
                    	// 1)如果为空,就将p.next 指向 新节点(hash,key,value,null)
                        p.next = newNode(hash, key, value, null);
                        // 2)如果链表长度>=8时,将链表转换成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 2.2.3.2 判断put的key值是否与e节点的key值想等
                    if (e.hash == hash &&    // 这里证明了put的key == 节点e的key
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 2.2.3.3 以上都不成立时,及时更新p节点(以便下一次的循环)  
                    p = e;
                }
            }
            // 2.2.4 如果存在e节点,就覆盖
            if (e != null) { // existing mapping for key
            	// 2.2.4.1 把e节点的值赋值给oldValue
                V oldValue = e.value;
                // 2.2.4.2 判断是否允许覆盖,并且oldValue是否为空
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                // 2.2.4.3 返回旧值
                return oldValue;
            }
        }
        // 2.3 如果不是覆盖的操作,那么就更改操作次数
        ++modCount;
        // 2.4 当size大于临界值 size: 指的是插入hashmap中键值对的数量  threshold = initialCapacity*loadFactory
        if (++size > threshold)  
        // 2.5 将数组的大小设置为原来的2倍,并将原来数组中的元素放到新数组中
            resize();  
        // 2.6 回调以允许LinkedHashMap后置操作
        afterNodeInsertion(evict);
        // 2.7 put成功,返回null
        return null;
    }

5.2 get方法

5.2.1 get方法
public V get(Object key) {
		// 1.定义一个节点e
        Node<K,V> e;
        // 2.通过hash,key来查找hashmap中的某个节点,如果找到了就返回该节点的值
        return (e = getNode(hash(key), key)) == null ? null : e.value;
}
5.2.2 getNode()方法
final Node<K,V> getNode(int hash, Object key) {
		// 定义node节点数组tab,node节点first、e,tab的长度n,键值k
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 1.给tab赋值table != null && tab.length > 0 && 根据hash得到tab[index] != null
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 1.1 比较key值是否想等
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                // 1.1.1 想等返回first节点
                return first;
            // 1.2 将first下一个节点的引用赋值给节点e != null
            if ((e = first.next) != null) {
            	// 1.2.1 判断first节点是否为红黑树节点
                if (first instanceof TreeNode)
                	// 1.2.2 是的话就用getTreeNode(hash,key)方法返回一个红黑树节点
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 1.2.2 循环,判断节点e的key值 ?= get的key
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 是的话就返回节点e
                        return e;
                } while ((e = e.next) != null);       // 令e = e.next 继续循环,直到找到,或者下一个节点为null为止
            }
        }
        // 未找到节点,返回null
        return null;
    }

六、HashMap的扩容机制

6.1 resize()方法

	// 扩容方法
    final Node<K,V>[] resize() {
    	// 1.定义一个数组保存原来的链表数组
        Node<K,V>[] oldTab = table;
        // 2.获取旧数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 3.获取旧的扩容阀域值
        int oldThr = threshold;
        // 4.定义新的数组长度和扩容阀域值
        int newCap, newThr = 0;
        // 扩容
        // 5.当旧的数组长度大于0时
        if (oldCap > 0) {
        	// 5.1 且 数组长度 大于 1<<32 是
            if (oldCap >= MAXIMUM_CAPACITY) {
            	// 5.1.1 令HashMap中的扩容阀域值=Integer.MAX_VALUE
                threshold = Integer.MAX_VALUE;
                // 5.1.2 同时返回旧数组
                return oldTab;
            }
            // 5.2 旧数组的2倍 < 1<<32 而且 旧数组的长度大于等于16时
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 5.2.1 令新的扩容阀域值变成原来的2倍
                newThr = oldThr << 1; // double threshold
        }
        // 有参的构造方法
        // 情况一:已经有了table数组,正在扩容,数组长度小于16
        // 情况二:使用有参构造HashMap,正在第一次使用put方法
        // 6.旧的扩容阀域值大于0时
        else if (oldThr > 0) // initial capacity was placed in threshold
        	// 6.1 使新的数组长度等于旧的扩容阀域值
            newCap = oldThr;
        // 7.当旧的数组长度和扩容阀域值都为0时
        // 无参构造方法
        else {               // zero initial threshold signifies using defaults
        	// 7.1 定义初始数组长度为16
            newCap = DEFAULT_INITIAL_CAPACITY;
            // 7.2 扩容阀域值为16*0.75=12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 8.新的扩容阀域值等于0时
        if (newThr == 0) {
        	// 8.1 定义ft = 数组长度*0.75
            float ft = (float)newCap * loadFactor;
           	// 8.2 给新的扩容阀域值赋值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        // 9.给HashMap内的扩容阀域值=newThr
        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;
    }

在JDK1.7中,resize时,index需要重新经过一个hash算法得到,JDK1.8对index的获取做了一些调整。
以前确定index是用e.hash&(oldCap-1),是取余重新hash,现在不用了,我在扩容是将数组的长度拓展为原来的2倍,所以元素要么在原位置,要么在移动的2次幂的位置,所以我们只需要看新增的hash值是0还是1,如果是0,那么在原位置没有变化,如果是1那么就在原来的基础上index + oldCap。

声明:hashMap底层原理网络上已有很多资源,我也是从网上东抄西补才写完了这篇博客,有很多地方可能没有理解到位请多多包涵,我会将查询信息的网址放在下文:
链接1:jdk1.7hashMap底层原理分析
链接2:jdk1.8hashMap底层原理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值