HashMap具有以下特点:
- Hashmap是基于Map的非同步实现,如果多线程修改,必须在外部保持同步
- 允许使用null值和null键
- 不保证映射顺序
- Hashmap实际上是一个链表散列的数据结构,即数组和链表的结合体
- Hashmap底层是一个数组,数组中每一项又是一个链表
- 默认容量 = 4(必须是2的n次方), 默认加载因子 = 0.75
- 只有对于HashMap中的Entry有新增或删除的操作,都会更新modCount
假定Hash函数将元素适当的分布在各桶之间,可为基本操作
get
和put
提供稳定的性能,迭代collection视图所需的时间与HashMap实例的容量(桶的数量)及其大小(键值映射关系数)成比例,所以如果迭代性能很重要,则不要将出示容量设置的太高或将加载因子设置的太低
成员变量
//默认初始容量为4
staic final int DEFAULT_INITIAL_CAPACITY = 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final HashMapEntry<?,?>[] EMPTY_TABLE = {};
transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;
transient int size;
int threshold;
final float loadFactor = DEFAULT_LOAD_FACTOR;
transient int modCount;
构造函数
public HashMap() {
//默认容量是4, 加载因子是0.75
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
} else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
initialCapacity = DEFAULT_INITIAL_CAPACITY;
}
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
threshold = initialCapacity;
init();
}
一般情况下,我们使用HashMap时都会调用空参构造函数,可以看到,空参构造函数的初始容量默认为4
方法
1. put
public V put(K key, V value) {
//如果table是空,重新初始化table,容量是threshold,如果是空构造函数,threshold就是4
if (table == EMPTY_TABLE) {
//【1.1】
inflateTable(threshold);
}
//如果key是null, 更新相应的Entry, key = null, 则Entry放在table[0]
if (key == null)
//【1.2】
return putForNullKey(value);
//计算hash值, 【1.3】
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
//获取hash值对应的index位置, 【1.4】
int i = indexFor(hash, table.length);
//如果对应的index位置已经有链表,证明hash冲突,遍历链表
for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果链表中某一个entry的key值与传入的key值相等,更新entry对应的value值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//如果没有hash冲突或者在hash冲突的链表中没有相等的key值,更新modCount
modCount++;
//添加新的Entry, 【1.5】
addEntry(hash, key, value, i);
return null;
}
- key = null时,对应的Entry放置在索引0处
- 如果key值存在了,新的value会替换旧的value,
put
方法会返回旧value - 计算对应的索引值的方法是, 获取key值hash值,然后用
hash & table.length - 1
, table.length总是2的n次方
对于任意给定对象,只要其
hashCode()
返回值相同,那么HashMap计算出来的hash值也总相同
1.1 inflateTable
private void inflateTable(int toSize) {
// 保证HashMap的容量是2的n次方
int capacity = roundUpToPowerOf2(toSize);
//计算threshold值
float thresholdFloat = capacity * loadFactor;
if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
thresholdFloat = MAXIMUM_CAPACITY + 1;
}
threshold = (int) thresholdFloat;
table = new HashMapEntry[capacity];
}
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
int rounded = number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (rounded = Integer.highestOneBit(number)) != 0
//Integer.bitCount返回number的二进制补码中1的总数量
? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
: 1;
return rounded;
}
HashMap的容量会强制性的圆整到2的n次方,即使在初始化时传入的初始容量不是2的n次方,这样做是为了减少hash碰撞率从而提升空间利用效率
1.2 Hashing.singleWordWangJenkinsHash
public static int singleWordWangJenkinsHash(Object k) {
int h = k.hashCode();
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10); //无符号右移6位
h += (h << 3);
h ^= (h >>> 6);//无符号右移6位,空位以0补齐, 带符号右移是根据最左边的符号位来确定空位补什么,如果符号位是1,则空位补1,符号位是0,空位补0
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
1.3 putForNullKey
private V putForNullKey(V value) {
//如果null已经有对应的value,则替换旧value为新value
for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
null可以做键值,但是null所对应的value每次只能存一个,如果已存在以null为键值的Entry, 会有新的value替换旧value
1.4 indexFor
static int indexFor(int h, int length) {
//length总是2的n次方时, h & (length - 1)等价于对length取模,h % length
return h & (length-1);
}
由于pow(2, n)是2的n次方,所以pow(2,n) - 1的二进制必然每一位都是1,这样用hash值同pow(2,n)-1进行位与操作,得到的值得低位与hash值得低位一致
1.5 addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//如果key-value键值对数量达到threshold且对应index处有Entry即存在链表, 扩容
resize(2 * table.length);
//计算hash值,null键值的hash是0
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//创建新的Entry,并放入对应的位置
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> e = table[bucketIndex];
table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
size++;
}
- 如果key-value键值对的总和达到
threshold
且对应的索引处存在链表,进行扩容 - 创建新的Entry,并放入
table
中
2. get
public V get(Object key) {
//如果key为null, 获取null对应的value
if (key == null)
//【2.1】
return getForNullKey();
//【2.2】
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
2.1 getForNullKey
private V getForNullKey() {
if (size == 0) {
return null;
}
for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
2.2 getEntry
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
这里吐槽一下:曾经遇到一个面试官问我如果哈希冲突的时候HashMap是通过什么方式获取value的,我很确定的说会遍历链表,然后判断entry的哈希值是否跟key值计算出来的hash值相等同时会判断entry的key值和传入的key是否会相等,然后那个面试官告诉我不对!!!弄得我一脸懵逼,我说就是这样的啊,结果他可能觉得我知错不改,脸色不太好的告诉我回去再好好看看。。。好吧,我现在再一次好好看了,依然不知道我哪里说错了,也许这位大哥可能有自己独特的理解
3. remove
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.getValue());
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
HashMapEntry<K,V> prev = table[i];
HashMapEntry<K,V> e = prev;
while (e != null) {
HashMapEntry<K,V> next = e.next;
Object k;
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
删除元素其实也是首先计算has值,之后会根据hash值回去元素的索引值,之后遍历索引位置处的链表,之后再进行entry的比对,如果想等,就会删除这个元素,这里要注意的是之前提到过的一旦HashMap中的元素发生删除或增加的改变时,会增加modCount
4. 遍历HashMap
一般遍历HashMap都是通过迭代器来遍历:
Iterator iter = map.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Object key = entry.getKey();
Object val = entry.getValue();
}
用迭代器来遍历Hashmap时,不可以调用put或remove这样会更改HashMap中Entry数量的操作,如果调用了,会抛出ConcurrentModificationException,这是因为
fast-fail
机制,至于具体原因,从下面的代码分析中可以知道
4.1 entrySet
public Set<Map.Entry<K,V>> entrySet() {
return entrySet0();
}
private Set<Map.Entry<K,V>> entrySet0() {
Set<Map.Entry<K,V>> es = entrySet;
return es != null ? es : (entrySet = new EntrySet());
}
调用entrySet()
以后会返回一个EntrySet
对象
4.2 EntrySet.iterator
public Iterator<Map.Entry<K,V>> iterator() {
return newEntryIterator();
}
Iterator<Map.Entry<K,V>> newEntryIterator() {
return new EntryIterator();
}
EntryIterator
继承自HashIterator
, EntryIterator.next
实际调用的是其父类HashIterator.nextEntry()
4.3 HashIterator构造函数
private abstract class HashIterator<E> implements Iterator<E> {
HashMapEntry<K,V> next; // next entry to return
int expectedModCount; // For fast-fail
int index; // current slot
HashMapEntry<K,V> current; // current entry
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
HashMapEntry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
...
}
在HashIterator
中有一个很重要的变量expectedModCount
, 可以看到在构造函数中将它的值摄为了HashMap.modCount
,这就意味着HashIterator
对象一旦创建,expectedModCount
值就是固定的,如果在用迭代器遍历HashMap时进行put
或remove
,那么modCount
值必然会和expectedModCount
不相等,一旦不相等,就会抛出ConcurrentModificationException
4.4 HashIterator.nextEntry
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
HashMapEntry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
HashMapEntry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
可以看到,正如之前所说,当expectedModCount != modCount
时,会抛出ConcurrentModificationException
, 那如果想在遍历的时候也可以进行删除操作,应该通过什么方法呢?答案是通过Iterator.remove
4.5 HashIterator.remove
public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}
由于HashIterator
是HashMap的内部类,所以可以调用HashMap的内部方法removeEntryForKey
,当删除完entry后,会更新expectedModCount
,这样在进行下一次的next()
调用时, expectedModCount
就会与modCount
保持一致
5. 遍历key和遍历value
如果通过迭代器遍历key和遍历value时的注意事项跟用迭代器遍历HashMap的注意事项一样,也是不可以在遍历时调用remove
方法,原理也是一样的,内部有一个expectedModCount
,如果要删除要掉用迭代器的remove
方法
6. Java8新特性
在Java8中新增了forEach
方法,接收一个BiConsumere<? super K, ? super V>
参数,这个参数是一个函数式接口,所谓函数式接口是指接口只有一个待实现的方法(Java8中,接口可以有默认的实现), 函数式接口可以用lambda
表达式来代替, BiConsumer<K, V>
的lambda
表达式原型是(K, V) - Void
, 即传入K,V, 返回Void