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值为参数二

0 0