一、总述
HashMap是基于散列表的Map的实现,提供了所有Map的操作接口,并支持使用null的key和null的value。HashMap是线程不安全的。
由于散列表存在地址碰撞问题,那HashMap是采取什么方案解决这个问题的呢?使用的就是拉链法,但是拉链法使用链表存储,查找时存在效率比较低。如果Map容量太大而散列表容量太小,则碰撞几率很大,效率也就会急剧下降;相反,如果Map容量太小而散列表容量太大,则存在存储空间浪费。那怎么解决这对矛盾呢?HashMap使用动态散列表解决这个问题。动态散列表就是给散列表一个初始容量,当Map大小达到散列表容量的一定比例时,就扩充散列表容量,这个比例我们就叫它扩容因子。
由于使用拉链法,所以键值对的原始信息也需要存储起来,因此HashMap使用HashMapEntry这个结构体来记录每一个键值对,同时这个结构体也是链表的每一个项
源码:
static class HashMapEntry<K, V>implements Entry<K, V> {
final K key; // key的值
V value; // value的值
final int hash; //key对应的hash
/** hash一样但是key不一样的另一个键值对(同一hash地址的另一个键值对) **/
HashMapEntry<K, V> next;
HashMapEntry(K key, V value, int hash,HashMapEntry<K, V> next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
// ... 其他代码
}
所以HashMap的映射关系就变成了一个Key映射到一个HashMapEntry
二、HashMap的属性含义及作用
/** 散列表最小容量 **/
private static final int MINIMUM_CAPACITY =4;
/** 散列表最大容量 **/
private static final int MAXIMUM_CAPACITY =1 << 30;
/** 空散列表,容量为0构造HashMap时,将使用这个散列表,当容量变化时该散列表将被替换 **/
private static final Entry[] EMPTY_TABLE =new HashMapEntry[MINIMUM_CAPACITY >>> 1];
/** 默认扩容因子 **/
static final float DEFAULT_LOAD_FACTOR =.75F;
/** 散列表存储数组。容量必须是2的指数(原因可能就是为了最优化散列表查找效率和空间利用率,具体算法我也没有看懂) **/
transient HashMapEntry<K, V>[] table;
/** 空Key对应的键值。由于空key是一个特殊的key,该key没有hashCode,无法在散列表中找到地址,所以专门为它开一个字段存储 **/
transient HashMapEntry<K, V> entryForNullKey;
/** 当前Map大小 **/
transient int size;
/** 记录修改次数。用于迭代操作校验 **/
transient int modCount;
/** 散列表扩容阀值,默认为散列表容量的0.75倍 **/
private transient int threshold;
/** 下面3个属性用于获取Map存储集合使用。他们都使用了延时初始化(使用时才初始化),
* 他们实现是通过内部类实现,所以调用时能马上获取Map当前状态数据 **/
private transient Set<K> keySet;
private transient Set<Entry<K,V>> entrySet;
private transient Collection<V>values;
三、HashMap的构造方法
1、无参构造方法
散列表将是EMPTY_TABLE;扩容阀值为-1
源码:
public HashMap() {
table = (HashMapEntry<K, V>[])EMPTY_TABLE;
threshold = -1;
}
2、带初始容量的构造方法
构造步骤:
a、初始容量为负数将抛出IllegalArgumentException
b、初始容量为0等将同于无参构造方法
c、如果容量在最小容量到最大容量范围之外,则取临界值,到步骤e
d、其他情况(容量在最小容量到最大容量范围之间),将容量向上取到2的指数
e、调用makeTable 创建散列表
源码:
public HashMap(int capacity) {
if (capacity < 0) {
throw newIllegalArgumentException("Capacity: " + capacity);
}
if (capacity == 0) {
@SuppressWarnings("unchecked")
HashMapEntry<K, V>[] tab =(HashMapEntry<K, V>[]) EMPTY_TABLE;
table = tab;
threshold = -1; // Forces firstput() to replace EMPTY_TABLE
return;
}
if (capacity < MINIMUM_CAPACITY) {
capacity = MINIMUM_CAPACITY;
} else if (capacity >MAXIMUM_CAPACITY) {
capacity = MAXIMUM_CAPACITY;
} else {
capacity =Collections.roundUpToPowerOfTwo(capacity);
}
makeTable(capacity);
}
其中makeTable方法源码:
private HashMapEntry<K, V>[]makeTable(int newCapacity) {
HashMapEntry<K, V>[] newTable =(HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];
table = newTable;
threshold = (newCapacity >> 1) +(newCapacity >> 2); // 3/4capacity
return newTable;
}
3、 带初始容量和扩容因子的构造方法
该构造方法直接调用带初始容量的构造方法实现,只是对扩容因子进行校验而已。并没有使用到扩容因子,所以扩容因子还是默认的0.75
源码:
public HashMap(int capacity, floatloadFactor) {
this(capacity);
if (loadFactor <= 0 ||Float.isNaN(loadFactor)) {
throw newIllegalArgumentException("Load factor: " + loadFactor);
}
}
4、通过已经存在的Map构造
构造步骤:
a、根据源Map大小变换出一个初始容量,然后调用带初始容量的构造方法构造
b、调用constructorPutAll进行数据填充(除非散列表为EMPTY_TABLE,否则该方法不会检查容量,也就是不会自动扩容)
源码:
public HashMap(Map<? extends K, ?extends V> map) {
this(capacityForInitSize(map.size()));
constructorPutAll(map);
}
其中constructorPutAll源码如下:
final void constructorPutAll(Map<?extends K, ? extends V> map) {
if (table == EMPTY_TABLE) {
doubleCapacity(); // Don't dounchecked puts to a shared table.
}
for (Entry<? extends K, ? extendsV> e : map.entrySet()) {
constructorPut(e.getKey(),e.getValue());
}
}
四、HashMap的操作API
1、put方法
参数为:键值对;返回值为:该键对应的旧值,如果不存在旧值返回null
操作步骤:
a、如果key为空,则调用putValueForNullKey方法添加,然后结束
b、如果key不为空,则根据key计算出一个hash值,并与当前散列表容量计算得出一个索引值
c、根据索引值取出对应下标的键值对链表
d、然后依次对Key进行比较(先比较hash值,hash值一样再比较equals,equals也认为一样才是真正一样)
e、存在一样的就替换该键值对对应的value,然后将旧value值返回
f、如果不存在一样的key(索引对应链表不存在或者链表对应的key不存在),则更新修改次数标志和Map大小,检查Map大小是否达到扩容阀值
g、达到则进行扩容,刷新索引值;没达到跳过此步骤
h、调用addNewEntry对应索引值的位置增加一条链表(链表不存在)或者对该位置链表增加一个键值对到链表头(链表存在),返回null
源码:
public V put(K key, V value) {
if (key == null) {
return putValueForNullKey(value);
}
int hash = secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length -1);
for (HashMapEntry<K, V> e =tab[index]; e != null; e = e.next) {
if (e.hash == hash &&key.equals(e.key)) {
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
modCount++;
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length -1);
}
addNewEntry(key, value, hash, index);
return null;
}
其中addNewEntry源码:
void addNewEntry(K key, V value, inthash, int index) {
table[index] = new HashMapEntry<K,V>(key, value, hash, table[index]);
}
扩容步骤:
a、如果当前容量达到最大容量,则不扩容直接返回
b、将容量扩大为原来的两倍,创建相应的散列表
c、遍历旧散列表的所有键值对,重新计算索引值,插入到新的散列表
d、返回新的散列表
代码:
private HashMapEntry<K, V>[]doubleCapacity() {
HashMapEntry<K, V>[] oldTable =table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
return oldTable;
}
int newCapacity = oldCapacity * 2;
HashMapEntry<K, V>[] newTable =makeTable(newCapacity);
if (size == 0) {
return newTable;
}
for (int j = 0; j < oldCapacity;j++) {
HashMapEntry<K, V> e =oldTable[j];
if (e == null) {
continue;
}
int highBit = e.hash &oldCapacity;
HashMapEntry<K, V> broken =null;
newTable[j | highBit] = e;
for (HashMapEntry<K, V> n =e.next; n != null; e = n, n = n.next) {
int nextHighBit = n.hash &oldCapacity;
if (nextHighBit != highBit) {
if (broken == null)
newTable[j |nextHighBit] = n;
else
broken.next = n;
broken = e;
highBit = nextHighBit;
}
}
if (broken != null)
broken.next = null;
}
return newTable;
}
2、putAll方法、remove方法、get方法、containsKey方法
这些方法都跟put类似,先根据key找到对应键值对,然后做相应操作
3、containsValue方法
参数为:value;返回值为:一个表示是否存在这样的一个键值对,它的值就是value的布尔值
操作步骤:
a、value为空,那就是循环散列表和散列表的每一条链表,查找是否存在值为空的键值对,存在就返回true
b、不存在就判断空key对应的键值对的value是否为空,然后返回
c、value不为空,那就是循环散列表和散列表的每一条链表,查找是否存在值通过equals方法判断一样的键值对,存在就返回true
d、不存在就判断空key对应的键值对的value通过equals方法判断是否一样一样,然后返回
源码如下:
public boolean containsValue(Object value){
HashMapEntry[] tab = table;
int len = tab.length;
if (value == null) {
for (int i = 0; i < len; i++) {
for (HashMapEntry e = tab[i]; e!= null; e = e.next) {
if (e.value == null) {
return true;
}
}
}
return entryForNullKey != null&& entryForNullKey.value == null;
}
for (int i = 0; i < len; i++) {
for (HashMapEntry e = tab[i]; e !=null; e = e.next) {
if (value.equals(e.value)) {
return true;
}
}
}
return entryForNullKey != null&& value.equals(entryForNullKey.value);
}
4、获取Map内部集合数据方法
由于这几个方法内部实现步骤基本一样,所以我就分析其中一个(keySet方法)
keySet方法源码很短,就是判断keySet属性是否存在值,存在就返回;否则就创建一个赋值到keySet属性并返回
public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null) ? ks : (keySet =new KeySet());
}
其中keySet属性没啥看的;现在我们就来看看KeySet这个类的实现。
源码:
private final class KeySet extendsAbstractSet<K> {
public Iterator<K> iterator() {
return newKeyIterator();
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
int oldSize = size;
HashMap.this.remove(o);
return size != oldSize;
}
public void clear() {
HashMap.this.clear();
}
}
这个类有以下特征:
a、这是一个实例内部类,继承了AbstractSet类
b、重写了一些方法,这些方法都是调用外部HashMap实例的相关属性和方法实现。
c、没有重写添加操作函数,调用添加函数将抛出UnsupportedOperationException异常(父类抛出)
这些方法中我们经常用到的就是迭代方法(iterator),接着我们就来看看这个方法
这个方法直接调用newKeyIterator方法返回,我们就来看看newKeyIterator方法。源码如下:
Iterator<K> newKeyIterator() { returnnew KeyIterator(); }
直接创建一个KeyIterator迭代器返回,那我们再看看KeyIterator实现。源码如下:
private final class KeyIteratorextends HashIterator implements Iterator<K> {
public K next() { returnnextEntry().key; }
}
这个类继承了HashIterator实现了Iterator,实现代码就一个方法(Iterator的next方法)
其中Iterator就是一个interface,没啥看的;我们就看看HashIterator。源码如下:
private abstract class HashIterator {
/**下一个键值对在散列表中的索引,初始值就是0 **/
int nextIndex;
/** 下一个键值对,初始值为为空key对应的键值对 **/
HashMapEntry<K, V> nextEntry =entryForNullKey;
/**最后一次返回的键值对,也就是当前迭代位置的键值对,删除时使用 **/
HashMapEntry<K, V>lastEntryReturned;
/**期望的修改值,用于校验迭代过程HashMap是否被外部修改,初始值为当前HashMap的修改值 **/
int expectedModCount = modCount;
HashIterator() {
if (nextEntry == null) {
HashMapEntry<K, V>[] tab= table;
HashMapEntry<K, V> next =null;
while (next == null &&nextIndex < tab.length) {
next = tab[nextIndex++];
}
nextEntry = next;
}
}
public boolean hasNext() {
return nextEntry != null;
}
HashMapEntry<K, V> nextEntry() {
if (modCount != expectedModCount)
throw newConcurrentModificationException();
if (nextEntry == null)
throw newNoSuchElementException();
HashMapEntry<K, V>entryToReturn = nextEntry;
HashMapEntry<K, V>[] tab = table;
HashMapEntry<K, V> next =entryToReturn.next;
while (next == null &&nextIndex < tab.length) {
next = tab[nextIndex++];
}
nextEntry = next;
return lastEntryReturned =entryToReturn;
}
public void remove() {
if (lastEntryReturned == null)
throw newIllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
HashMap.this.remove(lastEntryReturned.key);
lastEntryReturned = null;
expectedModCount = modCount;
}
}
分析:
a、这个类是一个实例内部类,可以访问外部类的实例变量
b、构造函数就是查找下一个不为空的nextEntry,找不到就不做什么
c、hasNext方法就是判断nextEntry属性是否不为空
d、nextEntry 方法就是查找下一个不为空的nextEntry,并返回当前的nextEntry属性;
查找之前先对修改值校验,所以调用这个方法的时候,外部不能就修改HashMap(并发下抛出ConcurrentModificationException的原因)
也对当前的nextEntry属性进行校验,所以也要先调用hasNext判断是否存在下一个
e、remove方法就是删除当前迭代到的值
删除之前对当前键值对进行非空判断,所以调用该方法之前要先调用nextEntry,并且知道当前的键值对不为空
该方法也对修改值校验,所以调用这个方法的时候,外部不能就修改HashMap
该方法删除了键值对之后重新更新了expectedModCount,所以下次的调用nextEntry不会引起抛出ConcurrentModificationException
五、HashMap使用细节
1、不允许存在相同的key(key的比较:先比较hashCode,再比较equal方法,两者都一样才是一样)
2、允许存在为空的key和value(最多只能存在一个key为空的键值对,后面的将会把前面的替换掉)
3、线程不安全,多线程操作需要同步或者使用Hashtable代替
4、使用迭代器获取下一个元素时,需要先判断是否还有下一个
5、使用迭代器迭代过程中不能通过HashMap自身操作函数修改HashMap
6、使用迭代器迭代过程中可以使用迭代器的remove删除元素;但对于同一个元素不能调用多次remove
7、扩容将进行大量遍历,所以如果知道大体容量,就使用带容量的构造参数构造