聊聊HashMap那点事儿

   项目中一般都会用到HashMap,面试里也经常会问到这些东西。那么, 今天咱们就来说一说HashMap,要深入的学习HashMap的原理还是很简单的,因为它是一个东西,不像spring是一堆东西。

    从哪里说起呢,先从最最基本的类型来说: String 和Integer。这两个类型基本上可以代表项目里的所有类型了。其他的什么枚举,或者对象什么的都是基于这两个类型的,至少目前来说是如此的。

     String和Integer是有局限性的,为什么呢,他们只能代表一个东西,不能代表一堆东西。当我们要存一堆东西时,怎么办呢?很简单,我们知道有数组。数组估计就是这么出来的。说到数组,就得说一个典型的异常了。数组下标溢出异常ArrayIndexOutOfBoundsException。说到这个异常,我就想说数组实现的基本原理了。想创建一个数组,你得提前申请一块内存,注意是一块。而不是几块合起来叫做一块。所以呢,数组是一段连续的内存地址。数组名默认指向地址的起始地址。每个数据元素占用一定的长度。在使用数组和指针的时候,尤其能够体会到这一点。所以数组有一个局限性。必须是一块内存。而且是刚开始的时候已经固定大小的内存了。

    总结:数组是用来存储东西的,而且初始化的时候就确定大小。换而言之,就是你想要一个数组可以,但必须告诉计算机你要一个多大的数组,然后计算机给你找一块这样大小的内存地址块。所以HashMap要想实现数据的存储,HashMap用到数组的可能性太大太大了。基于HashMap用到了数组,我们开始往下走。

    用到了数组,我们很轻松的解决了存储一大堆东西的问题。那么新的问题来了,一般我们在设计数据库的时候,会有主键,而不是单纯的数据索引。所以是不是可以往数组里存入的时候,顺道存储一个索引呢,这个索引由我们自己来决定,而不是依据数组的索引。所以存入数据的元素将会是一个对象,而不是单纯的String Integer对象了。Node这个对象就来了。key代表我们的索引,而value则代表我们真正想要存储的内容。

class Node<T>{
  String key;
  T value

}

    接着往下走,我们会遇到新的问题,就是虽然我们可以根据key获取自己的元素,但是在数组里,我们不得不依据循环遍历数组,对比key才能够获取我们的想要的内容。这样太麻烦了。有没有什么样的算法能够知道key就能够快速知道位置呢?

   在java里所有的引用类型都继承了Object类型,每个Object类型都有HashCode方法,会返回一个int  类型的hash值。插一句不知道是先有的HashMap还是先有的hashCode方法。那么既然每个类型都会返回一个int值。那么很简单我们每次存储的时候,利用这个key的hash值直接存入,取的时候再利用hash值直接确定索引,拿到值就OK了。幸福简直来的太快了。我们得想一下有没有其他的问题。答案是有的。

    利用hash值我们很轻松的能够存取,达到O(1)。不得不考虑一件事情数组的大小是有限制的,一旦hash值超过了数组的大小,那就数组下标溢出就出现了呀。咋办。人类的智力是超群的。我们利用了求余算法解决了这个问题。(当然了HashMap的hash算法不是求余这么简单的)走到这里,我们还没有大功告成呢,求余算法虽然能够保证不会出数组的边界,但是不同的hash值对容量求余之后会出现相同答案的呀。没事,我们可以让这个hash值再加1然后再求余。这个问题有问题的,否则HashMap不会不用这个方案。这个方法虽然解决了存的问题,你取怎么办???

       HashMap采用的是链式方案+equals方法解决的这个问题。当hash遇到冲突碰撞的时候,会直接在该Node下面在接一个Node。解决了存储的问题,当取的时候,先根据hash值找到这个练所在的索引位置,然后根据双等和equals方法来获取元素。所以我们会经常看到一个要点:重写一个对象的HashCode方法时,要重新equals方法。

    就这样HashMap重要的功能 存取功能就被我们解决了。不忘初心回首看下,我们HashMap要处理什么问题。存储一大堆东西,而且存取的时候不废力。基于上面我们的实现,当这个链特别特别长的时候,我们取的效率将会特别的低。有没有什么办法解决这个问题呢?两个解决办法:一种找到更加棒的数据结构,而且不影响我们HashMap其他的内容——Java8的红黑树出现了。链式长度达到8以上。另一种就是减少存储的时候key的hash值碰撞次数,碰撞次数越少,这个链就会越短。那么怎么才能够减少碰撞的次数呢。HashMap有两种处理方式

     第一是对hash值做处理,hashCode方法返回int类型的值以后,对这个值进行一系列的处理。得到的值能够十分均匀的分布到HashMap数组的长度里。java8采取高位与低位的方式获取hash值的。这块看的不太懂啊。来自一段解释:通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

    第二:hash值再保证均匀,均匀到极点,在HashMap的容量达到满的时候,碰撞的概率将会极大,所以HashMap采用空间换时间的办法。当元素个数超过阈值以后。HashMap的扩容触发点和我想的不一样,我原本以为是数组的索引占用太多才扩容。但是HashMap是根据元素个数,包括链表里的元素个数超过阈值来进行扩容的。由此,HashMap里的那些容量,负载银子,size等也都浮出水面了。


        在看原代码的过程中,想到了之前的一个问题:在java8新特性的时候,不能够在forEach方法里对HashMap进行元素的删除,添加操作。原因如下:在foreach方法里,有一个modCount,这个值是HashMao的int 类型的一个属性,每次修改或者删除元素的时候,会使该属性加1。foreach会对比这个值,如果两个值不相等的话,就会抛出异常。另一方面foreach方法接受的是一个消费型函数,所以能不对HashMap进行操作就不要操作了。尊重下作者

 public final void forEach(Consumer<? super K> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }



HashMap的分析先到这里为止。对于HashMap里的entrySet和keySet咱们有空了再聊。等哪天能够扯的话,再扯。


参考:

JDK1.8#HashMap源码及注释

https://tech.meituan.com/java-hashmap.html

https://www.cnblogs.com/zhangyinhua/p/7698642.html


猜你喜欢

转载自blog.csdn.net/wgp15732622312/article/details/80641459