各种Map总结

  • 就比如问你 HashMap 是不是有序的?你回答不是有序的。

  • 那面试官就会可能继续问你,有没有有序的Map实现类呢?你如果这个时候说不知道的话,那这块问题就到此结束了。如果你说有 TreeMap 和 LinkedHashMap。

  • 那么面试官接下来就可能会问你,TreeMap 和 LinkedHashMap 是如何保证它的顺序的?如果你回答不上来,那么到此为止。如果你说

  • TreeMap 是通过实现 SortMap 接口,基于红黑树,能够把它保存的键值对根据 key 排序,从而保证 TreeMap 中所有键值对处于有序状态。TreeMap的所有key都必须实现Comparable接口,所有key必须是同一类型。自定义类型需要重写equals方法,且返回值要和compareTo保持一致。

  • LinkedHashMap 则是通过维护一个双向链表,使用插入排序(就是你 put 的时候的顺序是什么,取出来的时候就是什么样子)和访问排序(改变排序把访问过的放到底部)让键值有序。也由于需要维护元素的插入顺序,所以性能较之HashMap要差一些。

  • 那么面试官还会继续问你,你觉得它们两个哪个的有序实现比较好?如果你依然可以回答的话,那么面试官会继续问你,你觉得还有没有比它更好或者更高效的实现方式?


有什么方法可以减少hash碰撞?

1.扰动函数可以减少碰撞

原理是如果两个不相等的对象返回不同的 hashcode 的话,那么碰撞的几率就会小些。这就意味着存链表结构减小,这样取值的话就不会频繁调用 equal 方法,从而提高 HashMap 的性能(扰动即 Hash 方法内部的算法实现,目的是让不同对象返回不同 hashcode)。

2.使用不可变的、声明作 final 对象,并且采用合适的 equals() 和 hashCode() 方法,将会减少碰撞的发生

不可变性使得能够缓存不同键的 hashcode,这将提高整个获取对象的速度,使用 String、Integer 这样的 wrapper 类作为键是非常好的选择。

为什么 String、Integer 这样的 wrapper 类适合作为键?

因为 String 是 final,而且已经重写了 equals() 和 hashCode() 方法了。不可变性是必要的,因为为了要计算 hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的 hashcode 的话,那么就不能从 HashMap 中找到你想要的对象。

3.开放定址法

当冲突发生时,使用某种探查技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的地址。按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、双重散列法等。

例子:已知一组关键字为 (26,36,41,38,44,15,68,12,06,51),用除余法构造散列函数,用线性探查法解决冲突构造这组关键字的散列表。
解答:为了减少冲突,通常令装填因子 α 由除余法因子是13的散列函数计算出的上述关键字序列的散列地址为 (0,10,2,12,5,2,3,12,6,12)。
       前5个关键字插入时,其相应的地址均为开放地址,故将它们直接插入 T[0]、T[10)、T[2]、T[12] 和 T[5] 中。
       当插入第6个关键字15时,其散列地址2(即 h(15)=15%13=2)已被关键字 41(15和41互为同义词)占用。故探查 h1=(2+1)%13=3,此地址开放,所以将 15 放入 T[3] 中。
       当插入第7个关键字68时,其散列地址3已被非同义词15先占用,故将其插入到T[4]中。
     当插入第8个关键字12时,散列地址12已被同义词38占用,故探查 hl=(12+1)%13=0,而 T[0] 亦被26占用,再探查 h2=(12+2)%13=1,此地址开放,可将12插入其中。
      类似地,第9个关键字06直接插入 T[6] 中;而最后一个关键字51插人时,因探查的地址 12,0,1,…,6 均非空,故51插入 T[7] 中。


HashMap 中 hash 函数怎么是实现的?

我们可以看到,在 hashmap 中要找到某个元素,需要根据 key 的 hash 值来求得对应数组中的位置。如何计算这个位置就是 hash 算法。

前面说过,hashmap 的数据结构是数组和链表的结合,所以我们当然希望这个 hashmap 里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个。那么当我们用 hash 算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。 所以,我们首先想到的就是把 hashcode 对数组长度取模运算。这样一来,元素的分布相对来说是比较均匀的。

但是“模”运算的消耗还是比较大的,能不能找一种更快速、消耗更小的方式?我们来看看 JDK1.8 源码是怎么做的(被楼主修饰了一下)

    static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

简单来说就是:

  • 高16 bit 不变,低16 bit 和高16 bit 做了一个异或(得到的 hashcode 转化为32位二进制,前16位和后16位低16 bit 和高16 bit 做了一个异或)

  • (n-1) & hash   得到hash桶的下标  


h%length与h&(length-1)得到的结果其实是一个值,但是为什么hashmap中要用后者呢

1.length(2的整数次幂)的特殊性导致了length-1的特殊性(二进制全为1)
2.位运算快于十进制运算,hashmap扩容也是按位扩容,所以相比较就选择了后者


你能谈谈他的容量问题吗? 

我:hashmap的初始容量是16,内部有一个负载因子初始为0.75,当数组中元素达到初始值*负载因子时,就会进行扩容。方法是使用一个新的数组,数组大小为原来的两倍。当然负载因子的大小是可调的,当我们有足够的内存而且非常看重时间的时候,可以将负载因子减小一点,反之可以增加负载因子。

那为什么初始容量是16?

·  我:在计算数组索引位置的时候, hash值&(数组长度-1)得到所在数组的index

采用两次hash    第一次是调用hashcode()方法得到h,第二次是用h&(length-1)。

当length为偶数的时候(length-1)得到的二进制的最后一位为1,&运算后即能得到奇数又能得到偶数。而length为奇数仅仅能得到偶数,这样浪费了一半空间。

这里扩容为啥是2倍?

因为2倍的话,更加容易计算他们所在的桶,并且各自不会相互干扰。

如原桶长度是4,现在桶长度是8,那么

·        桶0中的元素会被分到桶0和桶4中

·        桶1中的元素会被分到桶1和桶5中

·        桶2中的元素会被分到桶2和桶6中

·        桶3中的元素会被分到桶3和桶7中

为啥是这样呢?

桶0中的元素的hash值后2位必然是00,这些hash值可以根据后3位000或者100分成2类数据(0,4一类,8,12一类)。他们分别&(8-1)即&111,则后3位为000的在桶0中,后3位为100的必然在桶4中。其他同理,也就是说桶4和桶0重新瓜分了原来桶0中的元素。

如果换成其他倍数,那么瓜分就比较混乱了。

这样在瓜分这些数据的时候,只需要先把这些数据分类,如上述桶0中分成000和100 2类,然后直接构成新的链表,分类完毕后,直接将新的链表挂在对应的桶下即可

当length为合数[二的幂方]时h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率,这样保证了索引能均匀分布。

取16是为了增加桶数组的利用率和减少hash碰撞,还有提升扩容时分配元素的方便性。


HashMap在JDK1.8及以后的版本中引入了红黑树结构,若桶中链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。

为什么是8?为什么是6?

因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于66/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。


使用链表,平均查找长度?

最好情况是1,最差情况是n,所以平均是(1+n)/2


拉链法导致的链表过深,为什么不用二叉查找树代替而选择红黑树?为什么不一直使用红黑树?

之所以选择红黑树是为了解决二叉查找树的缺陷:二叉查找树在特殊情况下会变成一条线性结构(左斜树,右斜树等,这就跟原来使用链表结构一样了,造成层次很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋、右旋、变色这些操作来保持平衡。引入红黑树就是为了查找数据快,解决链表查询深度的问题。我们知道红黑树属于平衡二叉树,为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少。所以当长度大于8的时候,会使用红黑树;如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。


Hashtable

  • 数组 + 链表方式存储

  • 默认容量:11(质数为宜)

  • put操作:首先进行索引计算 (key.hashCode() & 0x7FFFFFFF)% table.length;若在链表中找到了,则替换旧值,若未找到则继续;当总元素个数超过 容量 * 加载因子 时,扩容为原来 2 倍并重新散列;将新元素加到链表头部

  • 对修改 Hashtable 内部共享数据的方法添加了 synchronized,保证线程安全


HashMap 与 Hashtable 区别

  • 默认容量不同,HashMap是16,Hashtable是11。扩容不同

  • 线程安全性:HashTable 安全,使用了synchronized同步

  • 效率不同:HashTable 要慢,因为加锁


Map是用来存储key-value类型数据的,一个对在Map的接口定义中被定义为Entry,HashMap内部实现了Entry接口。HashMap内部维护一个Entry数组transient Entry[] table;

当put一个新元素的时候,根据key的hash值计算出对应的数组下标。数组的每个元素是一个链表的头指针,用来存储具有相同下标的Entry。

hashmap放入顺序和输出顺序是不一致的!要想维持插入序,可使用LinkedHashMap

遍历HashMap的四种方法:

public static void main(String[] args) {Map<String,String> map=new HashMap<String,String>();map.put("1", "value1");map.put("2", "value2");map.put("3", "value3");map.put("4", "value4");//第一种:普通使用,二次取值System.out.println("\n通过Map.keySet遍历key和value:");  for(String key:map.keySet()){System.out.println("Key: "+key+" Value: "+map.get(key));}//第二种System.out.println("\n通过Map.entrySet使用iterator遍历key和value: ");  Iterator map1it=map.entrySet().iterator();while(map1it.hasNext()){Map.Entry<String, String> entry=(Entry<String, String>) map1it.next();System.out.println("Key: "+entry.getKey()+" Value: "+entry.getValue());}//第三种:推荐,尤其是容量大时  System.out.println("\n通过Map.entrySet遍历key和value");  for(Map.Entry<String, String> entry: map.entrySet()){System.out.println("Key: "+ entry.getKey()+ " Value: "+entry.getValue());}//第四种  System.out.println("\n通过Map.values()遍历所有的value,但不能遍历key");  for(String v:map.values()){System.out.println("The value is "+v);}}

Java集合:HashMap相关推荐

  1. Java集合HashMap

    HashMap Map接口的一个实现类 用于存储键值对映射关系 重复键 如果,出现重复键,将覆盖原有键的Value值 package bhz.aio;import java.util.HashMap; ...

  2. Java集合—HashMap底层原理

    原文链接:最通俗易懂搞定HashMap的底层原理 HashMap的底层原理面试必考题.为什么面试官如此青睐这道题?HashMap里面涉及了很多的知识点,可以比较全面考察面试者的基本功,想要拿到一个好o ...

  3. Java集合——HashMap、HashTable以及ConCurrentHashMap异同比较

    转发:https://www.cnblogs.com/zx-bob-123/archive/2017/12/26/8118074.html 0. 前言 HashMap和HashTable的区别一种比较 ...

  4. 深入java集合-HashMap

    本文为读书笔记,书籍为java并发编程的艺术 hashmap资料来自b站黑马 文章目录 1.HashMap 1.1 HashMap成员变量 问题: 为什么必须是2的n次幂?如果输入值不是2的幂比如10 ...

  5. 分析了解JDK1.8版本的Java集合HashMap的put()方法

    hashMap是java最常用的Key-Value形式的集合.了解其原理和底层代码是很有必要的,今天就记录下对HashMap的.put()方法的研究分析(元素添加方法): 先说下个人研究分析结果: H ...

  6. java集合 HashMap的三种遍历方式

    前言: HashMap的集合中的比重是无可厚非的,由自身的数组+链表/红黑树构成的(JDK 1.8),这样使得HashMap优点表现出来: 数组查询效率快: 链表的插入和删除效率也加快 但是HashM ...

  7. Java集合—HashMap为什么2倍扩容

    原文作者:很閒很快樂 原文地址:HashMap初始容量为什么是2的n次幂及扩容为什么是2倍的形式 HashMap的初始容量都是2的n次幂的形式存在的,而扩容也是2倍的原来的容量进行扩容,也就是扩容后的 ...

  8. Java 集合 — HashMap

    HashMap 无序(每次resize的时候都会变) 非线程安全 key和value都看可以为null 使用数组和链表实现 查找元素的时候速度快 几个重要属性: loadFactor:用来计算thre ...

  9. Java集合- HashMap 的底层数据结构实现原理

    一.HashMap 的数据结构 JDK1.8 之前 JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列. HashMap 通过 key 的 hashCode 经过扰 ...

  10. Java集合源码学习(四)HashMap

    一.数组.链表和哈希表结构 数据结构中有数组和链表来实现对数据的存储,这两者有不同的应用场景, 数组的特点是:寻址容易,插入和删除困难:链表的特点是:寻址困难,插入和删除容易: 哈希表的实现结合了这两 ...

最新文章

  1. form上传文件以及跨域异步上传
  2. Linux 软件管理工具之rpm、yum
  3. node-rsa加密,java解密调试
  4. 2020年日历电子版(打印版)_2020年第11期印花世界电子版/手机版,欢迎在线免费阅读!...
  5. notepad++修改背景色
  6. 第二轮冲次会议第七次
  7. ExtJs入门 (02)-布局面板
  8. 福州大学计算机学院董晨老师,福州大学代表队高分斩获第三届福建省高校网络空间安全大赛冠军...
  9. 面向空天地一体多接入的融合6G网络架构展望
  10. Linux内核分析 - 网络[二]:网卡驱动接收报文
  11. SVN工作副本已经锁定错误的解决方法
  12. django ajax 查询,Django分页和Ajax查询
  13. 我是如何通过思维系统实现年薪50万的?
  14. 如何在PHP7中安装mysql的扩展
  15. 游戏设计的艺术:一本透镜的书——第十九章 世界包含着各种空间
  16. sis最新ip地址2020入口一_2020最新国风修仙问道3DMMORPG手游大道争锋官网正版首发入口...
  17. 创建、删除ubuntu账户,添加用户到组
  18. 汉诺塔详解(超详细)
  19. 萨斯病毒感染情况_审美萨斯1:建筑与风格组织
  20. Your branch is up to date with 'origin/master'.但是本地代码却不是最新的

热门文章

  1. Linux 进程、端口、IP、连接数等查询脚本
  2. 多线程批量ping服务器
  3. 病毒周报(081110至081116)
  4. DTS和PTS的解释
  5. 论文笔记:MobileFaceNet
  6. QT+OpenCV照片动画风格转换
  7. vi 从第几行到第几行 替换_第三十三章:查找新函数:XLOOKUP,再也不用趴着电脑屏幕数第几列、第几行(二)...
  8. 将特定像素点在图像上连接起来_图像分割【论文解读】快速图像分割的SuperBPD方法 CVPR-2020...
  9. 【转】【OpenCV入门教程之一】 安装OpenCV:OpenCV 3.0、OpenCV 2.4.8、OpenCV 2.4.9 +VS 开发环境配置
  10. redis lua