Java你可能不知道的事(3)HashMap

概述

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的类结构关系。

Java你可能不知道的事(3)HashMap

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

3、内部介绍

Java你可能不知道的事(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 = <span class="hljs-built_in">new</span> HashMap()
        hashMap.<span class="hljs-built_in">put</span>(<span class="hljs-string">"one"</span>,<span class="hljs-string">"hello1"</span>)
        hashMap.<span class="hljs-built_in">put</span>(<span class="hljs-string">"two"</span>,<span class="hljs-string">"hello2"</span>)
        hashMap.<span class="hljs-built_in">put</span>(<span class="hljs-string">"three"</span>,<span class="hljs-string">"hello3"</span>)
        hashMap.<span class="hljs-built_in">put</span>(<span class="hljs-string">"four"</span>,<span class="hljs-string">"hello4"</span>)
        hashMap.<span class="hljs-built_in">put</span>(<span class="hljs-string">"five"</span>,<span class="hljs-string">"hello5"</span>)
        hashMap.<span class="hljs-built_in">put</span>(<span class="hljs-string">"six"</span>,<span class="hljs-string">"hello6"</span>)
        hashMap.<span class="hljs-built_in">put</span>(<span class="hljs-string">"seven"</span>,<span class="hljs-string">"hello7"</span>)

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

<span class="hljs-keyword">public</span> V <span class="hljs-title">put</span>(K key, V <span class="hljs-keyword">value</span>){
    <span class="hljs-keyword">int</span> hash = hash(key);
    <span class="hljs-keyword">int</span> i = indexFor(hash, table.length);

}

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

<span class="hljs-javadoc">/**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).

     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-title">HashMap</span>() {
        <span class="hljs-keyword">this</span>(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

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

<span class="hljs-keyword">public</span> <span class="hljs-title">HashMap</span>(<span class="hljs-keyword">int</span> initialCapacity, <span class="hljs-keyword">float</span> loadFactor) {
        <span class="hljs-keyword">if</span> (initialCapacity < <span class="hljs-number">0</span>)
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalArgumentException(<span class="hljs-string">"Illegal initial capacity: "</span> +
                                               initialCapacity);
        <span class="hljs-keyword">if</span> (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        <span class="hljs-keyword">if</span> (loadFactor <= <span class="hljs-number">0 || Float.isNaN(loadFactor))
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalArgumentException(<span class="hljs-string">"Illegal load factor: "</span> +
                                               loadFactor);

        <span class="hljs-keyword">this</span>.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }

<span class="hljs-keyword">void</span> init() {
    }
</=>

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

 <span class="hljs-javadoc">/**
     * An empty table instance to share when the table is not inflated.

     */</span>
    <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Entry<?,?>[] EMPTY_TABLE = {};

    <span class="hljs-javadoc">/**
     * The table, resized as necessary. Length MUST Always be a power of two.

     */</span>
    <span class="hljs-keyword">transient</span> Entry<k,v>[] table = (Entry<k,v>[]) EMPTY_TABLE;</k,v></k,v>

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

<span class="hljs-keyword">public</span> V <span class="hljs-title">put</span>(K key, V <span class="hljs-keyword">value</span>) {
        <span class="hljs-keyword">if</span> (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        <span class="hljs-keyword">if</span> (key == <span class="hljs-keyword">null</span>)
            <span class="hljs-keyword">return</span> putForNullKey(<span class="hljs-keyword">value</span>);
        <span class="hljs-keyword">int</span> hash = hash(key);&#x83B7;&#x53D6;hash&#x503C;
        <span class="hljs-keyword">int</span> i = indexFor(hash, table.length);&#x751F;&#x6210;&#x7D22;&#x5F15;
        <span class="hljs-keyword">for</span> (Entry<k,v> e = table[i]; e != <span class="hljs-keyword">null</span>; e = e.next) {
            Object k;

            <span class="hljs-keyword">if</span> (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.<span class="hljs-keyword">value</span>;
                e.<span class="hljs-keyword">value</span> = <span class="hljs-keyword">value</span>;
                e.recordAccess(<span class="hljs-keyword">this</span>);
                <span class="hljs-keyword">return</span> oldValue;
            }
        }

        modCount++;

        addEntry(hash, key, <span class="hljs-keyword">value</span>, i);
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
    }</k,v>

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

<span class="hljs-javadoc">/**
     * Returns index for hash code h.

     */</span>
    <span class="hljs-keyword">static</span> <span class="hljs-keyword">int</span> indexFor(<span class="hljs-keyword">int</span> h, <span class="hljs-keyword">int</span> length) {

        <span class="hljs-keyword">return</span> h & (length-<span class="hljs-number">1</span>);
    }

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

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

Java你可能不知道的事(3)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);到此结果如下图:

Java你可能不知道的事(3)HashMap

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

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

Java你可能不知道的事(3)HashMap

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

<span class="hljs-keyword">public</span> V <span class="hljs-title">put</span>(K key, V <span class="hljs-keyword">value</span>) {
        ...

        <span class="hljs-keyword">for</span> (Entry<k,v> e = table[i]; e != <span class="hljs-keyword">null</span>; e = e.next) {
            Object k;
            <span class="hljs-keyword">if</span> (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.<span class="hljs-keyword">value</span>;
                e.<span class="hljs-keyword">value</span> = <span class="hljs-keyword">value</span>;
                e.recordAccess(<span class="hljs-keyword">this</span>);
                <span class="hljs-keyword">return</span> oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, <span class="hljs-keyword">value</span>, i);
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
    }</k,v>

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

<span class="hljs-keyword">void</span> addEntry(<span class="hljs-keyword">int</span> hash, K key, V <span class="hljs-keyword">value</span>, <span class="hljs-keyword">int</span> bucketIndex) {
        <span class="hljs-keyword">if</span> ((size >= threshold) && (<span class="hljs-keyword">null</span> != table[bucketIndex])) {
            resize(<span class="hljs-number">2</span> * table.length);
            hash = (<span class="hljs-keyword">null</span> != key) ? hash(key) : <span class="hljs-number">0</span>;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, <span class="hljs-keyword">value</span>, bucketIndex);
    }

里面又调用了createEntry

<span class="hljs-keyword">void</span> createEntry(<span class="hljs-keyword">int</span> hash, K key, V <span class="hljs-keyword">value</span>, <span class="hljs-keyword">int</span> bucketIndex) {
        Entry<k,v> e = table[bucketIndex];
        table[bucketIndex] = <span class="hljs-keyword">new</span> Entry<>(hash, key, <span class="hljs-keyword">value</span>, e);
        size++;

    }</k,v>

到这里相信大家很清楚了。来看看图:

Java你可能不知道的事(3)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

结果图如下:

Java你可能不知道的事(3)HashMap

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

HashMap的取值过程

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

 <span class="hljs-keyword">public</span> V <span class="hljs-title">get</span>(Object key) {
        <span class="hljs-keyword">if</span> (key == <span class="hljs-keyword">null</span>)
            <span class="hljs-keyword">return</span> getForNullKey();
        Entry<k,v> entry = getEntry(key);

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span> == entry ? <span class="hljs-keyword">null</span> : entry.getValue();
    }</k,v>

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

final Entry<k,v> getEntry(<span class="hljs-built_in">Object</span> key) {
        <span class="hljs-keyword">if</span> (size == <span class="hljs-number">0</span>) {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
        }

        int hash = (key == <span class="hljs-literal">null</span>) ? <span class="hljs-number">0</span> : hash(key);
        <span class="hljs-keyword">for</span> (Entry<k,v> e = table[indexFor(hash, table.length)];
             e != <span class="hljs-literal">null</span>;
             e = e.next) {
            <span class="hljs-built_in">Object</span> k;
            <span class="hljs-keyword">if</span> (e.hash == hash &&
                ((k = e.key) == key || (key != <span class="hljs-literal">null</span> && key.equals(k))))
                <span class="hljs-keyword">return</span> e;
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }</k,v></k,v>

按什么规则存的就按什么规则取,获取到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。比如以下代码

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> TestMemory {

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span>(String[] args) {
        HashMap hashMap = <span class="hljs-keyword">new</span> HashMap();
        TestKey testKey = <span class="hljs-keyword">new</span> TestKey();
        testKey.setAddress(<span class="hljs-string">"sdfdsf"</span>);
        hashMap.put(testKey,<span class="hljs-string">"hello"</span>);
        testKey.setAddress(<span class="hljs-string">"sdfsdffds"</span>);
        System.<span class="hljs-keyword">out</span>.println(hashMap.<span class="hljs-keyword">get</span>(testKey));
    }
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> TestKey {
    String name;
    String address;

    <span class="hljs-keyword">public</span> String <span class="hljs-title">getName</span>() {
        <span class="hljs-keyword">return</span> name;
    }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setName</span>(String name) {
        <span class="hljs-keyword">this</span>.name = name;
    }

    <span class="hljs-keyword">public</span> String <span class="hljs-title">getAddress</span>() {
        <span class="hljs-keyword">return</span> address;
    }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setAddress</span>(String address) {
        <span class="hljs-keyword">this</span>.address = address;
    }

    @Override
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">hashCode</span>() {
        <span class="hljs-keyword">if</span> (name==<span class="hljs-keyword">null</span>){
            <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
        }
        <span class="hljs-keyword">return</span> name.hashCode();
    }
}

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

到这里相信你对HashMap内部已经非常清楚了,如果本篇文章对你有帮助记得点赞和评论,或者关注我,我会继续更新文章,感谢支持!

Original: https://www.cnblogs.com/yangqiangyu/p/5383867.html
Author: So,Cool
Title: Java你可能不知道的事(3)HashMap

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/607950/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • WWDC2016-session401-CodeSign大改版

    自动签名机制和手动签名都明显看起来很好用。 自动签名有log 手动签名有具体的错误提示信息。 session401 Xcode Signing. 亲,你的眼睛好大 相声演员吗? Y…

    Java 2023年5月30日
    079
  • Spring Cloud Alibaba分布式事务组件 seata 详解(小白都能看懂)

    一,什么是事务(本地事务)? 指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 简单的说,事务就是并发控制的单位,是用户定义的一个操作序列。 _而一个逻辑…

    Java 2023年6月14日
    089
  • 云原生系列2 部署你的第一个k8s应用

    云原生的概念和理论体系非常的完备,but talk is cheap , show me the code ! 但是作为一名程序员,能动手的咱绝对不多BB,虽然talk并不chea…

    Java 2023年6月8日
    079
  • MySQL学习之路(1):SQL脚本语言

    使用MySQL数据库,首先安装MySQL数据库,本文所有SQL脚本在MySQL上测试和执行。 安装Mysql服务器;安装Mysql workbench客户端,可以以图形化界面管理m…

    Java 2023年6月6日
    059
  • 企业微信第三方应用(三)基于springboot开发(获取Ticket,auth_code)

    一、构建spring boot项目1、新建项目新建一个模块(module):enterprise-wechat新建一个子模块(module):wechat目录结构如下: 结构描述:…

    Java 2023年6月7日
    081
  • Java-Security(五):Spring Security默认登录页面渲染流程

    本篇文章主要用来阐述Spring Security渲染默认登录表单页面流程。在默认登录表单页面渲染过程中主要涉及到以下3个拦截器: 1)FilterSecurityIntercep…

    Java 2023年5月29日
    084
  • Java动态脚本Groovy,高级啊!

    前言:请各大网友尊重本人原创知识分享,谨记本人博客: 南国以南i 简介: Groovy是用于Java虚拟机的一种敏捷的动态语言,它是一种成熟的面向对象编程语言,既可以用于面向对象编…

    Java 2023年6月5日
    098
  • MyBatis: Invalid bound statement (not found)错误的可能原因

    其他原因导致此问题解决参考: 1.检查 xml 文件所在 package 名称是否和 Mapper interface 所在的包名一致 mapper 的 namespace 写的不…

    Java 2023年5月30日
    066
  • MongoDb在windows10下的安装、创建用户和数据库

    1.mongodb下载地址https://www.mongodb.com/download-center#community 2.安装 3.在D:\MongoDB目录下创建db和l…

    Java 2023年6月16日
    078
  • java SPI思想

    为什么说java spi破坏双亲委派模型? – 大宽宽的回答 – 知乎 Original: https://www.cnblogs.com/myseries…

    Java 2023年5月29日
    075
  • 抵达心之自由

    如水涟漪,如树伫立,如草柔韧。 自由来自智慧。 你眼前所见即为真,所不见亦为真。假从何来?假并不存在于事物中,而是存在于标准中。 挣脱了标准的枷锁,就获得了第一层自由; 当深明生命…

    Java 2023年6月9日
    083
  • Markdown基础语法

    Markdown语法 ## 欢迎使用Markdown编辑器 你好! 这是你第一次使用 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下M…

    Java 2023年6月7日
    096
  • Java中修饰符的分类及用法

    是静态修饰符,静态就是指在编译后所分配的内存会一直存在,直到程序退出内存才会释放这个空间。 Original: https://www.cnblogs.com/mojospy/p/…

    Java 2023年6月9日
    074
  • 面经

    本文收集最近遇到的好问题,保持学习,保持进步。 数据结构 树的深度优先和广度优先的算法实现 数据库的联合索引的底层实现 map 如何保证顺序 谈谈 HashMap 的源码 谈谈如何…

    Java 2023年6月8日
    089
  • 教学日志:javaSE-循环语句

    /* while循环:先判断条件,再执行逻辑代码 四部分组成: 1、初始化:循环的初始化变量 2、条件判断:条件返回必须是true或false 3、循环体:条件满足的话执行的逻辑代…

    Java 2023年6月5日
    090
  • Oracle数据库😊

    sqlplus常用命令 连接数据库:conn 用户名/密码@网络服务标识[as sysdba] 断开数据库连接: 断开和oracle的连接但是不退出sqlplus窗口 区别:sta…

    Java 2023年6月15日
    050
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球