彻底理解HashMap中两大核心源码hash及indexFor算法

本篇文章主要讲解和实例演示HashMap两大核心源码:hash算法及indexFor算法,希望大家通过阅读本篇文章后,能够彻底理解这两大源码的方法和作用

一.HashMap的介绍

先简单过一下HashMap的概念,我们都知道Java集合有两大接口Collection和Map,本篇介绍的集合是实现Map接口的HashMap。

特点:我们知道HashMap是用来存储键值对的一种集合,存入元素时,以key和value同时存入HashMap中。

HashMap的作用(有什么好处我们为什么要用它):我之前的文章也介绍过Java中的另外两种集合,ArrayList和LinkedList,这两种集合的数据结构是数组和链表,也分析了这两种集合的优劣,ArrayList集合的查询效率高,以及LinkedList的增删效率高,那么讲到这,我们就在想有没有这样一种集合,既能发挥ArrayList的高效查询效率,又能发挥LinkedList的高效增删?于是就有了HashMap。

ArrayList和LinkedList相关文章: ArrayList及LinkedList.

HashMap的数据结构:数组+链表+红黑树(JDK1.8及以后),HashMap使用了数组和链表的链地址的数据结构,同时发挥高效查询以及高效增删。红黑树的主要作用是解决链表冲突,设计的好处是避免在最极端的情况下冲突的链表变得很长很长,在查询的过程中,效率变得非常慢(这部分的详细内容会在后面进行讲解),不得不说,程序的设计者考虑问题还是非常全面的。

二.HashMap的存储方式

存储的数据类型

前面已经了解HashMap的存储是以key-value的形式存储元素,其方法是使用Map.put(k,v),其中k的数据类型是引用类型,主要原因是,我们在插入元素时需要对key进行去重操作,使用的是hashcode和equals方法(需要重写),而基本数据类型是没有方法的,所以需要使用引用数据类型。

HashMap的存储方式演示: HashMap存储实例演示.

存储方式

上面已经讲过,使用HashMap存储时,以它的key作为键找到对应在数组中的位置,然后插入value。一张图演示HashMap的数据机构:
在这里插入图片描述

数组存放的是key的位置,链表是用来存放出现同一个key时不同的元素的空间。其中Entry中有每一个键值对(key和value)。

可能有人会有疑问:为什么key相同还会有不同的元素对象?

HashMap中的hashcode()

什么是hashcode,为什么HashMap要使用它?
hashcode是一个对象的散列码,主要是用于查找的快捷性,当我们要存放一个元素进HashMap中时,它先检查此元素的key是否重复,我们使用重写过的hashcode来计算key的散列值,将它存放在散列表里(hashcode的重写方式很多),其实hashcode就是计算存放元素的索引,这个索引尽量不出现重复,最好是不同的元素,存放在不同的位置。hashcode就是为了尽量让存进集合中的元素的位置更加分散、少出现元素位置冲突。

如何解决同一个key可能是不同的对象?
我们已经知道,通过计算key的hashcode来找到存放元素的位置,但是这种方式总会出现相同的hashcode值的现象,即,不同的对象有着同样的hashcode。
举例:

public static void main(String[] args){
    String A ="abc";
    Integer B=96354;
    System.out.println("A的hashcode为:"+A.hashCode());   
    System.out.println("B的hashcode为:"+B.hashCode());
    System.out.println(A.equals(B));
}

结果:
在这里插入图片描述

这就出现了不同的对象有着相同的hashcode,所以我们需要再使用重写的equals方法对对象的变量进行比较。

为何需要重写hashcode和equasl方法? hashcode和equals方法的重写.

为了使得hashcode的值更加地散列(降低出现相同hashcode值的概率),由此引出hash算法。

三.HashMap中的hash算法

首先看一下hash算法的源码:

static final int hash(Object key) {      
     int h;       
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

看上去很简单,定义int型变量h,(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)这个分步理解为:
1.如果key的值是null,那么hash算法返回的值为0。
2.如果key不为null,就执行(h = key.hashCode()) ^ (h >>> 16)。
3.计算key的hashcode然后与此hashcode的高16位进行异或运算。

那么为何hashcode要右移16位,且和自己进行异或运算?
在讨论这个问题前我们需要看HashMap中另一个核心方法的源码indexFor()方法(此方法为JDK1.7及以前的方法,JDK1.8已经省略了,但实际的作用还在使用)。

四.HashMap中的indexFor算法(JDK1.7)

先看源码

static int indexFor(int h, int length) {
        return h & (length-1);
    }

以上的方法是HashMap中put方法中计算数组下标值index的算法,简单介绍就是HashMap在插入元素时调用put方法,而put方法又用到indexFor()方法。
JDK1.8使用的indexFor()方法

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 判断node数组已经初始化,根据key的hash找到first node
    if ((tab = table) != null 
        && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        // 判断桶上第一个node的hash和key的hash一致,并判断key的值和first node 的key是否相同,相同就返回第一个Node
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 遍历桶中元素
        if ((e = first.next) != null) {
            // 如果是红黑树,走红黑树遍历查找方式  后文详解
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 链表方式查找Node: 采用do while形式遍历,保证循环至少走一次。 
            do {
                // 依次遍历,和遍历桶顶元素
                if (e.hash == hash && 
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

其中**tab[(n - 1) & hash]**部分和JDK1.7中的indexFor()方法作用一致

那么,为何要使用indexFor()方法呢?
我们来看一下这个方法的具体操作:
return h & (length-1);
找出在HashMap数组中的index,所以 h & (length-1) 就是key的hashcode在数组的位置,这么做有两种好处:

保证不发生数组越界
在HashMap中,数组的长度规定是2的幂次,那么长度转换为二进制为1000……00,1后面偶数个0,如果length-1转换为二进制为01111……111,0后面偶数个1,那么,length-1和h进行&操作,结果的值不会超过数组的大小,防止数组越界(主要作用)。

为何数组长度为2的幂次?
假设此时数组大小为16,length-1位15二进制为1111,假设有两个元素待插入,它们的hashcode为8和9,对应的二进制为1000和1001,分别&操作结果为:
hashcoe为8的元素:
1000
1111&操作结果为:
1000

hashcoe为9的元素:
1001
1111&操作结果为:
1001
发现不同hashcode的元素放入数组的位置不同,符合要求。
而如果数组长度不是2的幂次,那么会是什么结果呢?

假设数组长度为15,length-1为14,二进制为1110
hashcoe为8的元素:
1000
1110&操作结果为:
1000

hashcoe为9的元素:
1001
1110&操作结果为:
1000
发现不同hashcode的元素放在了数组同一个位置,发生hash冲突,两个元素都放在1000为数组idex下标为8的位置,而且,length-1二进制的最后一位为0,那么和任何hashcode&,最后一位都是0,这样,所有末尾为1的位置都用不到,不仅造成hash冲突,还会造成数组空间资源浪费。因此,我们通常规定HashMap数组长度为2的幂次,通过length-1后保证二进制最后位为1.

那么这样是否就解决问题了呢?(为何需要hash算法)
我们再举个例子,假设数组length的长度为8,length-1为7,二进制为111,此时有个元素key的hashcode为100001110110101,进行&运算
100001110110101
000000000000111
进行&运算后结果为: 000000000000101,我们发现,结果只与hashcode的后三位有关,不管前面位数的值是多少,由于数组长度固定,而且数组长度越小,那么使用到的hashcode的位数越少,正常情况是:
length为8,数组下标取决于hashcode后三位
length为16,数组下标取决于hashcode后四位
length为32,数组下标取决于hashcode后五位
length为2的n次方,数组下标取决于hashcode后n位(hashcode的长度一般不超出2的16次方)。

因此,为了让hashcode的后16位更加散列随机,就将hashcode的前16位参与运算,这样就保留了hashcode的前16位的特征,而将hashcode的前16位参与运算,最好的方法就是移动高位的16位。

为什么使用^不用&或者|
经过实验得知&或者|的结果都偏向1或者0,相比^并不均匀,所以,我们使用
^进行运算。

讲到这,我们就发现为何需要hash算法:
1.为了hashcode能够进一步实现散列随机
2.使得hashcode所有部位都能参与运算,实现hashcode的所有特征

HashMap极限容量

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;//将threshold置为Integer.MAX_VALUE
            return;//当HashMap的容量已经是2的31次方的时候,直接返回。
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

通过HashMap的扩容源码可知,当数组容量达到2的31次方时已经不会扩容了,而数组带下为2的幂次,所以可以发现绝大多数情况的数组大小都不会超过2的16次方。

总结:本文主要讲解了HashMap中的两大核心源码:hash算法和indexFor算法,通过讲解,希望大家都能够很好地理解这两大核心源码,最后,希望大家国庆玩得开心,但也不要忘了这段宝贵的学习时间。

别人结婚,我学Java。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值