Java中的HashMap详解!详细分析HashMap的工作方式和使用原理
HashMap的工作方式HashMap在Map.Entry静态内部类实现中存储key-value对HashMap使用哈希算法,在put() 和get() 方法中,使用了hashCode() 和equals() 方法
通过传递key-value对调用put() 方法时 ,HashMap使用key hashCode() 和哈希算法找到存储key-value对的索引 .Entry存储在LinkedList中,如果存在Entry, 会使用equals() 方法来检查传递的key是否存在.如果存在,就会覆盖value. 如果不存在,就会创建一个新的Entry来保存
通过传递key调用get() 方法时,再次使用key hashCode() 方法来找到数组中的索引,然后使用equals() 方法找出正确的Entry并返回Entry的值HashMap的实现原理
Java 中的数据结构映射定义了接口 java.util.Map, 接口有以下四个常用的实现类:
HashMap:HashMap根据键的hashCode值存储数据,通常可以根据键的hashCode值直接定位到键对应的值,从而具有很快的访问速度HashMap中遍历的顺序是不确定的HashMap中最多只允许一条数据的键为null, 可以允许多条数据的值为nullHashMap是线程不安全的.在同一时刻允许多个线程同时写入HashMap, 这样就会导致数据的不一致HashMap如果想要满足线程安全,可以使用Collections的synchronizedMap() 方法使得HashMap具有线程安全性,或者使用ConcurrentHashMap
Hashtable:Hashtable和HashMap类似,只是Hashtable继承自Dictionary类Hashtable中的键和值都不能为nullHashtable 是线程安全的.在同一时刻只允许一个线程写入 Hashtable. 但是 Hashtable 的并发性不如引入了分段锁的 ConcurrentHashMap Hashtable是通过为方法添加synchronized锁实现线程安全的ConcurrentHashMap是由Segment数据结构和HashEntry数据结构组成的 Segment是一种可重入锁ReentrantLock, 在ConcurrentHashMap中作为锁 HashEntry用于存储键值对数据一个ConcurrentHashMap包含一个Segment数组 .Segment的数据结构和HashMap类似,是一种数组和链表的结构一个Segment中包含一个HashEntry数组.每个HashEntry是一个链表结构的元素一个Segment守护一个HashEntry数组中的元素HashEntry中的数据进行修改时,必须首先获得HashEntry对应的Segment锁分段锁: 分段锁的含义就是用到哪一部分就锁定哪一部分 分段锁就是将整个Map划分成N个Segment, 在进行put() 和get() 操作时,根据键的hashCode值寻找到应该使用哪个Segment. 这个Segment做到了类似HashTable的线程安全 ConcurrentHashMap中的键和值都不能为null 不建议使用HashTable, 在不需要线程安全的场景中,可以使用HashMap. 在需要线程安全的场景中,可以使用ConcurrentHashMap
LinkedHashMap: LinkedHashMap是HashMap的一个子类LinkedHashMap中保存了数据的插入顺序.使用Iterator遍历LinkedHashMap时,首先得到的数据一定是首先插入的LinkedHashMap中可以使用带参的构造函数来按照访问次序进行排序
TreeMap: TreeMap实现了SortedMap接口TreeMap可以将保存的数据按照键来进行排序.默认是按照键值进行升序排序,也可以指定排序的比较器.使用Iterator遍历TreeMap时,得到的数据时排序后的数据如果需要使用排序的映射,建议使用TreeMap. 使用TreeMap时,键必须实现Comparable接口或者是在构造TreeMap时传入自定义的Comparator, 否则会在运行时抛出java.lang.ClassCastException异常
Map类都是要求映射中的键key是不可变对象:
不可变对象就是这个对象创建后,对象的hashCode值不会改变
如果对象的hashCode值发生改变,就很可能无法定位到映射的位置HashMap的数据结构HashMap使用数组+链表+红黑树的数据结构存储数据的HashMap的内部数据结构是一个桶数组
每一个桶中存放着一个单链表的头节点
每一个节点中存储着一个键值对EntryHashMap采用拉链法解决存在的Hash冲突问题
拉链法: 也就是链地址法,是数组和链表的结合.在每个数组元素上都有一个链表结构,当数据进行Hash之后,得到数组的下标,然后将数据存放到对应下标元素的链表上
Node: Node是HashMap的一个内部类,实现了Map.Entry接口,本质上就是一个键值对映射HashMap构造函数HashMap中有三个构造函数 : 通常情况下,使用默认的无参构造函数.在能够预估到数据的容量时推荐使用指定容量大小的构造函数public HashMap(); public HashMap(int initialCapacity); public HashMap(int initialCapacity, float loadFactor);构造函数中只是设置了几个参数的值,没有对数组和链表进行初始化,在第一次put操作时才调用resize() 方法初始化数组tab. 这样可以很好的节省空间HashMap函数构造过程: 首先,在数组Node[] table中 length: 初始化长度,默认为16 loadFactor: 负载因子,默认为0.75 默认负载因子0.75是对空间和时间效率的平衡性的选择,不建议修改,只有在时间和空间比较特殊的情况下才需要修改: 内存较多但是对时间效率要求很高: 降低负载因子loadFactor的值 内存紧张但是对时间效率要求不高: 增加负载因子loadFactor的值,这个值可以大于1 threshold: HashMap中能够容纳的最大数据量的键值对Node个数. threshold = length loadFactor : 定义好数组长度之后,负载因子越大,能够容纳的键值对个数越多 threshold是对应的数组长度length和负载因子loadFactor允许的最大元素数量,超过这个数量HashMap就会重新扩容resize. 扩容后的HashMap容量是当前容量的两倍HashMap重要方法hash(K) static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }Java的HashMap中,没有直接使用hashcode() 作为HashMap中的hash值HashMap中将hashcode() 的值无符号右移16位得到一个新值,然后将hashcode() 的值和这个新值进行异或运算得到最终的hash值保存在HashMap中. 这样可以避免哈希碰撞
比如容量大小n为16时 ,n-1为15(0x1111), 散列值真正生效的只是低4位,此时新增的键的hashcode() 的值如果是2,18,34这样以16的倍数为差的等差数列时,就会产生大量的哈希碰撞
使用这样的方法,将高16位和低16位进行异或,因为大部分hashcode() 的值分布已经很均匀了,即使发生碰撞也用O(logn)O(logn)O(logn)时间复杂度的红黑树进行了优化.这样通过使用异或的方法,不仅减少了系统开销,也不会因为tab长度较小时高位没有参与下标的运算引发哈希碰撞put(K, V)使用put(K, V) 操作时 ,HashMap计算键值K的哈希值,然后将这个键值对Entry放入到HashMap中对应的桶bucket上然后寻找以当前桶为头结点的一个单链表,顺序遍历单链表找到某个节点的Entry中的key等于给定的参数K如果找到则将旧的参数值V替换为参数指定的V. 否则直接在链表的尾部插入一个新的Entry节点if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) HashMap中重写equals() 方法必须也要重写hashcode() 方法:
根据hash值,定位到数组某个位置后,向位置中后面的链表添加元素时,判断元素是否一样中,首先判断hash值是否相等,然后再判断equals()
如果只对equals() 进行重写,不对hashcode() 进行重写时,依然会按照不同的两个对象处理,所以重写equals() 方法时必须也要重写hashcode() 方法
HashMap中既要判断hash值,也要使用equals() 方法判断:
HashMap中链表结构进行遍历判断时,重写的equals() 方法判断对象是否相等的业务逻辑比较复杂,这样下来的循环遍历判断影响性能
HashMap中将hash值的判断放在前面,只要hash值不同,整个条件就是false, 不需要进行equals() 方法判断对象是否相等,提升了HashMap的性能
HashMap中是根据hashcode() 的值定位到数组的位置的,同一个数组位置中后面的链表中元素的hashcode() 的值都相同.比较hashcode() 的值没有意义,因为必定相等 .HashMap中没有直接使用hashcode() 的值,用的是对hashcode() 的值进行移位和异或运算后的hash值,这里比较的是元素的hash值resize()初始化HashMap时,按照阈值threshold分配内存如果HashMap中的数据记录超过HashMap的阈值就会进行扩容扩容时,数组会采用将数组容量大小的值左移一位的算法将将数组扩容至两倍扩容时,根据数据的hash值与数组长度进行逻辑与运算,根据运算结果是否为0来决定数据是不动还是将数组索引位置变更为当前索引位置和原数组长度之和扩容时不会重新计算hash值 ,key的hash值会保存在数组位置的后面的node节点元素中treeifyBin()数组中单个链表长度超过8, 数组的长度超过64时才会进行链表结构到红黑树结构的转换,否则只是进行扩容操作HashMap中,使用红黑树结构占用空间大,尽可能不使用红黑树结构get(K)HashMap通过计算键的哈希值,寻找到对应的桶bucket, 然后顺序遍历桶bucket存放的单链表,通过比对Entry的键找到对应的哈希值如果对应位置后面是红黑树结构就在红黑树结构中查找,如果是链表结构就遍历链表,查询需要找的对象 红黑树遍历的时间复杂度 : O(logn)O(logn)O(logn) 链表遍历的时间复杂度 : O(n)O(n)O(n)Hash冲突Hash冲突:
因为Hash是一种压缩映射,这样每一个Entry节点无法对应到一个只属于自身的桶bucket
必然会存在多个Entry共用一个桶bucket, 拉成一个链条的情况.这种情况就是Hash冲突Hash冲突存在的问题:
在Hash冲突的极端情况下,某一个桶bucket后面挂着的链表会特别长,导致遍历的效率很低Hash冲突无法完全避免,为了提高HashMap的性能,需要尽量缓解Hash冲突来缩短每个桶的外挂链表的长度
当HashMap中存储的Entry较多时,需要对HashMap扩容来增加桶bucket的数量 这样对后续要存储的Entry来讲,就会大大缓解Hash冲突HashMap总结HashMap中MAXIMUM_CAPACITY设置为1<<30MAXIUM_CAPACITY:
int类型,表示HashMap的最大容量
使用 << 移位运算的结果不能超过int类型表示的最大值
使用1左移 << 运算时最大只能左移30位,否则就会溢出Java中的int类型占4个字节,每个字节占用8位,所以int类型占用32位Java中的int类型是有符号的,使用第1位作为符号位,此时还有31位,这时使用1左移只能左移30位HashMap中容量设置为2的整数幂次方通过限制一个数组长度length为2的整数幂次方的数,这样使得 (length - 1) & h 和 h % length 的结果是一致的HashMap中将容量设置为2的整数幂次方主要就是为了在取模和扩容时做优化,同时减少冲突 final Node getNode(int hash, Object key) { Node[] tab; Node first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }tab[(n - 1) & hash] : 根据hash值快速定位到数组的位置
数组tab
数组长度n
需要查找的key对应的值hash 因为数组长度n设置为2的整数幂次方,这样初始情况下n-1转换为2进制时各个位上都是1 此时使用 & 与对应的值hash进行运算时的结果就和hash值一样,也就快速定位到了数组中的位置 使得数组中的数据更加分散,减少碰撞如果数组长度不是设置为2的整数幂次方:
数组长度在初始情况下使用n-1转换为2进制时,存在0位,导致很多位置无法放置元素,造成空间浪费
数组的有效使用位置大量减少,增加了碰撞几率,减慢了查询速度HashMap中的负载因子设置为0.75泊松分布: Poisson分布.描述某段时间内,事件具体的发生概率
P ( X = k )= k ! λk e λ , k =0,1,…( λ 是均值, k 为发生次数)
TreeNode占用的空间是常规节点的两倍,所以只有当箱子bin(数组中的一个桶)中元素的数量超过TREEIFY_THRESHOLD时才会需要使用TreeNodeHashMap中的hash值分布比较均匀时,很少使用到TreeNode在随机hashcode情况下 ,bin中节点出现的频率遵循泊松Poisson分布,此时负载因子为 0.75, 均值 λ{lambda}λ 为 0.5如果调整负载因子的值,均值 λ{lambda}λ 会出现较大偏差HashMap扩容到32或者64时,一个箱子bin中存储8个数据量的概率为0.00000006. 所以当一个箱子中节点数目大于等于8个时,可以将HashMap中桶中的数据从链表结构转换为树结构存储,效果是最好的HashMap中的元素尽量使用迭代器Iterator遍历在迭代器Iterator中使用的fail-fast策略,在遍历发生线程并发时,可以立即抛出异常fail-fast策略:
HashMap是非线程安全的
使用迭代器Iterator过程中,如果其余的线程同时也在修改HashMap, 就会立即抛出ConcurrentModificationException异常fail-fast策略实现:
fail-fast策略通过modCount实现
modCount记录修改次数
在迭代器初始化过程中将modCount的值赋值迭代器的expectedModCount
在迭代器迭代过程中,判断modCount和expectModCount是否相等.如果不相等就说明有其余线程对HashMap进行了修改Map hashMap = new HashMap(); Iterator> entries = hashMap.entrySet().iterator(); while (entries.hasNext()) { Map.Entry entry = entries.next(); System.out.println("KEY = " + entry.getKey() + ", VALUE = " + entry.getValue()); }HashMap的使用特点HashMap中的扩容是一个特别损耗性能的操作,所以在初始化HashMap时,应该估算HashMap的大小,确定一个大致的数值,避免在使用HashMap时频繁初始化HashMap是线程不安全的,在并发的环境中建议使用ConcurrentHashMapJava 8中引入红黑树极大程度地优化了HashMap的性能.主要体现在哈希算法不均匀时,也就是拉链法中链表很长时,可以将链表转换为红黑树结构,此时算法复杂度由O ( n )下降为O(logn) O ( logn )
作者:攻城狮Chova
链接:https://juejin.cn/post/7084947689011413028
名门修谱现在读家谱,网谱好?还是纸谱好?家谱树现在看家谱,绝大多数人就只是为了看家族树,好解决一个问题,知道自己到底来自何处。但是在互联网家谱盛行的今天,很多人却说数字化家谱流失掉了传统家谱的灵蕴。对于家谱而言,它只是一
名门修谱稀有姓氏子姓,大多数为少族民族,云南较多子姓先秦时,子姓为大姓。商的始祖契曾任古帝舜的司徒,封于商。契母简狄吞玄鸟(燕)卵而生契。卵,子。契的子孙便以子为姓也有说是舜听说了契母简狄吞卵生子的传说,便赐契姓子,由姬姓改为子
名门修谱互联网家谱,信息数字化才是硬核修家谱在中国,家谱记录一个家族人丁的出生和死亡,记录的时间从近到远可以跨越几代几十代甚至几个世纪。因此,家谱文化的传承,在中国有着举足轻重的意义。从传统家谱到互联网家谱,家谱的发展
名门修谱家谱起源于中国,但有一点,国外做得更好家谱家谱虽然起源于中国,但毫不客气地讲,研究家谱中国却比国外要晚很多。在国外,像日本美国,甚至于英国研究我们的家谱都比较多。当然,台湾学者他们一直在研究,并且两年开一次研讨会,讨论
高通中国区董事长孟樸5G赛道上中国已走到世界前列今年是5G商用元年,随着小米OPPOvivo等众多品牌5G手机的涌现,消费者对于5G的热情不断高涨。另一方面,5G还将进入到制造交通等各行各业,为经济社会发展创造巨大的价值。最近,
大牌乳酸菌饮品哪里找京喜比淘宝还便宜2块钱最近消化总是不好,考虑了下觉得喝点乳酸菌饮品应该有用,既好喝,有营养,还助消化,不过对于这类产品还是买大牌的比较放心。我第一时间就想起了同事上次给我喝的那个伊利的畅意,还挺好喝,然
录制线上流量做回归测试的正确打开方式线上流量什么是录制线上流量回放为什么需要录制线上流量回放项目大迭代更新,容易漏测,或者有很多没用评估到的地方。如果用线上流量做一次回归测试,可以进一步减少bug的风险。大大节省构造
家谱核心是传承家族文化,东方文明的核心呢?家谱,就是家族文化传承的核心。家谱记载的内容,不但是祖宗流传下来的智慧结晶,更是一个家族能够发展绵延不断的根本所在。很多人对于家谱中传承的文化不理解,认为家谱中很多生僻字,甚至完全
修谱时,那些从字辈来寻根的人,要查清楚传统修谱很多家族在修谱时,难免会遇到一些怀抱同姓氏的家谱寻根而来的旁系或直系宗亲。字派在不同的地方也有不同的叫法行派字辈班辈行第派语等,在家谱族谱宗谱中也叫法不一。家谱字辈字辈在家
家谱中始迁祖名人谁影响力更大?家谱家谱应该如何修?修家谱要注意哪一点?为什么修家谱的时候,始迁祖一定要弄清楚。因为只有找对了始迁祖,才能够保证自己的家谱不会错,信息不会乱。家族名人什么是始迁祖?这对于那些不了解
初唐四杰王勃山水田园诗派王维,共一本家谱?王维太乙近天都,连山接海隅。白云回望合,青霭入看无。分野中峰变,阴晴众壑殊。欲投人处宿,隔水问樵夫。一首终南山记录王维曾隐居于长安附近时的心境。王维作为中国山水田园诗派的重要代表人