Java HashMap 深入源码分析
来源:互联网 发布:php redis 队列算法 编辑:程序博客网 时间:2024/06/13 07:52
源码均以JDK1.8作为参考
Map<K, V>接口是JDK1.2中引入的K,V形式集合约定,此种集合形式为键值对的存储提供了一种可行性实现,在JDK1.0中使用Dictionary及其子类进行此种数据格式的存储,
Dictionary也就是Map<K, V>的前身。
Map<K, V>:
Map<K, V>接口在JDK1.2被引入,此接口中对键值(K, V)形式数据格式的存取定义了一系列的规则,同时也定义了Map<K, V>中元素的基本格式:
interface Entry<K,V> { K getKey(); V getValue(); V setValue(V value); boolean equals(Object o); int hashCode();}
上面这个接口是Map<K, V>的内部接口,规定了Map<K, V>中元素的存取单元,即Entry<K, V>实现类的实例,每一个实现Map<K, V>接口的实现类,都需要自行实现
Entry<K, V>接口,以达到规定实现类内部存取单位的目的。
基于Map<K, V>接口实现的类,存取的最小单元就是Entry<K, V>的实例,同时也是应用中K, V的载体。
HashMap<K, V>:
HashMap<K, V>是Map<K, V>的一个标准实现,在HashMap<K, V>中K, V可以为null,K的null值只允许存在一个,V可以多个。且在get时,若根据K可以取得V,那么返回
V,若取不到V,那么返回NULL.
1.数据结构:
深入了解HashMap<K, V>之前,我们首先需要对HashMap<K, V>的数据结构有一个大致的了解,如下图:
HashMap<K, V>内部结构不像List<E>那么单一,首先HashMap<K, V>内部由一个列表维护其总线结构,数组的每一个索引位置又称为一个桶,这个桶内存储着Node<K,
V>操作单元。
正如上文所说,HashMap<K, V>中实现了存取单元Entry<K, V>,具体实现如下:
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; } 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; }}
2.容量计算:
HashMap<K, V>提供了四个构造方法,如下:
public HashMap():public HashMap(int initialCapacity): public HashMap(int initialCapacity, float loadFactor)public HashMap(Map<? extends K, ? extends V> m):
除了第四种构造函数外,其他的方式初始化HashMap<K, V>时,其内部数组都是为空的,第四种构造函数会根据传入的m参数的大小初始化HashMap<K, V>, HashMap<K,
V>在虚拟机内存足够大时,最大容量可以达到Integer.MAX_VALUE.
对于HashMap<K, V>的增长曲线,HashMap<K, V>中有一个负载因子的概念(loadFactor),这个概念的意思当 因子计算(factor)=集合内元素数量(size)/集合元素上限
(length) ,每次向HashMap<K, V>中put键值对时,若集合内元素 > 集合元素上限(length) * 负载因子(loadFactor)时,就对HashMap<K, V>进行扩容操作,扩展容量为之前的2
倍。
负载因子的作用:loadFactor是可以在初始化时指定的,当loadFactor越趋近于1时,相对来说HashMap<K, V>占用的内存越小,因为此时不需要对其频繁的进行扩容,当
HashMap<K, V>内数据量大时,会比较明显。但是此时一个桶中存储多个元素的几率会上升,导致索引效率变慢。当loadFactor越趋远于1时,相对来说HashMap<K, V>占用的
内存越大,原理同上,此时同一个桶中存储多个元素的情况发生几率会下降,索引效率会上升。
当然这种讨论是基于非Hash碰撞的情况。
3.HashMap<K, V>的Hash特性:
HashMap<K, V>之所以称为HashMap<K, V>,是因为发生put操作时,首先会根据传入的K进行hash计算,在JVM的一次运行状态下,hash值是不会发生改变的。当得到
hash值后,会根据hash & (size - 1)获取到当前K对应的桶的位置,源码片段如下:
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);
由源码可知,例如当K='123',HashMap<K, V>的size为16时,由于K='123'的hashCode为48690,那么可以计算出K='123'对应的桶的位置为48690&15=2,即K为'123'的
这个键值对应该在数组的第二个索引位置。当然这个计算结果会随着HashMap<K, V>的扩容(size)发生变化。
3.Hash之于HashMap<K, V>:
HashMap<K, V>是根据K的Hash来计算桶的位置,或者可以说HashMap<K, V>是根据Hash来计算内部元素的顺序,Hash之于HashMap<K, V>是一个灵魂的存在。
提到HashMap<K, V>,当然会提到Hash碰撞,Hash值不是完完全全的地址,而是地址中的一段值的混淆运算,两个不一样的值的Hash可能会一样,即发生了所谓的Hash碰
撞。当发生Hash碰撞时,HashMap<K, V>会将所有的K,V对存储在一个桶中,而由上面Node<K, V>的定义可知,在桶内元素超过一个以后,会形成一个链表。当这种情况发生
时,整个HashMap<K, V>会退化成一个链表,内部数组不会扩容,疯狂增长的只是其中一个桶内的元素,此时HashMap<K, V>的遍历效率有O(0)下降到O(n)。
Hash碰撞在使用JDK中已经完全实现hashCode的类作为K值时,发生的几率会很小,可以忽略不计,但是当使用自己定义的类作为K值时,就需要特别注意当前类的
hashCode的重写方式,避免Hash碰撞的发生。
4. 一桶多元素示例:
Map<String, String> map = new HashMap<String, String>(); map.put("123rsdfsdrtrt", "www");map.put("tyuytu", "ddd");map.put("123", "123");
"123rsdfsdrtrt".hashCode() != "tyuytu".hashCode();
但是"123rsdfsdrtrt".hashCode() &15 = "tyuytu".hashCode() &15,
此时,"123rsdfsdrtrt"与"tyuytu"元素即放在了HashMap<K, V>底层数组的同一个桶中。
5. 关于HashMap<K, V>遍历方式:
可以通过四种方式遍历HashMap<K, V>对象:
1) for each map.entrySet()
Map<String, String> map = new HashMap<String, String>();for(Entry<String, String> entry: map.entrySet()){ entry.getKey(); entry.getValue();}
2) 显示调用map.entrySet()的集合迭代器
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();while(iteretor.hasNext()){ Map.Entry<String, String> entry = iterator.next(); entry.getKey(); entry.getValue();}
3) for each map.ketSet() 再调用get获取
Map<String, String> map = new HashMap<String, String>();for (String key : map.keySet()) { map.get(key);}
4)for each map.entrySet() 用临时变量保存map.entrySet()
Set<Entry<String, String>> entrySet = map.entrySet();for (Entry<String, String> entry : entrySet) { entry.getKey(); entry.getValue();}
从上可以看出,遍历的主要来源分为两种:keySet和entrySet
如果只是遍历key而无需value的话,直接用keySet, 调用map.keySet()会生成KeyIterator迭代器,其next方法只返回key值。但如果需要value的值,需要重新调用get()。
如果既需要key也需要value,直接用entrySet,调用map.entrySet()会生成EntryInterator迭代器,其next返回一个Entry对象实例,包含key和value。
这两种的方式区别是keySet时,若需要value的值,会调用get(),此时会重新遍历一遍HashMap<K, V>内部的数组table。
HashMap<K, V>的public方法:
Int size(): 获取HashMap<K, V>中元素个数
Boolean isEmpty(): 判断HashMap<K, V>是否为空
V get(Object): 通过指定K获取元素V
Boolean containsKey(Object): 判断是否包含指定K
V put(K, V): 向HashMap<K, V>中加入元素, 返回V
Void pubAll(Map<? Extends K, ? Extends V>): 向其中加入Map<K, V>集合
V remove(Object): 根据指定K移除V,返回被移除的V
Clear(): 清除HashMap<K, V>中所有元素
Boolean containsValue(Object): 判断HashMap<K, V>集合中是否包含值V
Set<K> keySet(): 返回所有Key的Set<E>集合
Collection<V> values(): 返回所有值的集合
Set<Entry<K, V>> entrySet(): 返回所有Entry<K, V>的集合
Object clone():浅复制
在JDK1.8中,对于HashMap<K, V>新增了一些方法,使得某些操作的更加简单,效率更高,如下:
V getOrDefault(Object, V): 获取指定K的值V,若不存在或为null,返回传入的默认值
V putIfAbsent(K, V): 如果指定K的值存在,那么不改变原有的值
Boolean remove(Object, Object): 根据指定K移除V,返回被移除的V。若K对应的V与传入的参数二不等,那么放弃此次操作
Boolean repalce(K, V, V): 替换值,若K对应的V与参数二相等,那么替换为将V替换为参数三,否则放弃此次操作
Boolean repalce(K, V): 替换值,替换K对应的V值为参数二
- Java HashMap 深入源码分析
- java hashmap深入分析
- Java HashMap 源码分析
- java HashMap源码分析
- Java源码分析:HashMap
- Java-HashMap源码分析
- [Java]HashMap源码分析
- Java HashMap源码分析
- 《Java源码分析》:HashMap
- java HashMap源码分析
- java hashmap 源码分析
- 《Java源码分析》:HashMap
- HashMap-Java HashMap实例源码分析
- [java源码分析]HashMap源码分析
- 【Java源码分析】HashMap源码分析
- JAVA源码分析-HashMap源码分析(一)
- java源码分析之HashMap
- Java源码分析之HashMap
- MySQL导入及常用命令
- POJ2299 Ultra-QuickSort
- Android Studio中使用Android5.0新特性CardView
- leetCode 73.Set Matrix Zeroes (矩阵置0) 解题思路和方法
- 【Qt移植到linux】问题6 file not recognized is a directory
- Java HashMap 深入源码分析
- 脑在IDE模式下能正常启动,改成AHCI后蓝屏——解决方法
- Android开发实战之Intent传递对象(Serializable和Parcelable)
- 出现undefined reference to `forkpty' 错误解决方法
- 第三十七讲|用循环处理文字
- 文章标题
- POJ3749
- C#中Invoke的用法(转)
- android下拉刷新(android.support.v4.widget.SwipeRefreshLayout)