Java 中HashMap 详解_java hashmap-程序员宅基地

技术标签: 散列表  java  哈希算法  

本篇重点:

1.HashMap的存储结构

2.HashMap的put和get操作过程

3.HashMap的扩容

4.关于transient关键字

HashMap的存储结构

1. HashMap 总体是数组+链表的存储结构, 从JDK1.8开始,当数组的长度大于64,且链表的长度大于8的时候,会把链表转为红黑树。

2. 数组的默认长度是16。数组中的每一个元素为一个node,也就是链表的一个节点,node的数据包含: key的hashcode, key, value,指向下一个node节点的指针。

部分源码如下:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; 
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
...
}

3. 随着put操作的进行,如果数组的长度超过64,且链表的长度大于8的时候, 则将链表转为红黑树,红黑树节点的结构如下,TreeNode继承的LinkedHashMap.Entry是继承HashMap.Node的,所以TreeNode是上面Node的子类。

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
//...
}

4. HashMap类的主要成员变量:

/* ---------------- Fields -------------- */

    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;

View Code

HashMap的put操作过程

本小节讲述put操作中的主要步骤,细小环节会忽略。

1. map.put(key, value),首先计算key的hash,得到一个int值。

2.如果Node数组为空则初始化Node数组。这里注意,Node数组的长度length始终应该是2的n次方,比如默认的16, 还有32,64等

3.用 hash&(length-1) 运算得到数组下标,这里要提一句,其实正常我们最容易想到的,而且也是我之前很长一段时间以为的,这一步应该进行的是求模运算: hash % length ,这样得到的正好是0~length-1之间的值,可以作为数组的下标, 那么为何此处是位与运算呢?

先说结论: 上面提到数组的长度length始终是2^n,在这个前提下,hash & (length-1) 与hash % length是等价的。 而位与运算更快。这里后面会另开一遍进行详解。

4.  如果Node[hash&(length-1)]处为空,用传入的的key, value创建Node对象,直接放入该下标;如果该下标处不为空,且对象为TreeNode类型,证明此下标处的元素们是按照红黑树的结构存储的,将传入的key,value作为新的红黑树的节点插入到红黑树;否则,此处为链表,用next找到链表的末尾,将新的元素插入。如果在遍历链表的过程中发现链表的长度超过了8,此时如果数组长度<64则进行扩容,否则转红黑树。

5. 如果key的hash和key本身都相等则将该key对应的value更新为新的value

6. 需要扩容的话则进行扩容。

注意:

1. 如果key是null则返回的hash为0,也就是key为null的元素一直被放在数组下标为0的位置。

2. 在JDK 1.8以前,链表是采用的头部插入的方式,从1.8改成了在链表尾部插入新元素的方式。 这么做是为了防止在扩容的时候,多线程时出现循环链表死循环。具体会新开一遍进行详细演绎。

HashMap的get操作过程

get的过程比较简单。

1. map.get(key). 首先计算key的hash。

2. 根据hash&(length-1)定位到Node数组中的一个下标。如果该下标的元素(也就是链表/红黑树的第一个元素)中 key的hash的key本身 都和传入的key相同,则证明找到了元素,直接返回即可。

3.如果第一个元素不是要找的,如果第一个元素的类型是TreeNode,则按照红黑树的查找方法查找元素,如果不是则证明是链表,按照next指针找下去,直到找到或者到达队尾。

HashMap的扩容

先说这里的两个概念: size, length.

size:是map.size() 方法返回的值,表示的是map中有多少个key-value键值对儿

length: 这里是指Node数组的长度,比如默认长度是16.

如下面的代码:

        Map<Integer,String> map = new HashMap<>();
        map.put(1,"a");
        map.put(2,"b");
        map.put(3,"c");    

没有在构造函数中指定HashMap的大小,则数组的长度length取默认的16,put了3个元素,则size为3.

Q: 何时需要扩容呢?

A: 在put方法中,每次完成了put操作,都判断一下++size是否大于threshold,如果大于则进行扩容: 调用resize()方法。

Q: 那么threshold又是如何得到的呢?

A: 简单来讲threshold = length * loadfactor(默认为0.75)。 也就是说默认情况下,map中的键值对的个数(size)大于Node数组长度(length)的75%时,就需要扩容了。

Q: 扩容时具体做什么呢?

A: 首先计算出新的数组长度和新的threshold(阈值). 简单来讲,新的length/capacity 是原来的2倍(位运算左移一位),新的threshold为原来的2倍。 还有一些细节此处不再赘述。创建新的Node数组,将原来数组中的元素重新映射到新的数组中。

关于transient关键字

transient关键字的作用:用transient关键字修饰的字段不会被序列化

查看下面的例子:

public class TransientExample implements Serializable{
    private String firstName;
    private transient String middleName;
    private String lastName;

    public TransientExample(String firstName,String middleName,String lastName) {
        this.firstName = firstName;
        this.middleName = middleName;
        this.lastName = lastName;
    }
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("firstName:").append(firstName).append("\n")
                .append("middleName:").append(middleName).append("\n")
                .append("lastName:").append(lastName);
        return sb.toString();


    }


    public static void main(String[] args) throws Exception {
        TransientExample e = new TransientExample("Adeline","test","Pan");

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/path/testObj"));
        oos.writeObject(e);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/path/testObj"));
        TransientExample e1 = (TransientExample) ois.readObject();

        System.out.println("e:"+e.toString());
        System.out.println("e1:"+e1.toString());


    }
}

View Code

输出结果:

e:firstName:Adeline
middleName:test
lastName:Pan

e1:firstName:Adeline
middleName:null
lastName:Pan

被transient关键字修饰的middleName字段没有被序列化,反序列化回来的值是null

Q:HashMap类是实现了Serializable接口的,那么为何其中的table, entrySet变量都标为transient呢?

A:我们知道,table数组中元素分布的下标位置是根据元素中key的hash进行散列运算得到的,而hash运算是native的,不同平台得到的结果可能是不相同的。举一个简单的例子,假设我们在目前的平台有键值对 key1-value1,计算出key1的hash为1, 计算后存在table数组中下标为1的地方,假设table被序列化了,并传输到了另外的平台,并反序列化为了原来的HashMap,key1-value1仍然存在下标1的位置,当在这个平台运行get("key1")的时候,可能计算出key1的hash为2,就有可能到下标为2的地方去找该元素,这样就出错了。

Q:那么HashMap是如何实现的序列化呢?

A:HashMap是通过实现如下方法直接将元素数量(size), key, value等写入到了ObjectOutputStream中,实现的定制化的序列化和反序列化。在Serializable接口中有关于这种做法的说明。

private void writeObject(java.io.ObjectOutputStream out)

throws IOException

private void readObject(java.io.ObjectInputStream in)

throws IOException, ClassNotFoundException;

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/java1527/article/details/126850576

智能推荐

Qt C++ 实现无边框窗体自定义缩放和拖动_qt无边框窗口缩放-程序员宅基地

文章浏览阅读1.8k次。在本文中,我们使用Qt C++中创建了一个无边框窗体,并实现自定义缩放和拖动功能。我们利用标志隐藏了窗体的边框,并通过事件过滤器监听了窗体和顶部栏的事件,从而实现了窗口的拖动和缩放功能。我们还通过辅助函数判断鼠标所处的边缘区域,并设置相应的鼠标样式,提供了直观的用户反馈。_qt无边框窗口缩放

在Mac上通过ssh连接谷歌云上的服务器实例并使用SFTP方式上传文件_mac ssh sftp-程序员宅基地

文章浏览阅读1.3k次。1. 在Mac上通过ssh连接谷歌云上的服务器实例(1)先从本地mac电脑中通过一段简单的命令获得钥匙ssh-keygen -t rsa -f ~/.ssh/google_sem_key(生成key的文件名) -C **(服务器的用户名) -b 2048执行命令会,会让你输入并确认密码,这里直接确认即可然后输入以下命令进入.ssh目录并用ls命令列出当前目录下的文件内容cat google_sem_key.pub你会找到一大串乱码,复制下来。(2)登录谷歌云账户,_mac ssh sftp

vue + element实现el-date-picker的时间格式转换,以及自定义时间格式,修改输入的时间格式_el-date-picker组件 实现输入20240120 转化成20240120 支持手动输入还支-程序员宅基地

文章浏览阅读3.9k次。实现日期选择输入框的时间格式转换、根据数据的情况自动化格式为需要的格式,如果只是需要修改传给后端的值或者格式,可以使用 value-format实现,可以在文档上查看详细的介绍。_el-date-picker组件 实现输入20240120 转化成20240120 支持手动输入还支持时间框

TINYMCE实现从WORD直接粘贴并自动上传图片-程序员宅基地

文章浏览阅读150次,点赞9次,收藏4次。Tinymce编辑器实现从word直接粘贴并自动上传图片,一键粘贴word内容,支持快捷键粘贴(Ctrl+V)并自动上传图片,一键导入word文件,粘贴后文字和图片自动添加到编辑器中,图片自动上传到服务器中,服务器位置可以自定义,自己指定,图片存储接口也能够自定义。关于tinymce粘贴图片,粘贴word,一键导入word,粘贴word内容,网上能找到的方案不是特别多,都是通过HTML5提供的API来实现的。功能上来讲的确是非常方便,对文字,内容,新闻编辑工作来讲,能够大幅度提高效率。

超牛逼的几款轻量级笔记软件!-程序员宅基地

文章浏览阅读5.5k次。点击关注公众号,回复“1024”获取2TB学习资源!编程容易产生挫折,即使作为一种业余爱好也可能是这样。建立一个网页,手机APP或桌面应用都是个很大的工程,好的记笔记技能是让这个工程井然有..._轻量级记事本

ffmpeg报错:Unable to find a suitable output format for-程序员宅基地

文章浏览阅读1.9w次。关于报错:Unable to find a suitable output format for这里显示的路径和我的文件名并不同,说明这个文件名,不能包括空格,所以我把文件名改了就行了。解决。_unable to find a suitable output format for

随便推点

Lumina网络进入SDN市场-程序员宅基地

文章浏览阅读72次。通过从博科通信系统公司收购SDN控制器产品系列的相关资产,Lumina网络公司今天正式进入软件定义网络(SDN)市场。除了采用OpenDaylight?领先的SDN控制器解决方案,Lumina还带来一个由网络软件工程师组成的才华横溢的团队,以及包括全球一些最大的电讯服务提供商在内的现有客户。Lumina提供Lumina SDN控制器、应用和网络开发(N..._lumina sdn控制器

python引用传递的区别_php传值引用的区别-程序员宅基地

文章浏览阅读57次。PHP传值与传址(引用)传值和传引用的区别在于,如果一个参数比较大,占用大量的内存空间,那么传引用的话就会节省拷贝空间。传值:是把实参的值赋值给行参 ,那么对行参的修改,不会影响实参的值传引用 :真正的以地址的方式传递参数传递以后,行参和实参都是同一个对象,只是他们名字不同而已对行参的修改将影响实参的值说明:1....文章技术小哥哥2017-11-13632浏览量PHP常见面试题大全php中传值与..._python有跟php一样的引用传递吗

《TCP/IP详解 卷2》 笔记: 简介_tcpip详解卷二有必要看吗-程序员宅基地

文章浏览阅读7.4k次,点赞3次,收藏8次。 《TCP/IP详解 卷2》讲述的是4.4BSD-Lite(1994年发布的一个BSD操作系统的发行版)的TCP/IP协议栈源代码,之后许多Unix和非Unix(包括Linux)操作系统的网络协议栈的实现都参考了它。 这本书将近900页,讲述了约15000行的代码。这是我第二次阅读如此大篇幅的源代码讲解的书,前前后后,断断续续地花了几个月的时间。我并不是所有的内容都阅读过了,我只关注..._tcpip详解卷二有必要看吗

饺子播放器Jzvd使用过程中遇到的问题汇总-程序员宅基地

文章浏览阅读332次,点赞6次,收藏8次。继续看第一个方法第四行开始,毫无疑问,作者获取getDecorView()根视图,然后将原来的对象放入了跟视图,同时隐藏了状态栏,也即是开启了全屏模式,汗,简单粗暴。从上方代码第三行跳转到下面这个方法,可以看出作者用构造器构造了一个新的对象替代了原来位置的对象。还不够,那如何从全屏模式回来呢,作者在打开全屏的时候,就将原视图的父容器存放到了一个链表中。

python- flask current_app详解,与 current_app._get_current_object()的区别以及异步发送邮件实例-程序员宅基地

文章浏览阅读3.6k次,点赞4次,收藏8次。核心知识AppContext手动、自动入栈LocalStack是线程隔离的栈结构current_app是线程、协程隔离对象LocalProxy是获取当前线程隔离的代理对象一、flask中经典错误 working outside application context错误:working outside application contex原因:在没有获取到应用上下文的情况下,进行了上下文操作。代码:from flask import Flask, current_appapp =_flask current_app

堪比ps的mac修图软件 Pixelmator Pro 2.0.6中文版 支持Silicon M1_pixelmator堆栈-程序员宅基地

文章浏览阅读1.4k次。pixelmator pro中文版是最具创新性也是最好用的mac修图软件,拥有广泛的专业级,非破坏性的图像编辑工具,干净整洁的界面易于操作,支持常见的PSD、TIFF、JPEG、PNG、PDF、EPS 等图形文件格式,提供量选取、渐变、笔刷、填充、裁切,甚至魔术棒工具等功能,拥有50 多种专业的滤镜,它能实现的图片处理功能效果堪比Photoshop,在界面上它与 Photoshop 也有许多相似之处,但它更加轻量便捷!pixelmator pro 可以让常用的修图操作更爽更简单!软件来源:http._pixelmator堆栈

推荐文章

热门文章

相关标签