包括:
一. Map 介绍
二. HashMap 介绍
三. HashTable 介绍
一. Map
Map 是把键映射到值,也就是以一个键值对的形式存储。一个映射不能包含重复的键,一个键只能有一个值。某些映射可以保证其顺序,如TreeMap,某些则不行,如HashMap。
二. HashMap
它是基于哈希表的Map 接口的实现,允许使用null 值和null 键,并且不保证映射顺序。
HashMap 有两个参数影响性能:初始容量和加载因子。加载因子表示哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目超过了容量和加载因子的乘积的时候,就会进行重哈希操作。
加载因子默认为0.75,容量默认为16。加载因子过高,容易产生哈希冲突,加载因子过小,容易浪费空间,0.75是一种折中。
注意:HashMap 不是同步的。
HashMap 的整体思路就是先创建一个 table 数组,然后算出哈希值,找到table 数组特定的位置,找到或者存放该值。另外,由于哈希冲突,该位置可能有一个或者多个值(使用链表法进行连接),还需要进一步判断。
HashMap 有两个参数影响性能:初始容量和加载因子。加载因子表示哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目超过了容量和加载因子的乘积的时候,就会进行重哈希操作。
加载因子默认为0.75,容量默认为16。加载因子过高,容易产生哈希冲突,加载因子过小,容易浪费空间,0.75是一种折中。
注意:HashMap 不是同步的。
HashMap 的整体思路就是先创建一个 table 数组,然后算出哈希值,找到table 数组特定的位置,找到或者存放该值。另外,由于哈希冲突,该位置可能有一个或者多个值(使用链表法进行连接),还需要进一步判断。
主要方法:
put() 方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
在 put() 方法中, 首先会根据 key 算出哈希值,算出之后,再在固定位置进行存放,方法如下:
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);
<span style="color:#3366ff;">//如果此时已经有节点,那么实际上就是出现了哈希冲突,此时再使用链表法解决哈希冲突 </span>
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) {
<span style="color:#3366ff;">p.next = newNode(hash, key, value, null); </span>
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 一个元素的时候,首先检查 该table 数组的该位置上的第一个元素是否相同,如果不同,那么就使用了 for 循环遍历 该位置上的元素(因为可能已经出现了hash 冲突,存在链表)。最后,由 p.next=newNode(....) 可以直达,把新的元素插在该链表的最后。
get() 方法:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//首先会根据哈希值定位到数组中的元素位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//永远都是先检查第一个符不符合
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果不符合,其实这个位置已经有了哈希冲突,那么只能 e = e.next() 一个一个查找
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
总结:
- 可以有空值,可以有空键,但是键不能重复,非同步。
- HashMap由于使用了数组+链表(解决Hash冲突)的方式,所以增删查改的效率比较高。
三. Hashtable
HashTable 和 HashMap 的实现差不多,可以理解为 HashMap 的一个线程安全版本。它同样有初始容量和加载因子,内部也是创建table数组,并且通过哈希算法定位,如果有哈希冲突,也像HashMap一样,采用拉链法进行解决。
get() 方法:
普通方法:
put() 方法:
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
//取得key 的哈希值,也就是在 table 数组中的位置
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
// 取出该值,判断是不是之前是不是有了,有可的话,那么就替换,并且返回旧值
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//如果没有,那么就把该节点插入
addEntry(hash, key, value, index);
return null;
}
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//判断需不需要进行重哈希
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
//把节点插入到第一个位置上,如果已经有值,那么就插在原来的前面;如果没有,那么就直接放入
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
这里注意,在JDK8 中, HashMap 的 put 如果重现hash 冲突,是把新的元素放在最后面,而Hashtable 则是把最新的元素放在第一位。这里会有点区别。
get() 方法:
@SuppressWarnings("unchecked")
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
//定位到该位置
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
//如果该位置上有哈希冲突,也就是有多个值,那么就 for 循环取合适的那个
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
Ps:
HashMap 和 Hashtable 的几个不同点:
- HashMap 允许空值 和空键, 但是 HashTable 则不允许空值 和空键。
- HashMap 不是线程安全, Hashtable 是线程安全的。
- 对于hash冲突,HashMap把最新的元素放在了最后一位,Hashtable 则是把最新的元素放在了最前一位。