内容简介:Map类集合中的存储单位是Key-Value键值对,Map类使用一定的哈希算法形成比较均匀的哈希值作为Key,Value值挂在Key上。1、Key不能重复,Value可重复2、Value可以是List、Map、Set类对象
Map类集合中的存储单位是Key-Value键值对,Map类使用一定的哈希算法形成比较均匀的哈希值作为Key,Value值挂在Key上。
一、Map类特点:
1、Key不能重复,Value可重复
2、Value可以是List、Map、Set类对象
3、KV是否允许为null,以实现类约束为准
二、Map除提供增删改查外,还有三个Map特有方法。
1、返回所有的Key
Set<K> keySet();
返回Map类杜希昂中的Key的Set视图。
2、返回所有value
Collection<V> values();
返回Map类对象中的所有Value的Collection视图。
3、返回所有K-V键值对
Set<Map.Entry<K, V>> entrySet();
返回Map类对象中的K-V键值对的Set视图。
这些函数返回的视图支持清除操作(remove、clear),不支持修改和添加元素。
三、主要的Map类集合(图来自 Java 开发手册pdf)
Hashtable逐渐弃用,ConcurrentHashMap多线程比HashMap安全,但本文主要分析HashMap。
-------------------------------------------------------------------------------------------
HashMap
一、哈希类集合的三个基本存储概念
| 名称 | 说明 |
| table | 存储所有节点数据的数组 |
| slot | 哈希槽。即table[i]这个位置 |
| bucket | 哈希桶。table[i]上所有元素形成的表或数的集合 |
图示:
链表Node“Node是HashMap的一个静态内部类。
>//>Node是单向链表,实现了Map.Entry接口
>static >class Node<K,V> >implements Map.Entry<K,V> {
>final >int hash;
>final K key;
V value;
Node<K,V> next;
>//>构造函数
Node(>int hash, K key, V value, Node<K,V> next) {
>this.hash = hash;
>this.key = key;
>this.value = value;
>this.next = next;
}
>//> getter and setter ... toString ...
>public >final K getKey() { >return key; }
>public >final V getValue() { >return value; }
>public >final String toString() { >return key + "=" + value; }
>public >final >int hashCode() {
>return Objects.hashCode(key) ^ Objects.hashCode(value);
}
>public >final V setValue(V newValue) {
V oldValue = value;
value = newValue;
>return oldValue;
}
>public >final >boolean equals(Object o) {
>if (o == >this)
>return >true;
>if (o >instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
>if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
>return >true;
}
>return >false;
}
}
红黑树TreeNode:通过特定着色的旋转(左旋、右旋)来保证从根节点到叶子节点的最长路径不超过最短路径的2倍的二叉树,相比AVL树,更加高效的完成插入和删除操作后的自平衡调整。最坏运行时间为O(logN).
>static >final >class TreeNode<K,V> >extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; >//> red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; >//> needed to unlink next upon deletion
>boolean red;
TreeNode(>int hash, K key, V val, Node<K,V> next) {
>super(hash, key, val, next);
}
>/**>
* Returns root of tree containing this node.
>*/
>final TreeNode<K,V> root() {
>for (TreeNode<K,V> r = >this, p;;) {
>if ((p = r.parent) == >null)
>return r;
r = p;
}
}
二、HashMap定义变量
>/**> * 默认初始容量16(必须是2的幂次方) >*/ >static >final >int DEFAULT_INITIAL_CAPACITY = 1 << 4; >/**> * 最大容量,2的30次方 >*/ >static >final >int MAXIMUM_CAPACITY = 1 << 30; >/**> * 默认加载因子,用来计算threshold >*/ >static >final >float DEFAULT_LOAD_FACTOR = 0.75f; >/**> * 链表转成树的阈值,当桶中链表长度大于8时转成树 threshold = capacity * loadFactor >*/ >static >final >int TREEIFY_THRESHOLD = 8; >/**> * 进行resize操作时,若桶中数量少于6则从树转成链表 >*/ >static >final >int UNTREEIFY_THRESHOLD = 6; >/**> * 桶中结构转化为红黑树对应的table的最小大小 当需要将解决 hash 冲突的链表转变为红黑树时, 需要判断下此时数组容量, 若是由于数组容量太小(小于 MIN_TREEIFY_CAPACITY ) 导致的 hash 冲突太多,则不进行链表转变为红黑树操作, 转为利用 resize() 函数对 hashMap 扩容 >*/ >static >final >int MIN_TREEIFY_CAPACITY = 64; >/**> 保存Node<K,V>节点的数组 该表在首次使用时初始化,并根据需要调整大小。 分配时, 长度始终是2的幂。 >*/ >transient Node<K,V>[] table; >/**> * 存放具体元素的集 >*/ >transient Set<Map.Entry<K,V>> entrySet; >/**> * 记录 hashMap 当前存储的元素的数量 >*/ >transient >int size; >/**> * 每次更改map结构的计数器 >*/ >transient >int modCount; >/**> * 临界值 当实际大小(容量*负载因子)超过临界值时,会进行扩容 >*/ >int threshold; >/**> * 负载因子 >*/ >final >float loadFactor;
默认容量:16 DEFAULT_INITIAL_CAPACITY = >1 << >4
自定义初始化容量:构造函数 ↓
Map容量一定为2的幂次。
默认加载因子:0.75 DEFAULT_LOAD_FACTOR = >0.75f
>自定义负载因子:构造函数 ↓
>桶中节点从链表转化为红黑树:节点数大于8
>桶中元素从红黑树返回为链表:节点数小于等于6
>threshold:临界值 = 容量×负载因子,当实际容量大于临界值,为了减小哈希冲突,进行扩容。
三、构造函数
>/**>
* 传入初始容量大小,使用默认负载因子值 来初始化HashMap对象
>*/
>public HashMap(>int initialCapacity) {
>this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
>/**>
* 默认容量和负载因子
>*/
>public HashMap() {
>this.loadFactor = DEFAULT_LOAD_FACTOR; >//> all other fields defaulted
}
>/**>
* 传入初始容量大小和负载因子 来初始化HashMap对象
>*/
>public HashMap(>int initialCapacity, >float loadFactor) {
>//> 初始容量不能小于0,否则报错
>if (initialCapacity < 0)
>throw >new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
>//> 初始容量不能大于最大值,否则为最大值
>if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
>//>负载因子不能小于或等于0,不能为非数字
>if (loadFactor <= 0 || Float.isNaN(loadFactor))
>throw >new IllegalArgumentException("Illegal load factor: " +
loadFactor);
>//> 初始化负载因子
>this.loadFactor = loadFactor;
>//> 初始化threshold大小
>this.threshold = tableSizeFor(initialCapacity);
}
>/**>
* 找到大于或等于 cap 的最小2的整数次幂的数。
>*/
>static >final >int tableSizeFor(>int cap) {
>int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
>return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
tableSizeFor(int cap):用位运算找到大于或等于 cap 的最小2的整数次幂的数。比如10,则返回16。
四、put函数
>public V put(K key, V value) {
>//> 调用hash(key)方法来计算hash
>return putVal(hash(key), key, value, >false, >true);
}
>final V putVal(>int hash, K key, V value, >boolean onlyIfAbsent,
>boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
>int n, i;
>//> 容量初始化:当table为空,则调用resize()方法来初始化容器
>if ((tab = table) == >null || (n = tab.length) == 0)
n = (tab = resize()).length;
>//>确定元素存放在哪个桶中,桶为空,新生成结点放入桶中
>if ((p = tab[i = (n - 1) & hash]) == >null)
tab[i] = newNode(hash, key, value, >null);
>else {
Node<K,V> e; K k;
>//> 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
>if (p.hash == hash &&
((k = p.key) == key || (key != >null && key.equals(k))))
>//>如果键的值以及节点 hash 等于链表中的第一个键值对节点时,则将 e 指向该键值对
e = p;
>//> 如果桶中的引用类型为 TreeNode,则调用红黑树的插入方法
>else >if (p >instanceof TreeNode)
>//> 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(>this, tab, hash, key, value);
>else {
>//>对链表进行遍历,并统计链表长度
>for (>int binCount = 0; ; ++binCount) {
>//> 到达链表的尾部
>if ((e = p.next) == >null) {
>//>在尾部插入新结点
p.next = newNode(hash, key, value, >null);
>//> 如果结点数量达到阈值,转化为红黑树
>if (binCount >= TREEIFY_THRESHOLD - 1) >//> -1 for 1st
treeifyBin(tab, hash);
>break;
}
>//> 判断链表中结点的key值与插入的元素的key值是否相等
>if (e.hash == hash &&
((k = e.key) == key || (key != >null && key.equals(k))))
>break;
p = e;
}
}
>//>判断要插入的键值对是否存在 HashMap 中
>if (e != >null) { >//> existing mapping for key
V oldValue = e.value;
>//> onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
>if (!onlyIfAbsent || oldValue == >null)
e.value = value;
afterNodeAccess(e);
>return oldValue;
}
}
++modCount;
>//> 键值对数量超过阈值时,则进行扩容
>if (++size > threshold)
resize();
afterNodeInsertion(evict);
>return >null;
}
在new HashMap() 完成后,若没有put操作,是不会分配存储空间的。
-
当桶数组 table 为空时,通过扩容的方式初始化 table
-
查找要插入的键值对是否已经存在,存在的话根据条件判断是否用新值替换旧值
-
如果不存在,则将键值对链入链表中,并根据链表长度决定是否将链表转为红黑树
-
判断键值对数量是否大于阈值,大于的话则进行扩容操作
五、hash值的计算
- 根据存入的key-value对中的key计算出对应的hash值,然后放入对应的桶中,所以好的hash值计算方法十分重要,可以大大避免哈希冲突。
-
HashMap是以hash操作作为散列依据。但是又与传统的hash存在着少许的优化。其hash值是key的hashcode与其hashcode右移16位的异或结果。在put方法中,将取出的hash值与当前的hashmap容量-1进行与运算。得到的就是位桶的下标。那么为何需要使用key.hashCode() ^ h>>>16的方式来计算hash值呢。其实从微观的角度来看,这种方法与直接去key的哈希值返回在功能实现上没有差别。但是由于最终获取下表是对二进制数组最后几位的与操作。所以直接取hash值会丢失高位的数据,从而增大冲突引起的可能。由于hash值是32位的二进制数。将高位的16位于低位的16位进行异或操作,即可将高位的信息存储到低位。因此该函数也叫做扰乱函数。目的就是减少冲突出现的可能性。而官方给出的测试报告也验证了这一点。直接使用key的hash算法与扰乱函数的hash算法冲突概率相差10%左右。
>static >final >int hash(Object key) {
>int h;
>return (key == >null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
n = table.length;
index = (n-1) & hash;
- 根据以上可知,hashcode是一个32位的值,用高16位与低16位进行异或,原因在于求index是是用 (n-1) & hash ,如果hashmap的capcity很小的话,那么对于两个高位不同,低位相同的hashcode,可能最终会装入同一个桶中。那么会造成hash冲突,好的散列函数,应该尽量在计算hash时,把所有的位的信息都用上,这样才能尽可能避免冲突。这就是为什么用高16位与低16位进行异或的原因。
- 为什么capcity是2的幂?因为 算index时用的是
(n-1) & hash,这样就能保证n -1是全为1的二进制数,如果不全为1的话,存在某一位为0,那么0,1与0与的结果都是0,这样便有可能将两个hash不同的值最终装入同一个桶中,造成冲突。所以必须是2的幂。例子:十进制16 转化为 二进制10000,则16-1为 1111 -
在算index时,用位运算
(n-1) & hash而不是模运算hash % n的好处(在HashTable中依旧是取模运算)?- 位运算消耗资源更少,更有效率
- 避免了hashcode为负数的情况
六、扩容机制resize
在 HashMap 中,桶数组的长度均是2的幂,阈值大小为桶数组长度与负载因子的乘积。当 HashMap 中的键值对数量超过阈值时,进行扩容。
HashMap 按当前桶数组长度的2倍进行扩容,阈值也变为原来的2倍(如果计算过程中,阈值溢出归零,则按阈值公式重新计算)。扩容之后,要重新计算键值对的位置,并把它们移动到合适的位置上去。
>final Node<K,V>[] resize() {
>//> 拿到数组桶
Node<K,V>[] oldTab = table;
>int oldCap = (oldTab == >null) ? 0 : oldTab.length;
>int oldThr = threshold;
>int newCap, newThr = 0;
>//> 如果数组桶的容量大与0
>if (oldCap > 0) {
>//> 如果比最大值还大,则赋值为最大值
>if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
>return oldTab;
}
>//> 如果扩容后小于最大值 而且 旧数组桶大于初始容量16, 阈值左移1(扩大2倍)
>else >if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; >//> double threshold
}
>//> 如果数组桶容量<=0 且 旧阈值 >0
>else >if (oldThr > 0) >//> initial capacity was placed in threshold
>//> 新容量=旧阈值
newCap = oldThr;
>//> 如果数组桶容量<=0 且 旧阈值 <=0
>else { >//> zero initial threshold signifies using defaults
>//> 新容量=默认容量
newCap = DEFAULT_INITIAL_CAPACITY;
>//> 新阈值= 负载因子*默认容量
newThr = (>int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
>//> 如果新阈值为0
>if (newThr == 0) {
>//> 重新计算阈值
>float ft = (>float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (>float)MAXIMUM_CAPACITY ?
(>int)ft : Integer.MAX_VALUE);
}
>//> 更新阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
>//> 创建新数组
Node<K,V>[] newTab = (Node<K,V>[])>new Node[newCap];
>//> 覆盖数组桶
table = newTab;
>//> 如果旧数组桶不是空,则遍历桶数组,并将键值对映射到新的桶数组中
>if (oldTab != >null) {
>for (>int j = 0; j < oldCap; ++j) {
Node<K,V> e;
>if ((e = oldTab[j]) != >null) {
oldTab[j] = >null;
>if (e.next == >null)
newTab[e.hash & (newCap - 1)] = e;
>//> 如果是红黑树
>else >if (e >instanceof TreeNode)
>//> 重新映射时,需要对红黑树进行拆分
((TreeNode<K,V>)e).split(>this, newTab, j, oldCap);
>else { >//> preserve order
>//> 如果不是红黑树,则按链表处理
Node<K,V> loHead = >null, loTail = >null;
Node<K,V> hiHead = >null, hiTail = >null;
Node<K,V> next;
>//> 遍历链表,并将链表节点按原顺序进行分组
>do {
next = e.next;
>if ((e.hash & oldCap) == 0) {
>if (loTail == >null)
loHead = e;
>else
loTail.next = e;
loTail = e;
}
>else {
>if (hiTail == >null)
hiHead = e;
>else
hiTail.next = e;
hiTail = e;
}
} >while ((e = next) != >null);
>//> 将分组后的链表映射到新桶中
>if (loTail != >null) {
loTail.next = >null;
newTab[j] = loHead;
}
>if (hiTail != >null) {
hiTail.next = >null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
>return newTab;
}
整体步骤:
-
计算新桶数组的容量 newCap 和新阈值 newThr
-
根据计算出的 newCap 创建新的桶数组,桶数组 table 也是在这里进行初始化的
-
将键值对节点重新映射到新的桶数组里。如果节点是 TreeNode 类型,则需要拆分红黑树。如果是普通节点,则节点按原顺序进行分组。
七、常用Map遍历方法
>public >class Test {
>public >static >void main(String[] args) {
List list = >new ArrayList();
Map<String, String> map = >new HashMap<String, String>();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");
>//>第一种:普遍使用,二次取值
System.out.println("通过Map.keySet遍历key和value:");
>for (String key : map.keySet()) {
System.out.println("key= " + key + " and value= " + map.get(key));
}
>//>第二种:推荐,尤其是容量大时
System.out.println("通过Map.entrySet遍历key和value");
>for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry);
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
}
}
附:
1、JDK.7是基于数组加单链表实现(为什么不要双链表)
首先,用链表是为了解决hash冲突。
单链表能实现为什么要用双链表呢?(双链表需要更大的存储空间)
2、为什么要用红黑树,不是平衡二叉树?
插入效率比平衡二叉树高,查询效率比普通二叉树高。所以选择性能相对折中的红黑树。
3、重写对象的equals时一定需要重写hashcode,为什么?
判断两个对象是否相同,首先判断两个对象的hashcode是否相等,若不相等,直接返回false;若相等,使用equals判断。
即equals判断相等,则hashcode一定相等,hashcode相等,他们并不一定相同。
4、为什么默认加载因子为0.75?
调低负载因子时,HashMap 所能容纳的键值对数量变少。扩容时,重新将键值对存储新的桶数组里,键的键之间产生的碰撞会下降,链表长度变短。此时,HashMap 的增删改查等操作的效率将会变高,这里是典型的拿空间换时间。
相反,如果增加负载因子(负载因子可以大于1),HashMap 所能容纳的键值对数量变多,空间利用率高,但碰撞率也高。这意味着链表长度变长,效率也随之降低,这种情况是拿时间换空间。至于负载因子怎么调节,这个看使用场景了。
一般情况下,我们用默认值就可以了。大多数情况下0.75在时间跟空间代价上达到了平衡所以不建议修改。
Linux公社的RSS地址 : https://www.linuxidc.com/rssFeed.aspx
本文永久更新链接地址: https://www.linuxidc.com/Linux/2019-04/158351.htm
以上所述就是小编给大家介绍的《Java集合之HashMap详解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Scala 中的集合(二):集合性能比较
- Scala 中的集合(二):集合性能比较
- 《面试知识,工作可待:集合篇》:Java 集合面试知识大全
- 如何对集合对象求合计,然后追加在该集合对象中
- MongoDB指南---14、特殊的索引和集合:固定集合、TTL索引、全文本索引
- Python 集合相关操作
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java Servlet & JSP Cookbook
Bruce W. Perry / O'Reilly Media / 2003-12-1 / USD 49.99
With literally hundreds of examples and thousands of lines of code, the Java Servlet and JSP Cookbook yields tips and techniques that any Java web developer who uses JavaServer Pages or servlets will ......一起来看看 《Java Servlet & JSP Cookbook》 这本书的介绍吧!