HashMap原理学习

Stella981
• 阅读 844

概述

HashMap对于做Java的小伙伴来说太熟悉了。估计你们每天都在使用它。它为什么叫做HashMap?它的内部是怎么实现的呢?为什么我们使用的时候很多情况都是用String作为它的key呢?带着这些疑问让我们来了解HashMap!

HashMap介绍

1、介绍

HashMap是一个用”KEY”-“VALUE”来实现数据存储的类。你可以用一个”key”去存储数据。当你想获得数据的时候,你可以通过”key”去得到数据。所以你可以把HashMap当作一个字典。 那么HashMap的名字从何而来呢?其实HashMap的由来是基于Hasing技术(Hasing),Hasing就是将很大的字符串或者任何对象转换成一个用来代表它们的很小的值,这些更短的值就可以很方便的用来方便索引、加快搜索。

在讲解HashMap的存储过程之前还需要提到一个知识点
我们都知道在Java中每个对象都有一个hashcode()方法用来返回该对象的 hash值。HashMap中将会用到对象的hashcode方法来获取对象的hash值。

2、关系

图1展示了HashMap的类结构关系。

HashMap原理学习

HashMap继承了AbstractMap,并且支持序列化和反序列化。由于实现了Clonable接口,也就支持clone()方法来复制一个对象。今天主要说HashMap的内部实现,这里就不对序列化和clone做讲解了。

3、内部介绍

HashMap原理学习

上面的图很清晰的说明了HashMap内部的实现原理。就好比一个篮子,篮子里装了很多苹果,苹果里包含了自己的信息和另外一个苹果的引用

1、和上图显示的一样,HashMap内部包含了一个Entry类型的数组table, table里的每一个数据都是一个Entry对象。

2、再来看table里面存储的Entry类型,Entry类里包含了hashcode变量,key,value 和另外一个Entry对象。为什么要有一个Entry对象呢?其实如果你看过linkedList的源码,你可能会知道这就是一个链表结构。通过我找到你,你再找到他。不过这里的Entry并不是LinkedList,它是单独为HashMap服务的一个内部单链表结构的类。

3、那么Entry是一个单链表结构的意义又是什么呢?在我们了解了HashMap的存储过程之后,你就会很清楚了,接着让我们来看HashMap怎么工作的。

HashMap的存储过程

下面分析一段代码的HashMap存储过程。(这里只是作为演示的例子,并没有真实的去取到了Hash值,如果你有需要可以通过Debug来得到key的Hash值)

        HashMap hashMap = new HashMap();//line1  
        hashMap.put("one","hello1");//line2  
        hashMap.put("two","hello2");//line3  
        hashMap.put("three","hello3");//line4  
        hashMap.put("four","hello4");//line5  
        hashMap.put("five","hello5");//line6  
        hashMap.put("six","hello6");//line7  
        hashMap.put("seven","hello7");//line8

put操作的伪代码可以表示如下:

public V put(K key, V value){  
    int hash = hash(key);  
    int i = indexFor(hash, table.length);  
    //在table\[i\]的地方添加一个包含hash,key,value信息的Entry类。  
}

下面我们来看上面代码的过程
1、line1创建了一个HashMap,所以我们来看构造函数

/**  
     \* Constructs an empty <tt>HashMap</tt> with the default initial capacity  
     \* (16) and the default load factor (0.75).  
     */  
    public HashMap() {  
        this(DEFAULT\_INITIAL\_CAPACITY, DEFAULT\_LOAD\_FACTOR);  
    }

空构造函数调用了它自己的另一个构造函数,注释说明了构建了一个初始容量的空HashMap,那我们就来看它另外一个构造函数。

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;  
        threshold = initialCapacity;  
        init();  
    }

void init() {  
    }  

上面的代码只是简单的给loadFactor(其实是数组不够用来扩容的)和threshold(内部数组的初始化容量),init()是一个空方法。所以现在数组table还是一个空数组。

 /**  
     \* An empty table instance to share when the table is not inflated.  
     */  
    static final Entry<?,?>\[\] EMPTY_TABLE = {};

    /**  
     \* The table, resized as necessary. Length MUST Always be a power of two.  
     */  
    transient Entry<K,V>\[\] table = (Entry<K,V>\[\]) EMPTY_TABLE;

2、接下来到了line2的地方, hashMap.put(“one”,”hello1”);在这里先提一下put方法源码:

public V put(K key, V value) {  
        if (table == EMPTY_TABLE) {  
            inflateTable(threshold);//如果是空的,加载  
        }  
        if (key == null)  
            return putForNullKey(value);  
        int hash = hash(key);获取hash值  
        int i = indexFor(hash, table.length);生成索引  
        for (Entry<K,V> e = table\[i\]; e != null; e = e.next) {  
            Object k;  
            //遍历已存在的Entry,如果要存入的key和hash值都一样就覆盖。  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }

        modCount++;  
        //添加一个节点  
        addEntry(hash, key, value, i);  
        return null;  
    }

源码很简单,先判断table如果是空的,就初始化数组table,接着如果key是null就单独处理。否则的话就得到key的hash值再生成索引,这里用了indexFor()方法生成索引是因为:hash值一般都很大,是不适合我们的数组的。来看indexFor方法

/**  
     \* Returns index for hash code h.  
     */  
    static int indexFor(int h, int length) {  
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";  
        return h & (length-1);  
    }

就是一个&操作,这样返回的值比较小适合我们的数组。

继续 line2put操作,因为开始table是空数组,所以会进入 inflateTable(threshold)方法,其实这个方法就是出实话数组容量,初始化长度是16,这个长度是在开始的构造方法赋值的。
所以,现在空数组变成了长度16的数组了,就像下图一样。
HashMap原理学习

接着由于我们的key不为null,到了获取hash值和索引,这里假设int hash = hash(key)和int i = indexFor(hash, table.length)生成的索引i为hash=2306996,i = 4;那么就会在table索引为4的位置新建一个Entry,对应的代码是addEntry(hash, key, value, i);到此结果如下图:
HashMap原理学习

新建的Entry内部的变量分别是,hash,key,value,和指向下一节点的next Entry。

3、继续来看line3,line3和line2一样,而且数组不为空直接hash(key)和index。所以直接看图了
HashMap原理学习

4、到了line4,这里line4情况有点特殊,我们假设line4里key生成的hashcode产生的index也为4,比如hash(“three”) 的值 63281940
hash&(15)产生的index为4。这种情况由于之前的位置已经有Entry了,所以遍历Entry如果key和hashcode都相同,就直接替换,否则新添加一个Entry,来看一下对应源码

public V put(K key, V value) {  
        ...//一些代码  
        for (Entry<K,V> e = table\[i\]; e != null; e = e.next) {  
            Object k;  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }  
        //for循环里判断如果hash和key都一样直接替换。

        modCount++;  
        addEntry(hash, key, value, i);//没有重复的话就addEntry  
        return null;  
    }

上面代码先判断是否需要替换,不需要就调用了addEntry方法。来看addEntry

void addEntry(int hash, K key, V value, int bucketIndex) {  
        if ((size >= threshold) && (null != table\[bucketIndex\])) {  
            resize(2 * table.length);  
            hash = (null != key) ? hash(key) : 0;  
            bucketIndex = indexFor(hash, table.length);  
        }//判断数组容量是否足够,不足够扩容

        createEntry(hash, key, value, bucketIndex);  
    }

里面又调用了createEntry

void createEntry(int hash, K key, V value, int bucketIndex) {  
        Entry<K,V> e = table\[bucketIndex\];  
        table\[bucketIndex\] = new Entry<>(hash, key, value, e);  
        size++;  
        //获取当前节点,然后新建一个含有当前hash,key,value信息的一个节点,并且该节点的Entry指向了前一个Entry并赋值给table\[index\],成为了最新的节点Entry,同时将size加1。  
    }

到这里相信大家很清楚了。来看看图:
HashMap原理学习

5、到这里之后的代码都在上面的分析情况当中。我就不一一画图了,直接给出程序执行到最后的图
line5到line8

| 代码 | hashcode | index | key | value | next | | --- | :-: | --: | --- | --- | --- | | hashMap.put(“four”,”hello4”); | 54378290 | 9 | four | hello4 | null | | hashMap.put(“five”,”hello5”); | 39821723 | 8 | five | hello5 | null | | hashMap.put(“six”,”hello6”); | 86726537 | 4 | six | hello6 | line4产生的Entry | | hashMap.put(“seven”,”hello7”); | 28789082 | 2 | seven | hello7 | line3产生的Entry |

结果图如下:
HashMap原理学习

到此put 操作就结束了,再来看看取

HashMap的取值过程

我们通过hashMap.get(K key) 来获取存入的值,key的取值很简单了。我们通过数组的index直接找到Entry,然后再遍历Entry,当hashcode和key都一样就是我们当初存入的值啦。看源码:

 public V get(Object key) {  
        if (key == null)  
            return getForNullKey();  
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();  
    }

调用getEntry(key)拿到entry ,然后返回entry的value,来看getEntry(key)方法

final Entry<K,V> getEntry(Object key) {  
        if (size == 0) {  
            return null;  
        }

        int hash = (key == null) ? 0 : hash(key);  
        for (Entry<K,V> e = table\[indexFor(hash, table.length)\];  
             e != null;  
             e = e.next) {  
            Object k;  
            if (e.hash == hash &&  
                ((k = e.key) == key || (key != null && key.equals(k))))  
                return e;  
        }  
        return null;  
    }

按什么规则存的就按什么规则取,获取到hash,再获取index,然后拿到Entry遍历,hash相等的情况下,如果key相等就知道了我们想要的值。

再get方法中有null的判断,null取hash值总是0,再getNullKey(K key)方法中,也是按照遍历方法来查找的。

到这你肯定明白了为什么HashMap可以用null做key。

了解的存储取值过程和内部实现,其它的方法自己看看源码很好理解,在此就不一一解释了。

几个问题

问题1、HashMap是基于key的hashcode的存储的,如果两个不同的key产生的hashcode一样取值怎么办?
看了上面的分析,你肯定知道,再数组里面有链表结构的Entry来实现,通过遍历所有的Entry,比较key来确定到底是哪一个value;

问题2、HashMap是基于key的hashcode的存储的,如果两个key一样产生的hashcode一样怎么办?
在put操作的时候会遍历所有Entry,如果有key相等的则替换。所以get的时候只会有一个

问题3、我们总是习惯用一个String作为HashMap的key,这是为什么呢?其它的类可以做为HashMap的key吗?
这里因为String是不可以变的,并且java为它实现了hashcode的缓存技术。我们在put和get中都需要获取key的hashcode,这些方法的效率很大程度上取决于获取hashcode的,所以用String的原因:1、它是不可变的。2、它实现了hashcode的缓存,效率更高。如果你对String不了解可以看:Java你可能不知道的事-String

问题4、可变的对象能作为HashMap的key吗?
可变的对象是可以当做HashMap的key的,只是你要确保你可变变量的改变不会改变hashcode。比如以下代码

public class TestMemory {

    public static void main(String\[\] args) {  
        HashMap hashMap = new HashMap();  
        TestKey testKey = new TestKey();  
        testKey.setAddress("sdfdsf");//line3  
        hashMap.put(testKey,"hello");  
        testKey.setAddress("sdfsdffds");//line5  
        System.out.println(hashMap.get(testKey));  
    }  
}

public class TestKey {  
    String name;  
    String address;

    public String getName() {  
        return name;  
    }

    public void setName(String name) {  
        this.name = name;  
    }

    public String getAddress() {  
        return address;  
    }

    public void setAddress(String address) {  
        this.address = address;  
    }

    @Override  
    public int hashCode() {  
        if (name==null){  
            return 0;  
        }  
        return name.hashCode();  
    }  
}

上面的代码line3到line5对象里的address做了改变,但是由于hashCode是基于name来生成的,name没变,所以依然能够正常找到value。但是如果把setAdress换成name,get就会返回null。这就是为什么我们选择String的原因。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
3年前
HashMap 的底层实现原理
HashMap是一个用于存储KeyValue键值对的集合,每一个键值对也叫做Entry。这些个Entry分散存储在一个数组当中,这个数组就是HashMap的主干。HashMap数组每一个元素的初始值都是Null。 !(https://oscimg.oschina.net/oscnet/8495d30fe00a2865dd74088d2
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这