理解Java集合(三)Map:HashMapSortedMapTreeMap

更新时间:2023-05-18 20:33:47 阅读: 评论:0

理解Java集合(三)Map:HashMapSortedMapTreeMap Map的系谱图,map下包括HashMap,SortedMap以及TreeMap等
⼀、HashMap是开发中使⽤很频繁的⼀中Map。
1. ⾸先看⼀下其数据结构
实际上是⼀个“链表散列”的数据结构,即数组和链表的结合体。
香蕉的吃法
HashMap底层就是⼀个数组结构,数组中的每⼀项⼜是⼀个链表。当新建⼀个HashMap的时候,就会初始化⼀个数组。
源码如下:
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table;
//Entry就是数组中的元素,每个 Map.Entry 其实就是⼀个key-value对,它持有⼀个指向下⼀个元素的引⽤,这就构成了链表。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
……
}
2.  HashMap的存储实现:
public V put(K key, V value) {
// HashMap允许存放null键和null值。
// 当key为null时,调⽤putForNullKey⽅法,将value放置在数组第⼀个位置。
if (key == null)
return putForNullKey(value);
苏莱曼// 根据key的keyCode重新计算hash值。
int hash = hash(key.hashCode());
// 搜索指定hash值在对应table中的索引。五子棋玩法
int i = indexFor(hash, table.length);
// 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下⼀个元素。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
描写春天的比喻句return oldValue;
}
}
// 如果i索引处的Entry为null,表明此处还没有Entry。
modCount++;
// 将key、value添加到i索引处。
addEntry(hash, key, value, i);
return null;
}
/
**
* 当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有 * 如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。addEntry(hash, key, value, i)⽅法根据计算出的hash值,将key-value对放在数组table的i索 */
void addEntry(int hash, K key, V value, int bucketIndex) {
// 获取指定 bucketIndex 索引处的 Entry
Entry<K,V> e = table[bucketIndex];
// 将新创建的 Entry 放⼊ bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
吃字笔顺table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 如果 Map 中的 key-value 对的数量超过了极限
if (size++ >= threshold)
/
/ 把 table 对象的长度扩充到原来的2倍。
resize(2 * table.length);
}
//当系统决定存储HashMap中的key-value对时,完全没有考虑Entry中的value,仅仅只是根据key来计算并决定每个Entry的存储位置。我们完全可以把 Map 集合中的/**
*    在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。但是HashMap的数据结构是数组和链表的结合 * 对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调⽤ hash(int h) ⽅法所计算得到的 hash 码值总是相同的。我们⾸先想到的就是把hash值对数组 *
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
//这个⽅法⾮常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,⽽HashMap底层数组的长度总是 2 的 n 次⽅,这是HashMap在速度上的优化。
//在 HashMap 构造器中有如下代码:
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
//这段代码保证初始化时HashMap的容量总是2的n次⽅,即底层数组的长度总是为2的n次⽅。当length总是 2 的n次⽅时,h& (length-1)运算等价于对length取模,也就关于最后的初始化是HashMap的容量总是2的n次⽅,⽹上有⼀个说法:
假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:
h & (table.length-1) hash  table.length-1 
8 & (15-1):0100&  1110 = 0100
9 & (15-1):0101&  1110  = 0100
8 & (16-1):0100& 1111= 0100
9 & (16-1): 0101& 1111= 0101
从上⾯的例⼦中可以看出:当它们和15-1(1110)“与”的时候,产⽣了相同的结果,也就是说它们会定位到数组中的同⼀个位置上去,这就产⽣了碰撞,8和9会被放到数组中的同⼀个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进⾏“与”,那么 最后⼀位永远是0,⽽0001,0011,0101,1001,1011,0111,1101这⼏个位置永远都不能存放元素了,空间浪费相当⼤,更糟的是这种情况中,数组可以使⽤的位置⽐数组长度⼩了很多,这意味着进⼀步增加了碰撞的⼏率,减慢了查询的效率!⽽当数组长度为16时,即为2的n次⽅
时,2n-1得到的⼆进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)⽅法对key的hashCode的进⼀步优化,加⼊了⾼位计算,就使得只有相同的hash值的两个值才会被放到数组中的同⼀个位置上形成链表。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的⼏率较⼩,那么数据在数组上
分布就⽐较均匀,也就是说碰撞的⼏率⼩,相对的,查询的时候就不⽤遍历某个位置上的链表,这样查询效率也就较⾼了。
春节广州旅游攻略根据上⾯ put ⽅法的源代码可以看出,当程序试图将⼀个key-value对放⼊HashMap中时,程序⾸先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过equals ⽐较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。如果这两个 Entry 的 key 通过equals ⽐较返回 fal,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,⽽且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() ⽅法的说明。
3.  HashMap的读取实现:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
//从HashMap中get元素时,⾸先计算key的hashCode,找到数组中对应位置的某⼀元素,然后通过key的equals⽅法在对应位置的链表中找到需要的元素。
总结HashMap的存取过程:HashMap 在底层将 key-value 当成⼀个整体进⾏处理,这个整体就是⼀个 Entry 对象。HashMap 底层采⽤⼀个 Entry[] 数组来保存所有的 key-value 对,当需要存储⼀个 Ent
ry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals⽅法决定其在该数组位置上的链表中的存储位置;当需要取出⼀个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals⽅法从该位置上的链表中取出该Entry。
4. HashMap的resize(rehash):
当HashMap中的元素越来越多的时候,hash冲突的⼏率也就越来越⾼,因为数组的长度是固定的。所以为了提⾼查询的效率,就要对HashMap的数组进⾏扩容,数组扩容这个操作也会出现在ArrayList中,这是⼀个常⽤的操作,⽽在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。那么HashMap什么时候进⾏扩容呢?当HashMap中的元素个数超过数组⼤⼩*loadFactor时,就会进⾏数组扩容,loadFactor的默认值为0.75,这是⼀个折中的取值。也就是说,默认情况下,数组⼤⼩为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的⼤⼩扩展为 2*16=32,即扩⼤⼀倍,然后重新计算每个元素在数组中的位置,⽽这是⼀个⾮常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提⾼HashMap的性能。
5. HashMap的性能参数:
HashMap 包含如下⼏个构造器:
1. HashMap():构建⼀个初始容量为 16,负载因⼦为 0.75 的 HashMap。
2. HashMap(int initialCapacity):构建⼀个初始容量为 initialCapacity,负载因⼦为 0.75 的 HashMap。
3. HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因⼦创建⼀个 HashMap。HashMap的基础构造器HashMap(int initialCapacity, float l    initialCapacity:HashMap的最⼤容量,即为底层数组的长度。
loadFactor:负载因⼦loadFactor定义为:散列表的实际元素数⽬(n)/ 散列表的容量(m)。
负载因⼦衡量的是⼀个散列表的空间的使⽤程度,负载因⼦越⼤表⽰散列表的装填程度越⾼,反之愈⼩。对于使⽤链表法的散列表来说,查找⼀个元素的平均时间是  HashMap的实现中,通过threshold字段来判断HashMap的最⼤容量:
threshold = (int)(capacity * loadFactor);
threshold就是在此loadFactor和capacity对应下允许的最⼤元素数⽬,超过这个数⽬就重新resize,以降低实际的负载因⼦。默认的的负见仁见智
载因⼦0.75是对空间和时间效率的⼀个平衡选择。当容量超出此最⼤容量时, resize后的HashMap容量是容量的两倍:
if (size++ >= threshold)
resize(2 * table.length);
6.    Fail-Fast机制:
java.util.HashMap不是线程安全的,因此如果在使⽤迭代器的过程中有其他线程修改了map,那么将抛出
ConcurrentModificationException,这就是所谓fail-fast策略。这⼀策略在源码中的实现是通过modCount域,modCount就是修改次
数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
狗摇尾巴;
}
}
//在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表⽰已经有其他线程修改了
在HashMap的API中指出:
由所有HashMap类的“collection 视图⽅法”所返回的迭代器都是快速失败的:在迭代器创建之后,如果从结构上对映射进⾏修改,除
⾮通过迭代器本⾝的 remove ⽅法,其他任何时间任何⽅式的修改,迭代器都将抛出ConcurrentModifi
cationException。因此,⾯对并
发的修改,迭代器很快就会完全失败,⽽不冒在将来不确定的时间发⽣任意不确定⾏为的风险。 注意,迭代器的快速失败⾏为不能得到保
证,⼀般来说,存在⾮同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最⼤努⼒抛
出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败⾏为应该仅⽤
于检测程序错误。

本文发布于:2023-05-18 20:33:47,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/82/685846.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:数组   元素   位置
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图