`

HashMap解决hash冲突的方法

 
阅读更多

       在Java编程语言中,最基本的结构就是两种,一种是数组,一种是模拟指针(引用),所有的数据结构都可以用这两个基本结构构造,HashMap也一样。当程序试图将多个 key-value 放入 HashMap 中时,以如下代码片段为例:

HashMap<String,Object> m=new HashMap<String,Object>();
m.put("a", "rrr1");
m.put("b", "tt9");
m.put("c", "tt8");
m.put("d", "g7");
m.put("e", "d6");
m.put("f", "d4");
m.put("g", "d4");
m.put("h", "d3");
m.put("i", "d2");
m.put("j", "d1");
m.put("k", "1");
m.put("o", "2");
m.put("p", "3");
m.put("q", "4");
m.put("r", "5");
m.put("s", "6");
m.put("t", "7");
m.put("u", "8");
m.put("v", "9");

        HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。当程序执行 map.put(String,Obect)方法 时,系统将调用String的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。源码如下:

   public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //判断当前确定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那么新值覆盖原来的旧值,并返回旧值。
            //如果存在相同的hashcode,那么他们确定的索引位置就相同,这时判断他们的key是否相同,如果不相同,这时就是产生了hash冲突。
            //Hash冲突后,那么HashMap的单个bucket里存储的不是一个 Entry,而是一个 Entry 链。
            //系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),
            //那系统必须循环到最后才能找到该元素。
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
 

       上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可.HashMap程序经过我改造,我故意的构造出了hash冲突现象,因为HashMap的初始大小16,但是我在hashmap里面放了超过16个元素,并且我屏蔽了它的resize()方法。不让它去扩容。这时HashMap的底层数组Entry[]   table结构如下: 

   

       Hashmap里面的bucket出现了单链表的形式,散列表要解决的一个问题就是散列值的冲突问题,通常是两种方法:链表法和开放地址法。链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。java.util.HashMap采用的链表法的方式,链表是单向链表。形成单链表的核心代码如下:

 

    void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
 } 

     上面方法的代码很简单,但其中包含了一个设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。

       HashMap里面没有出现hash冲突时,没有形成单链表时,hashmap查找元素很快,get()方法能够直接定位到元素,但是出现单链表后,单个bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。

       当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。

 

  • 大小: 34.1 KB
分享到:
评论
5 楼 dashuaifu 2016-07-29  
4 楼 u011905329 2016-04-01  
通俗易懂,底层的东西很重要
3 楼 zhglhy 2015-07-16  
感谢分享,学习
2 楼 大江帅 2015-04-29  
通俗易懂,不错
1 楼 prettyzdw 2013-04-07  
好文,做java,底层还是要关心一下的。

相关推荐

    rust使用的自定义哈希算法(加上 hashmap/set 别名):快速、确定性_rust_代码_下载

    liballoc 中的 hashmap 默认使用 SipHash,它并没有我们想要的那么快。在编译器中,我们并不真正担心 DOS 尝试,因此我们使用快速非加密哈希。 这与 Firefox 使用的算法相同——它是一种不基于任何广为人知的算法的...

    集合常见面试题

    hashmap如何解决hash冲突,为什么hashmap中的链表需要转成红黑树? hashmap什么时候会触发扩容? jdk1.8之前并发操作hashmap时为什么会有死循环的问题? hashmap扩容时每个entry需要再计算一次hash吗? hashmap的...

    Java的HashMap的工作原理是什么

    hashmap是一个key-value键值对的数据结构,从结构上来讲在jdk1.8...HashMap是用hash表来存储的,在hashmap里为解决hash冲突,使用链地址法,简单来说就是数组加链表的形式来解决,当数据被hash后,得到数组下标,把数

    jdk1.7和jdk1.8中hashmap区别

    Hashmap解决冲突是采用链表,性能上就抱有一定疑问,如果说成百上千个节点在Hash时发生碰撞。存储在一个链表中,那么如果要查找其中的一个节点,就不可避免的花费 O(n) 的查找时间,这是很大的性能损失。这个问题在...

    Java魔法解密:揭秘HashMap底层机制.pptx.pptx

    HashMap内部采用数组和链表的方式存储数据,每个元素都包含键值对,通过hash函数将键映射到数组的索引位置,实现高效的查找和插入。 HashMap的性能优化策略。 HashMap在性能优化方面采取多种策略,如扩容机制、负载...

    在Java8与Java7中HashMap源码实现的对比

    主要介绍了在Java8与Java7中HashMap源码实现的对比,内容包括HashMap 的原理简单介绍、结合源码在Java7中是如何解决hash冲突的以及优缺点,结合源码以及在Java8中如何解决hash冲突,balance tree相关源码介绍,需要的...

    超实用的面试题整理

    HashMap内部维护了一个存储数据的Entry数组,HashMap采用链表解决冲突,每一个Entry本质上是一个单向链表。当准备添加一个key-value对时,首先通过hash(key)方法计算hash值,然后通过indexFor(hash,length)求该key-...

    HashMap 源码分析

    在JDK1.8之前采用的是数组+链表的数据结构来存储数据,是不是觉得很熟悉,没错这玩意在1.8之前的结构就和HashTable一样都是采用数组+链表,同样也是通过链地址法(这里简称拉链法)来解决冲突,但是HashMap和HashTable...

    散列表(HashMap)

    利用Double hashing解决散列表的冲突,完美实现Hash Map

    Hash知识点总结

    Hash相关知识点 面试基本提纲  ... equals 方法用来做什么 4. hash 表用在 Java 中是什么形式的 1. HashMap 是什么 2. HashSet 是什么 3. HashMap 中为什么用红黑树 5. hash 表是线程安全的么? 答题技

    2023年最新java面试大全

    【01期】Spring,SpringMVC,SpringBoot,Spring...【16期】你能谈谈HashMap怎样解决hash冲突吗 【17期】什么情况用ArrayList or LinkedList呢? 【18期】Java序列化与反序列化三连问:是什么?为什么要?如何做?

    Java集合框架源码剖析:HashSet 和 HashMap

    总体介绍  之所以把HashSet和HashMap放在一起讲解,是因为二者在Java里有着相同的实现,前者仅仅是对后者做了一层包装,也是说...  根据对冲突的处理方式不同,哈希表有两种实现方式,一种开放地址方式(Open addre

    史上最全的Java面试题集锦.pdf

    Hashmap 源码级掌握,扩容,红⿊树,最⼩树化容量,hash冲突解决,有些⾯试官会提出发⾃灵魂的审问,⽐如为什么是红⿊树, 别的树不可以吗;为什么8的时候树化,4不可以吗,等等 concureentHashMap,段锁,如何分段...

    1、Java基础(35题).pdf

    1. Jdk1.8以前是进⾏行行四次扰动计算,可能从速度功效各⽅方⾯面考虑...Node⾥里里有next引⽤用指向下⼀一个节点,因为hashmap解决冲突的思路路是拉链法。 3. 另外变化⽐比较⼤大的还有扩容机制,也就是resize⽅方法。

    HashMap(二)原理讲解

    1.HashMap的继承体系是怎么样的? 2.Node数据结构的分析? Node的成员变量 final int hash; final K key; ...hash:存放哈希值 ...指的是,相同 hash 值的键值对会发生冲突组成链表 时间复杂度 由O(1)退化为O(n) 6.J

    HashMap源码阅读

    hash冲突解决 1.7和1.8区别 扩容机制(为什么是2倍) rehash过程 红黑树的左右旋 一、底层数据结构 // 1.位桶数组 transient Node[] table;//存储(位桶)的数组 // 2.数组元素Node实现了Entry接口 //Node是单向链表...

    sesvc.exe 阿萨德

    如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。 如果当前桶为红黑树,那就要按照红黑树的方式写入数据...

    新东方一面(2020春招—Java后端)

    自我介绍? 项目介绍?...解决Hash冲突的方法?除了开放寻址法和链表法还有吗? 散列函数有哪些?有什么优缺点? 线程和进程? 为什么切换进程开销大? 进程间的通信方式? 手写二叉树及实例化? JVM内

    CuckooMap:使用布谷鸟哈希的HashMap的Python实现

    为了解决这个问题,我选择定义一个函数来检测是否有超过2000个基因剔除。 一旦达到此限制,我将拒绝二传手。 拒绝不会影响其他元素的放置。 这是哈希图的固定大小的实现。 我选择将其初始化为2倍的存储桶数,以...

Global site tag (gtag.js) - Google Analytics