源码均以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值为参数二