之前我们看过了两种类型的集合,ArrayList集合和LinkedList集合,两种集合各有优势,我们不具体说了,但是本篇要看的集合可以完成它们完成不了的任务。比如:现有一篇文章,要你统计其中出现了哪些单词,每个单词总共出现了几次。这个问题很明显需要记录两个变量(某单词及其出现次数),但是我们之前介绍的集合都只能同时存储一种类型的变量,无法实现对应的效果。
我们的HashMap又可以叫做键值对集合(官方名称映射),比如:
public class Test_Class {
public static void main(String[] args){
HashMap<String,Integer> map = new HashMap<String, Integer>();
map.put("hello",10);
}
}
/*第一条记录,保存了hello这个单词在文章中总共出现10次*/
一、超接口Map
想要研究一个具体的类,先要看看它的父类或者父接口。HashMap实现了Map<K,V>接口,K表示键,V表示值,一个键对应一个值,整个集合中键是唯一的不可重复。
public interface Map<K,V> {
int size();
boolean isEmpty();
boolean containsKey(Object key);
boolean containsValue(Object value);
V get(Object key);
V put(K key, V value);
V remove(Object key);
void putAll(Map<? extends K, ? extends V> m);
void clear();
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
......
//源码中方法很多,我们简单介绍几个常用的
}
接口中定义了 V get(Object key); 方法用来根据指定的键值,返回该键所对应的value值,有方法 V put(K key, V value); 添加指定键值和value的一条记录到集合中,有方法 V remove(Object key); 根据指定的value值删除一条或者多条记录等等,当然这些方法都只是声明,我们马上可以看看HashMap对这些方法的具体的实现是什么。
二、HashMap的实现原理
HashMap是Map的一种实现,它具体实现了上述Map接口中的所有方法。我们看看它的内部是以什么样的形式组织和存储内容的。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Node<K,V>[] table;
transient int size;
transient int modCount;
final float loadFactor;
/*成员内部类,很重要的类*/
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;
}
}
}
上述代码主要从代码的角度展现了HashMap的内部数据是以什么样的形式来存储的。我们可以看到,HashMap中定义了初始容量为16,最大容量,默认的 DEFAULT_LOAD_FACTOR(具体是什么下面再说) 参数值,实际上HashMap内部存储为数组加链表,定义了一个Node类数组,每个Node结点指向一个链表的头部串联整个链表。(单向链表)
Node结点其实构成的是一个单向链表,每个结点由 final K key;,不可更改的键,可以修改的 V value; 值,还有指向下一个结点的指针Node<K,V> next; 组成,当然还包含了对结点的操作方法。
三、添加元素到集合中
我们使用put方法添加元素到集合中:
public V put(K key, V value) {
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;
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;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
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;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
接下来我们来解释一下整体的调用过程,首先在调用put方法时候,调用了另外的一个方法 putVal(看起来十分恶心),传过去一个key个hash值,还有key和value,还有两个布尔参数(后面我们会知道他们的作用)。
首先if判断这个HashMap是否是null,如果是的话,调用resize方法初始化一个HashMap(容量为16,loadFactor参数为0.75等其他参数值),然后 (p = tab[i = (n - 1) & hash]) == null 用我们想要插入的元素的hash值和HashMap容量取模判断将要存储的位置,因为我们知道每个对象的hash值是不同的,所以在插入元素的时候用他们的hash值和整个HashMap数组长度取模使得他们根据自己的hash存储在HashMap的固定位置上,如果我们将要插入节点的位置上是空的就 tab[i] = newNode(hash, key, value, null);,新建节点直接存放在此位置上。
假如我们使用hash取模之后得到的索引位置为3,发现此处为空,于是占用他。
我们接着看,刚才是预定的位置没被使用,那如果已经被占用了怎么办?
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
这行代码紧接着,对占用我们位置的结点键的值和我们的键的值进行比较,如果一样说明,这是对这条记录的修改,于是准备覆盖原位置的结点将p赋值给e。紧接着的一行代码可以先跳过设计到树,最后一个else中主要是将我们将要插入的结点链接到某个链表上。方法的尾部判断e是否为空,如果不为空说明在某条链表中找到与我们将要插入的结点的键的值一样的结点(就是需要覆盖原链表中某个结点的值),于是覆盖某个结点的值返回该结点的值。当然方法的最后对容量进行了判断,如果超过需要扩充的界限就会调用方法扩充HashMap数组的长度。
最后总结一下整个put方法的几个重要的过程:
- 判断HashMap是否为空,是就初始化一个HashMap
- 判断预定位置是否被占用,未被占用则占用
- 预定位置被占用则判断是否有键值相等的结点,有则覆盖
- 遍历之后没有找到键值相等的结点,就将此结点链接在某链表的末尾
- 判断容量是否越界,是则扩充容量
四、删除操作
remove方法的源码如下:
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/*主要还是调用方法removeNode*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
我们具体看看代码,第一个if语句主要是判断:如果当前的HashMap不为null并且将要删除的结点的key和capcity取模后存在于HashMap数组中,就执行余下的代码。(主要还是为了保证将要删除的元素是存在的),接下来的一句if判断主要是,如果第一个位置的结点就是我们将要删除的结点的话,就将此结点赋值node,接下来就是遍历整个链表查找将要删除的键值,如果找到赋值给node。最后根据不同情况进行删除操作,如果p=node 说明将要删除的结点是第一个结点直接将此位置赋null即可,否则,将p的next赋null即可(经过遍历p始终指向将要删除的前一个结点),最后完成删除。
最后小结一下整个删除操作的过程:
- 传入指定的键值,将键值及其hash值传入方法removeNode
- 通过判断HashMap是否为null以及hash取模之后的位置是否为null,提高函数执行效率
- 在链表中寻找键值,并且做好标记准备删除
- 删除元素结点
至于get查找返回指定的值,是否包含指定键,是否包含指定值等方法类似。
最后,因为HashMap存取没有顺序,如果对于一些需要按键存取数值的数据并且对存取顺序没有要求的话,可以考虑使用HashMap提升效率。
本篇文章是博主查看jdk源码总结而来,如果有不当之处,望大家指出来。