Map接口主要借助了hash的思想,以hash表键值对的形式存储,键用于hash定位,具有极高的效率。其接口主要实现类如下:
Map
├Hashtable(基本同hashMap,默认为11,只不过hashtable为线程安全的,不允许有null值,put, get 都加锁)
├HashMap(Entry链表+数组,默认容量为16,负载因子为0.75;长度大于n*16*0.75则容量增大一倍)
└LinkedHashMap(底层为hashMap的Entry双向链表,继承自hashMap)
└TreeMap(底层为红黑树实现,继承自AbstractMap,而AbstractMap又实现了Map接口)
└ LinkedHashMap(底层为hash表和双链表表)
├concurrentHashMap(采用锁分离 来保证大并发的效率,Segment数组结构和HashEntry数组结构组成,table[]--hashTable ,segments[]--table;put加锁,get不加锁)
一、Map接口常见实现类介绍
Map底层数据结构为哈希表,用Entry数组表示,Entry数据结构如下:
private static class Entry<K,V> implements Map.Entry<K,V> {
int hash;
K key;
V value;
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}...
}
Entry数组存储示意图如下:
在存储entry时,根据key的hash值定位到Entry数组的相应位置,如果该位置没有元素则直接存入,若该位置有元素则将entry链接至以该位置元素为表头的链表(JDK1.8中此处做了优化,当链表长度超过一定长度时会转变为红黑树存储)。在Entry同一个位置的entry具有相同的key哈希值。在查找相应的元素时,首先计算该entry对象key属性的哈希值,然后根据其hash值定位到Entry数组相应的位置,然后遍历链表并比较entry对象,直到找到相等值。
initialCapacity 和负载因子load factor。当数组容量达到Entry.leng*load factor时,会重新分配一个容量为原来两倍的Entry数组,并将原来Entry数组中的元素重新hash至新的Entry数组。负载因子是时间和空间上的一种折中,负载因子越大表示散列表填充程度越大,反之越小。散列表填充程度越大,发生元素碰撞的概率越大,链表长度也就越长,查找元素时也就越慢。增大负载因子可以减少散列表所占空间,但会增加查询数据的时间开销(put/get均会用到查询);减小负载因子会提高数据查询的性能,但会增加散列表所占用的存储空间。
load factor默认为0.75,可以根据 实际需要适当地调整 load factor 的值;如果程序比较关心空间开销、内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕则可以适当的减少负载因子。通常情况下,程序员无需改变负载因子的值。
1、Hashtable继承自抽象类Dictionary,并实现Map接口
默认大小为11、线程安全(对Entry数组的操作加锁synchronized)、key-value不允许为null、扩容为2*n+1、散列方法是(hash & 0x7FFFFFFF) % tab.length
a、put操作
先根据entry的key哈希值定位到散列表的相应位置,如果该位置具有相同key的元素直接覆盖,如果散列表达到容量极限需要扩容并重新哈希原来的散列表,最后把待插入的entry放入的到相应的位置。
其源码如下:
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;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;//根据key值哈希定位
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {//key已经存在则直接覆盖
V old = e.value;
e.value = value;
return old;
}
}
modCount++;
if (count >= threshold) {//容量达到阈值则扩容极限重新哈希
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
Entry<K,V> e = tab[index];//取出表头元素
tab[index] = new Entry<K,V>(hash, key, value, e);//将元素插入作为新的表头
count++;
return null;
}
hastable扩容:
protected void rehash() {
int oldCapacity = table.length;
Entry[] oldMap = table;//老的散列表
int newCapacity = oldCapacity * 2 + 1;//为保证散列效果,表长度为奇数
Entry[] newMap = new Entry[newCapacity];.//新的散列表,容量为原来的两倍
modCount++;
threshold = (int)(newCapacity * loadFactor);//扩容阈值
table = newMap;//原来的table引用指向新的散列表
//重新hash老的散列表,并将其插入到新的散列表中
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = newMap[index];
newMap[index] = e;
}
}
}
原来的散列表仍然保存,能够保证在扩容时,其他线程正常访问散列表。
b、get操作
先根据key定位到相应的列表,然后遍历列表,找不到返回null
public synchronized V get(Object key) {
Entry tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;//根据key的哈希值定位
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {//遍历列表并比较
return e.value;
}
}
return null;
}
2、HashMap继承自抽象类AbstractMap,并实现Map接口
hash&(length-1)
初始化大小为第一个大于给定值并且为2^n的整数,如果给定大小为20,那么初始化大小为32。初始化源码如下:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Find a power of 2 >= initialCapacity找到第一个大于给定值并且为2^n的整数,为了便于散列,且在定位时低位跟1做位与
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init(); //回调函数,子类实现
}
a、put操作
跟hashtable操作基本类似,散列方式不同、扩容方式不同。根据key的hash值找到散列表中的索引后,会循环遍历table[i]所在链表,若找到已存在key值则直接覆盖,如不存在则通过addEntry添加新对象至链表头部。
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());//二次散列使得散列更加均匀
int i = indexFor(hash, table.length);//根据散列定位 //若i处索引不为null,通过循环不断遍历e的下一个元素
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k; //有相同key则覆盖
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);//回调函数,子类实现
return oldValue;
}
}
//能执行到此处,说明两点1、i处索引为空,2、遍历完链表没有找到与key相同的值
modCount++;
addEntry(hash, key, value, i);//将key、value 添加到索引i处 return null;
}
二次散列:
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
根据哈希值找到索引:
static int indexFor(int h, int length) {
return h & (length-1);//h每一位跟1做与操作,极快
}
添加元素,先添加再扩容
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);//扩容为原来的两倍
}
扩容:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
重新hash:
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;//原来的散列表直接赋值null
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
如果在resize过程中,有其他线程视图调用get遍历数据会在成错误。
hashMap是轻量级的hashTable,主要在hash值和散列定位方面做了优化
hashTable 默认大小为何是11?
hashtable默认大小是11是因为除(近似)质数求余的分散效果好:
Hashtable的扩容是这样做的:
int oldCapacity = table.length;
int newCapacity = oldCapacity * 2 + 1;
虽然不保证capacity是一个质数,但至少保证它是一个奇数。
Hashtable的寻址是这样做的:
Entrytab[]=table;inthash=key.hashCode();intindex=(hash&0x7FFFFFFF)%tab.length;
hashMap 与hashtable区别: 父类不同,线程安全性、hash值、默认长度,扩容大小,null
1、二者继承自不同的类,hashMap继承自AbstractMap ,hashTable继承自Dictionary,但二者都实现了Map接口
2、hashtable是线程安全的
3、二者的散列表长度取法不一样。hashMap默认是16,长度是2^n。hashTable 默认长度为11,且长度是自定义的init*2增长
4、二者的在散列表中的定位不同,hashMap是自定义hash值之后hash&(length-1), hashTable是直接取hashcode然后(hashcode&0X7FFFFFFF)%length
5、hashtable不允许key-Value为null