java 集合框架-HashMap
Posted 智公博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java 集合框架-HashMap相关的知识,希望对你有一定的参考价值。
一、概述
HashMap应该是我们最常会使用到的Map容器了吧,这里先简单罗列写它的特点:
1.实现所有Map接口方法,允许null值key、null值value
2.不保证元素的顺序,包括先后添加的顺序,特别是随着容量增加顺序不能保证
3.实现等价于HashTable,区别是HashMap是非线程安全的
4.假设hash方法可以正确的分散,基本的get、put操作的时间复杂度为常量时间(N)
5.迭代器迭代时间与容量正比例,常用迭代操作的初始化容量不要大,负载因子不要太小
6.主要的两个参数:初始容量和负载因子;当映射大小达到容量和因子的乘积大小后自动扩大容量,对全部映射重新hash,容量大概变为原来的2倍;需要衡量好初始容量和因子的大小,默认因子的大小为0.75,因子大可以增加空间使用率、减少重新hash次数,但是影响get,put的性能
7.非线程安全,可以使用Collection.synchronizedMap或者Collections.synchronizedMap包装为线程安全Map
8.与ArrayList相似,HashMap在发现并发时一样是有‘快速失败’机制,在获取迭代器后,如果数据结构发送改变,将会抛出ConcurrentModificationException异常
二、源码分析
1、变量
由于后面的方法中源码常出现HashMap的变量,这里先列出来并说明作用
//默认初始化大小为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
最大容量大小为2的30次方:1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子大小为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//空数组
static final Entry<?,?>[] EMPTY_TABLE = ;
//存储键值对映射的Entry数组,默认为空数组,大小必须为2的次幂
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//保存HashMap实例映射的数量
transient int size;
//当前大小的上限阀值,达到这个值后需要扩容(resize),该值应为容量与负载因子的乘积
//当table还是空数组情况下,这个值是第一次初始化的大小
int threshold;
//负载因子
final float loadFactor;
//可理解为修改次数,每次map结构修改都会加1,用于检测并发快速失败
transient int modCount;
//String类型key默认的可选threshold,当threshold大于这个值时,启用可选hash算法,可以减少由于弱hash值计算方法导致的hash冲突,这个值可以通过系统变量:jdk.map.althashing.threshold设置
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
下面的代码就是为了从jdk.map.althashing.threshold获取这个ALTERNATIVE_HASHING_THRESHOLD;
这段’静态’代码作用很简单,就是获取这个ALTERNATIVE_HASHING_THRESHOLD_DEFAULT值,当设置为-1时表示不启用改可选hash方法,值还是Integer.MAX_VALUE;
static final int ALTERNATIVE_HASHING_THRESHOLD;
static
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
// disable alternative hashing if -1
if (threshold == -1)
threshold = Integer.MAX_VALUE;
if (threshold < 0)
throw new IllegalArgumentException("value must be positive integer.");
catch(IllegalArgumentException failed)
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
ALTERNATIVE_HASHING_THRESHOLD = threshold;
//hash种子参数,随机值,每个实例可能不一样在,计算hash值时使用,用于减少hash碰撞(hash值冲突),为0时表示可选hash不启用
transient int hashSeed = 0;
2、方法
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);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
void init()
使用构造方法创建一个HashMap对象后,实际table还是一个空数组,init方法是空实现,并没有进行实际的初始化操作;这里有一点有注意的就是 threshold 等于指定的初始化大小而不是initialCapacity与loadFactor的乘积,当table还是空数组时,第一次初始化到threshold大小就是initialCapacity;
private static int roundUpToPowerOf2(int number)
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
这个静态内部方法,用于计算入参number的处于的下一个2次幂结果,这样说有点难理解,还是解析代码:当number大于最大容量值,返回最大值;当numer小于等于1时,返回1;其他情况返回(number-1)最高位代表的值乘2。也即是获取2次幂中第一个大于number的值;所以这个方法的返回值肯定是2次幂的数;
下面我们来看下核心的put方法,涉及容量的扩展、hash计算
public V put(K key, V value)
if (table == EMPTY_TABLE)
inflateTable(threshold);
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next)
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
modCount++;
addEntry(hash, key, value, i);
return null;
当表table还是空(刚new出来的实例)会进行一次扩容,每次扩容大小为threshold,这里第一次threshold的值是initialCapacity;
从第二段代码可以看出,key值是允许为null的,并且是单独出来,后面我们再看具体处理实现;
接着就是hash计算以及put存储和碰撞处理,对于以及存在的key,会进行值的替换并返回旧值;
最后如果是新key值,将该key-value保存到table中;
下面我们来看put方法中所调用到方法的具体实现:
private void inflateTable(int toSize)
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
实际这个方法可以看成是初始化的方法,因为第一次table对象创建只在这个方法里面;roundUpToPowerOf2方法获取下一个2次幂数,threshold设置为下一次需要扩容的目标大小(toSize),创建完table数组对象后,初始化hashSeed:
final boolean initHashSeedAsNeeded(int capacity)
boolean currentAltHashing = hashSeed != 0;
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching)
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
return switching;
只有当capacity大于ALTERNATIVE_HASHING_THRESHOLD且还没有使用过可选hash因子时,才会随机一个值作为hash因子,否则都是0;这个方法只会在初始化(相当于)inflateTable方法,和扩容方法 resize 才调用;当map的容量大于某个值时切换使用一个随机的hash因子主要为了减少hash值的重复;
下面具体看看null值的key处理:
private V putForNullKey(V value)
for (Entry<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处理和put方法结果差不多,主要区别是null的hash值固定为0,当然也不会存在不同key重复的hash值,所以索引index位置也不需要计算,也是0;
计算hash值的方法:
final int hash(Object k)
int h = hashSeed;
if (0 != h && k instanceof String)
return sun.misc.Hashing.stringHash32((String) k);
h ^= k.hashCode();
// 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);
首先判断了是否启用了可选的hash因子,如果启用了并且key是String类型,则使用特殊底层方法计算hash值;下面的具体的hash值算法,使用了很多的位移和异或操作,我们这里只需要知道它的目的就是减少不同key的hash值等到相同值的发生,不深入去理解这算法设计为什么如此;
static int indexFor(int h, int length)
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
索引index方法,是得到hash值后,计算key对应的hash值应该放在哪个table的那个位置(index);对于这种需求:在一个定长的数组计算位置、位置值范围为0~~length-1、且 要尽可能均匀分散,我们一把最常使用的方法就是取模运算;这里使用一个与运算的巧妙之处是与length有关的,table的length总是为大于0的2的次幂数,所以length-1的值总是为$2^x$
,二进制就是(11…11),与运算后得到的结果肯定小于length-1,且能均分分布;加上前面的hash计算算法,能有效的减少hash冲突碰撞;
接下来有必要先看看键值对Entry的实现,然后才继续往下看addEntry方法:
static class Entry<K,V> implements Map.Entry<K,V>
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n)
value = v;
next = n;
key = k;
hash = h;
public final K getKey()
return key;
public final V getValue()
return value;
public final V setValue(V newValue)
V oldValue = value;
value = newValue;
return oldValue;
public final boolean equals(Object o)
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2)))
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
return false;
public final int hashCode()
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
public final String toString()
return getKey() + "=" + getValue();
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m)
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m)
Entry实现也简单易懂,记录每个键值对key-value,以及key值的hash值,重要的是有一个next变量用于存储hash冲突(key值hash 值相同)的情况,当有碰撞冲突是,next就相当于一个链表,后面会看到如何存储和查找;equal的逻辑是key和value都相等才任务entry相等,这里还有两个接口方法是空实现的:recordAccess 方法在值替换的时候会被调用,recordRemoval 方法在值被移除的时候会被调用;
接下来继续看 addEntry 方法:
void addEntry(int hash, K key, V value, int bucketIndex)
if ((size >= threshold) && (null != table[bucketIndex]))
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
createEntry(hash, key, value, bucketIndex);
每次增加Entry都先判断是否需要扩容:当前大小size是否大于等于阀值且本次需要存储的index在table非空;所以size达到阀值并不是扩容的唯一条件,如果index的位置还是空,还可以继续存储不扩容,resize扩容方法,扩容目标直接是当前table长度的2倍,完成扩容后需要重新计算当前需要存储key的hash值和index;
void createEntry(int hash, K key, V value, int bucketIndex)
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
createEntry 方法是很简单链表操作,这里就可以看出Entry实现中next是如何使用的了;
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, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
resize 扩容方法只会由put和putAll方法触发调用到,用于当容量达到阀值时增加table大小;首先判断大小不能超过MAXIMUM_CAPACITY,然后新创建一个数组table,并且判断是否需要重新hash:没有切换hash因子 hashSeed就不需要重新hash,应该已存在的key 的hash值不会改变;
void transfer(Entry[] newTable, boolean rehash)
int newCapacity = newTable.length;
for (Entry<K,V> e : table)
while(null != e)
Entry<K,V> next = e.next;
if (rehash)
e.hash = null == e.key ? 0 : hash(e.key);
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
transfer 方法将原来table的entry转换到newTable中;无论是否重hash,index都是需要重新索引的,因为index值是与table长度有关,所以创建新table后长度改变需要重新索引;后面的就是链表操作了;
到这里,添加键值对元素的相关主要方法差不多列举完了,下面继续看看获取的方法:
public V get(Object key)
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
final Entry<K,V> getEntry(Object key)
if (size == 0)
return null;
int hash = (key == null) ? 0 : hash(key);
for (Entry<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;
与put方法类似,get的时候先计算hash值,然后计算索引值index,然后在链表做顺序判断key值相等的entry
最后我们来看下HashMap是如何实现迭代器的;Map的元素单位是key-value键值对,即是Entry,Map的迭代器其实就是Set的迭代器,Map不仅提供EntrySet,而且还区分开了:KeySet、valueSet,内部实现了的迭代器有:EntryIterator,KeyIterator,ValueIterator,而这个三个迭代器都继承自一个基类:HashIterator:
private abstract class HashIterator<E> implements Iterator<E>
Entry<K,V> next; // next entry to return
int expectedModCount; // For fast-fail
int index; // current slot
Entry<K,V> current; // current entry
HashIterator()
expectedModCount = modCount;
if (size > 0) // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
public final boolean hasNext()
return next != null;
final Entry<K,V> nextEntry()
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null)
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
current = e;
return e;
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;
HashIterat 有4个私有变量:
next:迭代器实例下一个值
current:当前值
expectedModCount:修改值,并发修改快速失败机制
index:next值在table的索引值
HashIterat 只有一个无参构造函数,创建实例是初始化私有变量:记录expectedModCount 值,遍历确认next值和index索引,默认current为null;
与其他集合类相同,nextEntry、remove 等方法先判断ModCount值,确认是否在迭代器创建后还发生过修改;nextEntry 方法遍历找下一个值next,并设置current值为原next值,放回current;remove就是直接从current获取到key直接移除这个key的键值对;
其他三个迭代器EntryIterator,KeyIterator,ValueIterator,都是只重写next方法,简单返回值:
private final class ValueIterator extends HashIterator<V>
public V next()
return nextEntry().value;
private final class KeyIterator extends HashIterator<K>
public K next()
return nextEntry().getKey();
private final class EntryIterator extends HashIterator<Map.Entry<K,V>>
public Map.Entry<K,V> next()
return nextEntry();
对HashMap的源码分析从变量、保存、获取、迭代这几个方法解析了HashMap的实现,当然源码不止这一点,其他的方法不再一一列出解析;
以上是关于java 集合框架-HashMap的主要内容,如果未能解决你的问题,请参考以下文章
Java集合框架中的Hashtable、HashMap、HashSet、哈希表概念