HashMap简介:

  HashMap在日常的开发中应用的非常之广泛,它是基于Hash表,实现了Map接口,以键值对(key-value)形式进行数据存储,HashMap在数据结构上使用的是数组+链表。允许null键和null值,不保证键值对的顺序。

HashMap检索数据的大致流程:

  当我们使用HashMap搜索key所对应的value时,HashMap会根据Hash算法对key进行计算,得到一个key的hash值,再根据hash值算出该key在数组中存储的位置index,然后获取数组在index位置的键值对e,再使用链表对e进行遍历,查找遍历的元素是否和给定的key相符合,若有符合的,则返回其value值。

自己手动画了一个HashMap的数据结构图:

HashMap源码分析:

  HashMap是存储键值对的集合,实现了Map接口,下面我们看一下Map接口的定义:

/**
*映射key到value的顶级接口,不能包含重复的key,一个key最多可以映射到一个value,键和值均可为null
*/
public interface Map<K,V> { //返回该map包含的键值对的个数,如果键值对的个数超过了Integer.MAX_VALUE,则返会Integer.MAX_VALUE
int size(); //如果该Map没有包含键值对,则返回true,否则返回false
boolean isEmpty(); //判断该map是否包含指定的key所对应的键值对,key可以为null
boolean containsKey(Object key); //判断该map是否包含指定的value所对应的键值对,若map中包含有一个及以上的key,对应指定的value,则返回true,value可以为null
boolean containsValue(Object value); //返回指定的key所对应的value,若key不存在,则返回null;但是返回null的key,不代表key在map中不存在,有可能是key所对应的value为null
V get(Object key); //将指定的key和value映射为一个键值对,放入map中;若之前该map中包含了指定的Key,则该key所对应的老的值oldValue将会被替换为value
V put(K key, V value); //删除指定的key所对应的键值对,并返回该key对应的value
V remove(Object key); //将指定的map中的键值对复制到当前map中
void putAll(Map<? extends K, ? extends V> m); //清除map中所有的键值对,该操作完成后,该map就是空的了
void clear(); //将map中所有的key返回,由于map中的key是不能重复的,所以用Set集合的方式装载所有的key
Set<K> keySet(); //将map中所有的value返回,由于map中的value是可重复的,所以用Collection集合的方式装载所有的value
Collection<V> values(); //将map中所有的键值对Entry返回,由于map中的键值对是不可重复的(key不可重复),所以用Set集合的方式装载所有的value
Set<Map.Entry<K, V>> entrySet(); //Map中承载键值对的数据结构Entry
interface Entry<K,V> { //返回键值对的键值key
K getKey(); //返回键值对的value值
V getValue(); //将当前键值对的value值 替换为指定的value值
V setValue(V value); //判断指定的对象和当前键值对是否equals相等,若相等,则代表是同一个键值对;
boolean equals(Object o); //返回当前键值对的hashCode值
int hashCode();
} //判断指定对象和当前Map的equals是否相等
boolean equals(Object o); //返回当前Map的hashCode
int hashCode();
}

下面我们具体的看一下HashMap:

    //HashMap是基于hash表来实现Map接口的,HashMap维护了下面几个变量:

    //HashMap默认的初始化大小为16
static final int DEFAULT_INITIAL_CAPACITY = 16; //HashMap包含键值对的最大容量为2^30,HashMap的容量一定要是2的整数次幂
static final int MAXIMUM_CAPACITY = 1 << 30; //默认的加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f; //装载键值对的内部容器Entry数组,长度一定得是2的幂
transient Entry[] table; //HashMap中包含的键值对的个数
transient int size; //HashMap的极限 当键值对的个数达到threshold时,数组table要扩容的
int threshold; //加载因子
final float loadFactor; //HashMap结构上被改变的次数,结构上的改变包括:键值对的大小的改变,修改HashMap的内部结构(比较进行了rehash操作),此属性用来保持fail-fast
transient volatile int modCount;

接下来我们看一下HashMap的构造函数:

/**
*根据指定的容量initialCapacity和加载因子loadFactor构造HashMap
*/
public HashMap(int initialCapacity, float loadFactor) {
//对initialCapacity进行参数校验,若小于0,则抛出IllegalArgumentException异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//若initialCapacity大于MAXIMUM_CAPACITY(2^30),则将MAXIMUM_CAPACITY赋值给initialCapacity
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//对参数loadFactor进行有效性校验,不能<=0,不能非数字,否则抛出IllegalArgumentException异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor); // Find a power of 2 >= initialCapacity 找到一个2的幂的数capacity,使其不小于参数initialCapacity
int capacity = 1;
//若capacity小于initialCapacity,则将capacity扩大一倍
while (capacity < initialCapacity)
capacity <<= 1; this.loadFactor = loadFactor;
//设置极限,大小为 capacity * loadFactor,即(容量*负载因子)
threshold = (int)(capacity * loadFactor);
//创建一个大小为capacity的Entry数组table,用来保存键值对
table = new Entry[capacity];
//调用方法init(),进行额外的初始化操作
init();
}
//init方法是空的,如果你定制额外的初始化操作,可以继承HashMap,覆盖init()方法
void init() { } //通过指定的容量initialCapacity来构造HashMap,这里使用了默认的加载因子DEFAULT_LOAD_FACTOR 0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
} //无参的构造函数 加载因子为DEFAULT_LOAD_FACTOR 0.75,容量为默认的DEFAULT_INITIAL_CAPACITY 16,极限为 16*0.75=12
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}

下面我们看一下HashMap中承载键值对的数据结构Entry的实现,Entry<K,V>是HashMap的一个静态内部类

/**
*Entry是HashMap里面承载键值对数据的数据结构,实现了Map接口里面的Entry接口,除了方法recordAccess(HashMap<K,V> m),recordRemoval(HashMap<K,V> m)外,其他方法均为final方法,表示即使是子类也不能覆写这些方法。
*/
static class Entry<K,V> implements Map.Entry<K,V> {
//键值,被final修饰,表明一旦赋值,不可修改
final K key;
//value值
V value;
//下一个键值对的引用
Entry<K,V> next;
//当前键值对中键值的hash值
final 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;
} //设置value值,返回原来的value值
public final V setValue(V newValue) {
  V oldValue = value;
value = newValue;
return oldValue;
}
//比较键值对是否equals相等,只有键、值都相等的情况下,才equals相等
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();
//若k1 == k2(k1,k2引用同一个对象),或者k1.equals(k2)为true时,进而判断value值是否相等
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
//若v1 == v2(v1,v2引用同一个对象),或者v1.equals(v2)为true时,此时才能说当前的键值对和指定的的对象equals相等
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
//其他均为false
return false;
} public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
} public final String toString() {
return getKey() + "=" + getValue();
} //此方法只有在key已存在的时候调用m.put(key,value)方法时,才会被调用,即覆盖原来key所对应的value值时被调用
void recordAccess(HashMap<K,V> m) {
}
//此方法在当前键值对被remove时调用
void recordRemoval(HashMap<K,V> m) {
}
}

下面是Hash的put方法的实现:

/**
*将指定的键key,值value放到HashMap中
*/
public V put(K key, V value) {
//首先判断键key是否为null,若为null,则调用putForNullKey(V v)方法完成put
if (key == null)
return putForNullKey(value);
//程序走到这,说明key不为null了,先调用hash(int)方法,计算key.hashCode的hash值
int hash = hash(key.hashCode());
//再调用indexFor(int,int)方法求出此hash值对应在table数组的哪个下标i上 (bucket桶)
int i = indexFor(hash, table.length);
//遍历bucket桶上面的链表元素,找出HashMap中是否有相同的key存在,若存在,则替换其value值,返回原来的value值
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//若元素e.hash值和上面计算的hash值相等,并且(e.key == 入参key,或者入参key equals 相等 e.key),则说明HashMap中存在了和入参相同的key了,执行替换操作
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
//在执行替换操作的时候,调用Entry对象的recordAccess(HashMap<K,V> m)方法,这个上面说过了
e.recordAccess(this);
return oldValue;
}
}
//程序走到这,说明原来HashMap中不存在key,则直接将键值对插入即可,由于插入元素,修改了HashMap的结构,因此将modeCount+1
modCount++;
//调用addEntry(int,K,V,int)方法进行键值对的插入
addEntry(hash, key, value, i);
//由于原来HashMap中不存在key,则不存在替换value值问题,因此返回null
return null;
}

当key为null时,HashMap是这样进行数据插入的:

//先看看HashMap中原先是否有key为null的键值对存在,若存在,则替换原来的value值;若不存在,则将key为null的键值对插入到Entry数组的第一个位置table[0]
private V putForNullKey(V value) {
//获取Entry数组的第一个元素:table[0],然后通过e.next进行链表的遍历,若遍历过程中有元素e.key为null,则替换该元素的值
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//说明原来之前HashMap中就已经存在key问null的键值对了,现在又插入了一个key为null的新元素,则替换掉原来的key为null的value值
if (e.key == null) {
V oldValue = e.value;
e.value = value;
//在执行替换操作的时候,调用Entry对象的recordAccess(HashMap<K,V> m)方法,这个上面说过了
e.recordAccess(this);
return oldValue;
}
}
//程序走到这,说明HashMap中原来没有key为null的键值对,需要直接插入元素,由于插入元素,修改了HashMap的结构,因此将modeCount+1
modCount++;
//调用addEntry(int,K,V,int)方法进行键值对的插入,这里将入参hash设置为0,K为null,插入的位置index也为0,说明key为null的元素插入在bucket[0]第一个桶上
addEntry(0, null, value, 0);
return null;
}

HashMap在插入数据之前,要根据key值和hash算法来计算key所对应的hash值

//根据key的hashCode值,来计算key的hash值
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);
}
/**
*在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置,如何计算这个位置就是hash算法.
*HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,
*那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表.
*所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的,但是,“模”运算的消耗还是比较大的,
*能不能找一种更快速,消耗更小的方式那?java中时这样做的 :将hash值和数组长度按照位与&来运算
*/
static int indexFor(int h, int length) {
return h & (length-1);
}

下面我们看一看实际进行数据添加的操作addEntry方法

/**
*将指定的key,value,hash,bucketIndex 插入键值对,若此时size 大于极限threshold,则将Entry数组table扩容到原来容量的两倍
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
//取出原来bucketIndex处的键值对e
Entry<K,V> e = table[bucketIndex];
//创建一个新的键值对,使用给定的hash、key、value,并将新键值对的next属性指向e,最后将新键值对放在bucketIndex处
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//将size+1,若此时size 大于极限threshold,则将Entry数组table扩容到原来容量的两倍
if (size++ >= threshold)
//调用resize(int)方法,进行数组的扩容
resize(2 * table.length);
}

我们知道HashMap采用的数组+链表来实现的,当HashMap中元素的个数size大于极限threshold时,会进行数组的扩容操作,这个操作使用resize(int newCapacity)方法实现的:

/**
*将HashMap中Entry数组table的容量扩容至新容量newCapacity,数组的扩容不但涉及到数组元素的复制,还要将原数组中的元素rehash到新的数组中,很耗时;如果能预估到放入HashMap中元素的大小,最好使用new HashMap(size)方式创建足够容量的HashMap,避免不必要的数组扩容(rehash操作),提高效率
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果原数组的大小已经为允许的最大值MAXIMUM_CAPACITY了,则不能进行扩容了,这里仅仅将极限threshold设置为Integer.MAX_VALUE,然后返回
if (oldCapacity == MAXIMUM_CAPACITY) {
//将极限threshold设置为Integer.MAX_VALUE
threshold = Integer.MAX_VALUE;
return;
}
//程序走到这,说明可以进行扩容,先创建容量为newCapacity的新Entry数组newTable
Entry[] newTable = new Entry[newCapacity];
//调用tranfer(Entry[] newTable)方法,进行数组元素的移动和rehashing
transfer(newTable);
//将新数组newTable赋值给table
table = newTable;
//计算极限threshold的最新值
threshold = (int)(newCapacity * loadFactor);
} //将原Entry数组table中的所有键值对迁移到新Entry数组newTable上
void transfer(Entry[] newTable) {
//原数组赋值给src
Entry[] src = table;
//新数组长度为newCapacity
int newCapacity = newTable.length;
//遍历原数组
for (int j = 0; j < src.length; j++) {
//获取原数组中的元素(键值对),赋值给e
Entry<K,V> e = src[j];
//若元素e不为null
if (e != null) {
//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
src[j] = null;
//遍历元素e所对应的bucket桶内的所有元素
do {
//获取下一个元素next
Entry<K,V> next = e.next;
//重新计算键值对e在新数组newTable中的bucketIndex位置(即rehash操作)
int i = indexFor(e.hash, newCapacity);
//标记[1]
e.next = newTable[i];
//将当前元素e放入新数组的i位置
newTable[i] = e;
//访问下一个Entry链上的元素
e = next;
} while (e != null);
}
}
}

注释标记[1]处,将newTable[i]的引用赋给了e.next,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素终会被放到Entry链的尾部(如果发生了hash冲突的话);

若某个桶上面的元素不止一个,则会行成一个链表,在resize过程中,会导致链表逆序排列。比如在第i个桶内,有元素tab[i1]=a -> tab[i2]=b -> tab[i3]=c -> tab[i4]=d,

在resize后,比如会放到新的数组下标j处,则新的链表为newTab[j1]=d -> newTab[j2]=c -> newTab[j3]=b -> newTab[j4]=a

下面我们看一下HashMap检索数据的操作:

//获取指定key所对应的value值
public V get(Object key) {
//若key==null,则直接调用getForNullKey()方法
if (key == null)
return getForNullKey();
//到这说明key != null,先获取key的hash值
int hash = hash(key.hashCode());
//在indexFor(int hash,int length)获取key落在Entry数组的哪个bucket桶上,获取该bucket桶上的元素e,进而遍历e的链表,找相对应的key,若找到则返回key所对应的value值,找不到则返回null
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//若元素e.hash == 上面计算的hash值,并且(e.key 和入参key是同一个对象的引用,或者e.key和入参key equals相等),
//则认为入参key和当前遍历的元素e的key是同一个,返回e的value值
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
//上面遍历了一遍也没有找到,则返回null
return null;
} //获取key为null的value值,由上面putForNullKey方法可知,key为null的键值对,被放在了Entry数组table的第一个bucket桶(table[0])
private V getForNullKey() {
//获取Entry数组table的第一个元素e,从e开始往下遍历,若找到元素e.key 为null的,则直接返回该元素e.value值
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//找到元素e.key 为null的,则直接返回该元素e.value值
if (e.key == null)
return e.value;
}
//从table[0]开始,遍历链表一遍,没有找到key为null的,则返回null
return null;
} //获取指定key所对应的键值对Entry,先获取key的hash值,再获取该hash值应放入哪个hash桶,然后遍历该桶中的键值对,若有则返回该键值对;若没有则返回null
final Entry<K,V> getEntry(Object key) {
//若key为null,则hash值为0;若key != null,则利用hash(int)计算key的hash值
int hash = (key == null) ? 0 : hash(key.hashCode());
//获取该hash值应放入哪个hash桶,并遍历该hash桶
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//若元素e.hash == hash,并且(e.key == key,或者 key.equals(e.key)为true),则认为入参key和当前遍历的元素e.key是同一个,返回该元素e
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
//若没有则返回null
return null;
}
//判断HashMap中是否含有指定key的键值对,内部用getEntry(Object key)来实现
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
//将指定Map中的所有元素(键值对)拷贝到当前HashMap中,对于当前HashMap中存在的key,则进行value值的替换
public void putAll(Map<? extends K, ? extends V> m) {
//若指定的Map中没有元素,则直接返回
int numKeysToBeAdded = m.size();
if (numKeysToBeAdded == 0)
return; //若必要,则进行数组的扩容
if (numKeysToBeAdded > threshold) {
int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
if (targetCapacity > MAXIMUM_CAPACITY)
targetCapacity = MAXIMUM_CAPACITY;
//计算新的容量
int newCapacity = table.length;
while (newCapacity < targetCapacity)
newCapacity <<= 1;
//若新容量大于目前数组的长度,则调用resize(int)进行扩容
if (newCapacity > table.length)
resize(newCapacity);
}
//获取指定Map的迭代器,循环调用put(K k,V v)方法,进行键值对的插入
for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
Map.Entry<? extends K, ? extends V> e = i.next();
put(e.getKey(), e.getValue());
}
}

下面看下HashMap的remove操作:

/**
* 删除指定key所对应的元素
*/
public V remove(Object key) {
//调用方法reomveEntryForKey(Object key)来删除并获取指定的entry
Entry<K,V> e = removeEntryForKey(key);
//若entry为null,则返回null;否则返回entry的value值
return (e == null ? null : e.value);
} /**
*移除并返回指定key所关联的键值对entry,若HashMap中没有键值对和指定的key相关联,则返回null
*/
final Entry<K,V> removeEntryForKey(Object key) {
//若key为null,则hash值为0;若key != null,则利用hash(int)计算key的hash值
int hash = (key == null) ? 0 : hash(key.hashCode());
//获取key应放入的在数组中的位置索引i
int i = indexFor(hash, table.length);
//标识前一个元素
Entry<K,V> prev = table[i];
//标识遍历过程中的当前元素
Entry<K,V> e = prev;
//遍历bucket桶table[i]中的元素,寻找对应的key
while (e != null) {
//当前元素的下一个元素
Entry<K,V> next = e.next;
Object k;
//元素e.hash和上面计算的hash值相等,并且(key == e.key 或者key.equals(e.key) 为true),说明找到了指定key所对应的键值对
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
//由于删除了一个元素,修改了HashMap的结构,因此将modCount+1
modCount++;
//将size-1
size--;
//若待查找的元素为桶内的第一个元素,则当前元素的下一个元素next放入数组中位置i中
if (prev == e)
table[i] = next;
//否则将上一个元素的next属性指向 当前元素的next
else
prev.next = next;
//当元素被remove时,调用Entry对象的recordRemove(Map<K,V> m)方法
e.recordRemoval(this);
//返回找到的当前元素
return e;
}
//程序走到这,说明当前元素不是要查找的元素;则将当前元素赋值给上一个元素,将下一个元素赋值给当前元素,以完成遍历
prev = e;
e = next;
} return e;
}
//判断HashMap中是否包含指定的对象value
public boolean containsValue(Object value) {
//若value为null,则调用containsNullValue()方法处理
if (value == null)
return containsNullValue();
//将数组table赋值给tab
Entry[] tab = table;
//遍历数组tab的每个索引位置(此层循环对应数组结构)
for (int i = 0; i < tab.length ; i++)
//对应指定的索引位置i,遍历在索引位置i上的元素(此层循环对应链表结构)
for (Entry e = tab[i] ; e != null ; e = e.next)
//若某个元素e.value和指定的value equals相等,则返回true
if (value.equals(e.value))
return true;
//遍历完成没有找到对应的value值,返回false
return false;
} //判断HashMap是否包含value为null的元素
private boolean containsNullValue() {
//将数组table赋值给tab
Entry[] tab = table;
//遍历数组tab的每个索引位置(此层循环对应数组结构)
for (int i = 0; i < tab.length ; i++)
//对应指定的索引位置i,遍历在索引位置i上的元素(此层循环对应链表结构)
for (Entry e = tab[i] ; e != null ; e = e.next)
//若某个元素e.value == null,则返回true
if (e.value == null)
return true;
//没有找到value值为null的,返回false
return false;
}
//清除HashMap中所有的键值对,此操作过后,HashMap就是空的了
public void clear() {
//要删除所有的元素,肯定会对HashMap的结构造成改变,因此modCount+1
modCount++;
Entry[] tab = table;
//遍历数组,将数组中每个索引位置的设置为null
for (int i = 0; i < tab.length; i++)
tab[i] = null;
//将size 修改为0
size = 0;
}

现在看一下上面没有讲的一个构造函数:

//构造一个新的HashMap,以承载指定Map中所有的键值对,使用默认的加载因子DEFAULT_LOAD_FACTOR
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
//调用putAllForCreate(Map<? extends K, ? extends V>)方法完成元素的迁移
putAllForCreate(m);
} private void putAllForCreate(Map<? extends K, ? extends V> m) {
for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
Map.Entry<? extends K, ? extends V> e = i.next();
//在迭代器循环中调用putForCreate(K k,V v)来实现元素的创建
putForCreate(e.getKey(), e.getValue());
}
} //该方法和put方法不同,它不会进行数组的扩容resize,和对modCount的检查
private void putForCreate(K key, V value) {
//若key为null,则hash值为0;若key != null,则利用hash(int)计算key的hash值
int hash = (key == null) ? 0 : hash(key.hashCode());
//求key应该放入哪个hash桶(bucket)内
int i = indexFor(hash, table.length);
//遍历链表,若key之前在Map中已经有了,则直接返回
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
//调用createEntry(int hash,K k,V v,int bucketIndex)方法完成键值对的创建并加入到链表中
createEntry(hash, key, value, i);
} void createEntry(int hash, K key, V value, int bucketIndex) {
//将bucketIndex位置的元素取出来
Entry<K,V> e = table[bucketIndex];
//创建一个新的键值对,使用给定的hash、key、value,并将新键值对next指向e,最后将新键值对放在bucketIndex处
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//将数组大小size + 1
size++;
}

  下面我们讲下HashMap的负载因子loadfactor, 所谓负载因子就是散列表的实际元素数目(n)/散列表的容量(m), 它衡量的是一个散列表的空间的使用程度,默认情况下loadfactor是0.75,它的作用是当HashMap中元素的个数size 大于(HashMap的容量capacity * 负载因子loadfactor)时,该HashMap便会进行扩容resize。

  我们先说下hash冲突:

  当利用HashMap存数据的时候,先根据key的hashcode值来计算key的hash值(利用hash函数),再根据hash值来计算该key在数组table中的位置index(hash & (length-1)),比如我们要存两个键值对key1-value1和key2-value2,

如果key1、key2分别通过hash函数计算的hash值hash1、hash值hash2 相等,那key1和key2一定会落在数组table的同一个位置上,这就产生了冲突,这个冲突是由不同的key值但是却相同的hash值引起的,称之为hash冲突。HashMap解决hash冲突的方式就是链表,虽然key1-value1和key2-value2这两个键值对落在了数组table的同一个位置上,但是它们是链表的方式连接在一起,当HashMap查找key1时,就需要遍历这个链表了。

当负载因子越大的时候,出现hash冲突的可能性越大,这意味着数组table中某个具体的桶bucket上不止有一个元素(此时就构成了链表了)可能性增大,在检索数据的时候需要遍历链表的可能性增大,因此检索的效率就比较低(耗时长),但是空间的利用率越高。

当负载因子越小的时候,出现hash冲突的可能性越小,这意味着数组table中某个具体的桶bucket上不止有一个元素(此时就构成了链表了)可能性减小了,在检索数据的数据需要遍历链表的可能性减小,因此检索的效率就比较高,但是空间利用率越低。

  上面的源码解析了提到了HashMap的容量一定得是2的整数此幂(2^n),这又是问什么呢?

  通过上面的源码我们知道:根据hash值求该hash值在数组中的位置的实现是: hash & (length-1),当数组table的长度length为2的整数次幂的时候,那(length-1)二进制表示形式从低位开始一定都是1,直到为0,如果length依次为2,4,8,16,32,64,则(length-1)一次为1,3,7,15,31,63,那(length-1)的二进制形式依次为1,11,111,1111,11111,我们知道二进制的与运算&,当一方位数全是1的时候,进行&运算的结果完全取决于另外一方。

比如从0开始到15,依次和15进行&运算,看看结果的平均分布情况:

操作数0-15 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
15的二进制 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
进行位与&操作结果 0000 0001 0010  0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111

通过位与&操作后,发现0-15完全平均的落在了数组的各个索引位置

下面通过一个小例子予以验证:

public class HashDemo {

    private final int[] table;

    public HashDemo(int size) {
this.table = new int[size];
for (int i = 0; i < size; i++) {
table[i] = i;
}
} //求key所对应的在数组中的位置
public int index(int key){
//求hash值
int hash = hash(key);
//返回key所对应的在数组中的位置
return hash & (table.length-1);
} //HashMap中hash函数的实现,求hash值
public int hash(int h){
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
} public static void main(String[] args) {
Map<String,Integer> keyToNumber = new HashMap<String, Integer>();
int size = 16;
HashDemo hashDemo = new HashDemo(size);
int testSize = 1000;
for (int i = 0; i < testSize; i++) {
int index = hashDemo.index(i);
Integer number = keyToNumber.get("key" + index);
if (number == null) {
keyToNumber.put("key"+index,1);
}else {
keyToNumber.put("key"+index,keyToNumber.get("key"+index)+1);
}
}
Iterator<Map.Entry<String, Integer>> it = keyToNumber.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Integer> entry = it.next();
System.out.println(entry.getKey() + " == "+entry.getValue());
}
} }

当我们将size设置为16 (2的4次幂)时,控制台输出:

key4   == 62
key3   == 62
key6   == 62
key5   == 62
key0   == 62
key2   == 62
key1   == 62
key10   == 63
key11   == 63
key8   == 63
key7   == 62
key9   == 63
key15   == 63
key14   == 63
key13   == 63
key12   == 63

由上面的输出可见,数据非常平均的分配在了数组的16个索引位置,

当size设置为非2的整数次幂的时候,比如50,这时控制台输出:

key0   == 120
key17   == 124
key16   == 124
key1   == 120
key49   == 128
key48   == 128
key32   == 128
key33   == 128

由此可见1000个数据只分配在了8个索引位置上。

使用HashMap的注意事项:

  1.HashMap采用数组+链表的形式存储数据,当预先知道要存储在HashMap中数据量的大小时,可以使用new HashMap(int size)来指定其容量大小,避免HashMap数组扩容导致的元素复制和rehash操作带来的性能损耗。

  2.HashMap是基于Hash表、实现了Map接口的,查找元素的时候,先根据key计算器hash值,进而求得key在数组中的位置,但是要尽量避免hash冲突造成的要遍历链表操作,因此在我们手动指定HashMap容量的时候,容量capacity一定得是2的整数次幂,这样可以让数据平均的分配在数组中,减小hash冲突,提高性能。

  3.HashMap是非线程安全的,在多线程条件下不要使用HashMap,可以使用ConcurrentHashMap代替。

												

HashMap 源码解析的更多相关文章

  1. 【转】Java HashMap 源码解析(好文章)

    ­ .fluid-width-video-wrapper { width: 100%; position: relative; padding: 0; } .fluid-width-video-wra ...

  2. HashMap源码解析 非原创

    Stack过时的类,使用Deque重新实现. HashCode和equals的关系 HashCode为hash码,用于散列数组中的存储时HashMap进行散列映射. equals方法适用于比较两个对象 ...

  3. Java中的容器(集合)之HashMap源码解析

    1.HashMap源码解析(JDK8) 基础原理: 对比上一篇<Java中的容器(集合)之ArrayList源码解析>而言,本篇只解析HashMap常用的核心方法的源码. HashMap是 ...

  4. 最全的HashMap源码解析!

    HashMap源码解析 HashMap采用键值对形式的存储结构,每个key对应唯一的value,查询和修改的速度很快,能到到O(1)的平均复杂度.他是非线程安全的,且不能保证元素的存储顺序. 他的关系 ...

  5. HashMap源码解析和设计解读

    HashMap源码解析 ​ 想要理解HashMap底层数据的存储形式,底层原理,最好的形式就是读它的源码,但是说实话,源码的注释说明全是英文,英文不是非常好的朋友读起来真的非常吃力,我基本上看了差不多 ...

  6. 详解HashMap源码解析(下)

    上文详解HashMap源码解析(上)介绍了HashMap整体介绍了一下数据结构,主要属性字段,获取数组的索引下标,以及几个构造方法.本文重点讲解元素的添加.查找.扩容等主要方法. 添加元素 put(K ...

  7. 给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析

    前面了解了jdk容器中的两种List,回忆一下怎么从list中取值(也就是做查询),是通过index索引位置对不对,由于存入list的元素时安装插入顺序存储的,所以index索引也就是插入的次序. M ...

  8. 【Java深入研究】9、HashMap源码解析(jdk 1.8)

    一.HashMap概述 HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现.与HashTable主要区别为不支持同步和允许null作为key和value.由于HashMap不是线程 ...

  9. HashMap 源码解析(一)之使用、构造以及计算容量

    目录 简介 集合和映射 HashMap 特点 使用 构造 相关属性 构造方法 tableSizeFor 函数 一般的算法(效率低, 不值得借鉴) tableSizeFor 函数算法 效率比较 tabl ...

随机推荐

  1. Ubuntu搭建lnmp环境

    1.安装nginx 安装 sudo apt-get install nginx 服务启动.停止.重启 /etc/init.d/nginx start /usr/sbin/nginx -c /etc/n ...

  2. Eclipse出现"Running Android Lint has encountered a problem"解决方案

    安装eclipse for android 时候的错误记录,转载自:http://blog.csdn.net/chenyufeng1991/article/details/47442555 (1)打开 ...

  3. Oozie分布式任务的工作流——Spark篇

    Spark是现在应用最广泛的分布式计算框架,oozie支持在它的调度中执行spark.在我的日常工作中,一部分工作就是基于oozie维护好每天的spark离线任务,合理的设计工作流并分配适合的参数对于 ...

  4. vim环境变量配置、背景色配置

    我们使用vi或者vim的时候,如果想要显示行号,可能会这样做:切换到命令模式,然后输入set nu,再按回车键就显示了:还有就是咱们在编写程序的时候,有的时候会希望按下回车键后,光标不是每次都在行首, ...

  5. LeetCode - Two Sum

    Two Sum 題目連結 官網題目說明: 解法: 從給定的一組值內找出第一組兩數相加剛好等於給定的目標值,暴力解很簡單(只會這樣= =),兩個迴圈,只要找到相加的值就跳出. /// <summa ...

  6. MyEclipse对Maven的安装

    好记性不如烂笔头,记录一下. 操作系统:windows 7 MyEclipse2015 JDK1.7 maven的下载链接,点这里下载apache-maven-3.0.4-bin.tar.gz. 下载 ...

  7. Collections

    2017-01-06  22:50:43 数据结构和算法 <如何学习数据结构?>:https://www.zhihu.com/question/21318658 <How do I ...

  8. Java中的Checked Exception——美丽世界中潜藏的恶魔?

    在使用Java编写应用的时候,我们常常需要通过第三方类库来帮助我们完成所需要的功能.有时候这些类库所提供的很多API都通过throws声明了它们所可能抛出的异常.但是在查看这些API的文档时,我们却没 ...

  9. WPF自定义控件第一 - 进度条控件

    本文主要针对WPF新手,高手可以直接忽略,更希望高手们能给出一些更好的实现思路. 前期一个小任务需要实现一个类似含步骤进度条的控件.虽然对于XAML的了解还不是足够深入,还是摸索着做了一个.这篇文章介 ...

  10. 【Java并发编程实战】-----“J.U.C”:CountDownlatch

    上篇博文([Java并发编程实战]-----"J.U.C":CyclicBarrier)LZ介绍了CyclicBarrier.CyclicBarrier所描述的是"允许一 ...