首页 > 编程知识 正文

Java基础 HashMap实现原理及方法

时间:2023-05-05 07:32:30 阅读:50796 作者:1452

http://www.Sina.com/http://www.Sina.com /

下图是HashMap的源代码,可以看到它继承自AbstractMap,实现了Map、Cloneable和Serializable接口。 列出了一些默认值。

在此,称为modCount的属性值,是HashMap的内部结构发生变化(内部结构,复盖相同的key,put )一个value的原始值不是结构的变化)的次数,主要是在反复的迅速失败中

threshold判断HashMap的最大容量,Threshold=(int ) ) capacity * loadFactor );

加载因子:哈希表中的实际元素数/哈希表容量。

这是测定哈希表空间的使用度,负荷因子越大表示哈希表的装填度越高(装满行李),使用链表法的哈希表空间的利用越充分,检索时间越长,效率越低越小,结论就越与大相反。

Entry是HashMap的静态内部类,从上面可以看到,Entry是承载key-value的值,缺省表是Entry数组。 看看Entry的源代码吧。

1、什么是HashMap?以下是对应的小栗子:

1、

映射,String map2=new HashMapString,String (;

stringR2=map2.put(null,null );

system.out.println('R2'R2 );

string R3=map2. get (空);

system.out.println('R3'R3 );

stringR4=map2.put(null,' 32 ';

system.out.println(R4 ) R4;

输出结果: R2空值

R3空值

R4空值

2,

3、由此可见,hashmap的hash方法如果key为空则缺省为0,否则key取hashcode异或运算h,无符号地向右移位16位

HashMap通常提起他,我们想到的就是键值对方式存储(key-value型式),可以接收null键值和null值。基于Map接口的非同步实现(也就是线程不安全),并不保证映射的顺序,特别不保证这个顺序恒久不变jdk的1.8和更低版本以及1.8和更高版本HashMap的数据结构略有变化。 这里主要介绍1.8以前的数据结构。 put时如果key传入为null,那么value一定会为null,无论value输入什么.并且map.put()/get()的返回值是value的类型,并且当存入空的时候为hashmap中数组第一位。由上面给出的属性源代码可以看出,HashMap是这样的数据结构。 (table数组,Entry类是存储Key-value的链表的节点。 )拉链法)也称为链表数组。

2、HashMap的数据结构是什么样的呢?中,默认值增加了树的默认值,Entry类更改为Node,但类的内容没有更改。

然后,添加TreeNode节点,从链接的HashMap继承,链接的HashMap从HashMap继承。 TreeNode的内容如下所示。

红黑树是一种自平衡二叉搜索树。 其统计性能优于二叉树(AVL树)的平衡。 该树结构从根节点开始,左边的子节点小于它,右边的子节点大于它。 每个节点都符合这个特性,所以容易查找,是很好的数据结构。 但是,存在容易偏向某一方的问题。 这样就像链表结构,失去了树结构的优点,搜索时间也变差了。 (在此详细介绍此结构后,我们将打开数据结构列。)

阅读源代码时,您会发现树比链表占用了更多的空间。 8后,你决定如何使用这两种数据结构吗?

1、如果一个内部表的索引超过8个节点,链表将转换为红黑树

2、内部表索引小于6个节点时,树返回链表

主要是数组+链表的结构

平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

3、HashMap的工作原理

            它的原理就是Hashing原理。在上一部分我们讲到了HashMap的数组结构,每添加(put())或是获取(get())都是向每个数组位(array[i])对应为一个bucket里的Entry中添加键值对。而通过hashCode()得出一个hash值来算出bucket(水桶)的位置来进行存取(这里的运算都是位运算)。

4、HashMap的存取   1、存入主要是put()方法:                

            可以从上面源码看出在put时,根据hash值来确定存放数组的索引位置。如果该位置上已经存放了Entry那么,那么将会以链表的形式存放键值对,原来的Entry放在next进行链接,及新加入的放链头,原来的放链尾。如果该位置上没有Entry则直接存放到对应的数组索引的位置上。

 2、取出get()方法:

        从源码上可以看出get()也是先算取key的hash值,然后找到hash值对应的索引,看是否有链表,若有则看是否和链表中Entry的key是否相等,相等则equals()取value值。实际上就是将key-value看成一个整体Entry,来用hashCode()来查找hash值,如果有链表就判断链表,没有链表就直接取对应的数组索引。

 3、扩容resize()(rehash)方法:

5、HashMap的一些问题思考 1、什么时候会出现扩容呢?

    

        源码上addEntry是size大于threshold时开始调用resize()方法。也就是HashMap中,数组元素超过了数组阀值的时候就重新扩容,以降低实际的负载因子。(threshold>capacity*loadfactor)默认的的负载因子 0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize后的HashMap 容量是原容量的两倍。

2、HashMap容量一定为2的幂呢?

            首先,我们希望元素存放的更均匀,最理想的效果是每个bucket中存放一个Entry(key-value)。这样查询的时候效率高,不需要遍历链表,也不需要equals去比较链表中的key的内容。而且,这样空间利用率最大,时间复杂度最优。因为HashMap的底层数组的长度为2^n次方,不同的key算得的index的相同几率较小,数组上分布就比较均匀,也就是碰撞几率小,相对查询时效率会高。

            假设:数组长度为32时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。

3、Hash冲突(碰撞)及解决Hash冲突的方法?        

             hash冲突就是当hash值相同,此时他们确定的索引位置相同,这时他们的key如果不相同,则为hash冲突。

            解决hash冲突的办法:

                    通常有:1、开放定址法 (基本思想是通过一个探测算法,当某个槽位已经被占据的情况下据需查找下一个可以使用的槽位)。 

                                  2、再哈希法(基本思想是同事构造多个哈希函数,当函数1冲突时,再用下一个方法计算,直到冲突不再产生)。这种方法不已长生聚焦,但增加计算时间。                                               

 

                                  3、链地址法(基本思想是将相同的hash值的对象组成一个成为同义词链的单链表,并将单链表的头指针存在哈希表的这个hash值中)。适用于经常插入和删除。

                                  4、建立公共溢出区(将哈希表分为基本表和溢出表两部分,凡是发生冲突的都放在溢出表中)。

                Java.until.HashMap就是采用链表法的方式。当发生碰撞,对象将会存储在LinkedList的下一结点中。HashMap在每个LinkedList节点中存储键值对对象。

4、重新调整HashMap大小存在的问题 

          HashMap数组扩容后很消耗性能:原数组中的数据必须重新计算其出现在新数组中的位置,并存储进去。

         在多线程下,HashMap扩容后可能会产生条件竞争(race condition)。如果两个线程都发现HashMap需要重新调整大小,他们会同时试着调整大小,在调整大小的过程中,存储在LinkedList中的元素次序会反过来,因为移动到新的bucket位置的时候Hashmap并不会将元素放在LinkedList的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争产生,就会出现死循环。

5、Fail-First机制 

   java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了 hashmap,那么将抛出 ConcurrentModificationException,这就是所谓 fail-fast 策略。 

       在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 Map,则会抛出异常,下图是源代码: 

  解决办法:

        1、在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。          

        2、使用CopyOnWriteArrayList来替换ArrayList。推荐使用该方案。

6、为什么String, Interger这样的wrapper类适合作为键?

           因为他们由final修饰,使用不可变类就能保证hashCode是不变的,而且重写了equals和hashcode方法,避免了键值对改写。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,提高HashMap性能。

        

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。