Hash表与HashMap知识总结 - Xiasm/Java-Android-Learn GitHub Wiki
上面我总结了顺序表和链表,大的方面来说,顺序表优点有查找元素很容易(寻址容易),尾插、尾删容易,缺点有头插、中间插或删除需要移动下标被影响的元素,性能消耗较大;链表的优点有插入或删除一个元素很容易,缺点是查找元素需要从头或尾遍历找到,那么有没有一种数据结构集合两者的优点,既实现查找元素容易,且插入或删除元素也容易呢?答案是肯定的,下面我们就来讲解一下这种数据结构。
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
百度百科上有这么一句解释我觉得很好“给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。”,那么只要通过特定的算法,在表的内存区域中能从关键码快速的找到关键值,我们就可以称这种数据结构为Hash表。总而言之,hash表有两个重要的角色,一个是通过key找到value的函数,另一个就是存放数据的表。
注意,这个函数的结果是有要求的,就是结果是尽可能的散列开来的,那么如果有两个不同的关键码通过这个函数求得相同结果怎么办呢?这个也需要考虑处理方式。
HashMap是Hash表的一种实现,下面我就通过HashMap原理的分析来一步步带大家了解Hassh表。
- HashMap是如何存储的? 首先我们看put()方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//HashMap中的hash算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//通过hash值将value放入特定的区域
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;
}
putVal()方法很长,我们不再详细介绍,它的目的就是将value按照hash函数的散列结果放入特定的内存中。下面我们来看关键点:putVal()方法中有一个变量table,看下它的类型:
transient Node<K,V>[] table;
table是Node<K,V>类型的,我们接着看Node类
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;
}
}
Node类是一个链表,它有个属性next指向一个Node,并且Node里面存储着当前Value的Key和hash值,所以可以看出来,整个HashMap的存储结构就是一个顺序表和链表的组合,好多人称之为拉链。用一个图来表示:
由图我们可以看出,HashMap在处理hash值冲突的解决办法是,如果两个key计算得到的哈希值相同,那么就在数组当前索引加入一个链表,依次存储hash值相同的key-value。
这样的数据结构有没有什么缺点呢?缺点也是有的,其一就是这种结构增大了内存开销,但这不是最明显的缺点,最明显的缺点就是数组的长度是固定的,如果我们存入的键值对超过了数组的长度怎么办呢?肯定要队数组扩容,而这个扩容的方法是最复杂的,因为一旦扩容之后就需要重新计算每个Key在新数组里的hash值,然后将键值对放入对应的位置中。
所以,我们在用HashMap时候,最好要知道我们大概有多少数据,在初始化的时候指定好HashMap的容量。