JDK版本1.7
|
数据结构
|
底层实现
|
初始容量
|
扩容
|
负载因子(默认值)
|
线程安全
|
链表插入值方法
|
|
HashMap
|
数组+链表
|
16
|
整个map扩容,newsize = oldsize*2,size一定为2的n次幂
|
0.75
|
否
|
头插法
|
HaspMap
- 底层数组+链表实现,可以存储null键和null值,线程不安全
- 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
- 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
- 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
- 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
- 计算index方法:index = hash & (tab.length – 1)
源码解析
1.初识HashMap实现
public class HashMap
extends AbstractMap
implements Map, Cloneable, Serializable
{
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 初始化桶大小,因为底层是数组,所以这是数组默认的大小
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30; //桶的最大值
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认的负载因子(0.75)
/**
* An empty table instance to share when the table is not inflated.
*/
static final Entry[] EMPTY_TABLE = {};
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table = (Entry[]) EMPTY_TABLE; //table 真正存放数据的数组
/**
* The number of key-value mappings contained in this map.
*/
transient int size; //Map 存放数量的大小
/**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
int threshold; //桶大小,可在初始化时显式指定
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor; //负载因子,可在初始化时显式指定
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
/**
* The default threshold of map capacity above which alternative hashing is
* used for String keys. Alternative hashing reduces the incidence of
* collisions due to weak hash code calculation for String keys.
*
* This value may be overridden by defining the system property
* {@code jdk.map.althashing.threshold}. A property value of {@code 1}
* forces alternative hashing to be used at all times whereas
* {@code -1} value ensures that alternative hashing is never used.
*/
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, 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;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
HashMap中给定的默认容量为 16,负载因子为 0.75,Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能,因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。
static class Entry implements Map.Entry {
final K key;
V value;
Entry next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry 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;
}
-
key 就是写入时的键
-
value 自然就是值
-
HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构
-
hash 存放的是当前 key 的 hashcode
2.put方法
2-1 put方法一览
public V put(K key, V value) {
if (table == EMPTY_TABLE) { //判断当前数组是否需要初始化
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value); //如果 key 为空,则 put 一个空值进去
int hash = hash(key); //根据 key 计算出 hashcode
int i = indexFor(hash, table.length); //根据计算出的 hashcode 定位出所在桶
for (Entry e = table[i]; e != null; e = e.next) { //如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值
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); //如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置
return null;
}
- 判断当前数组是否需要初始化
- 如果 key 为空,则 put 一个空值进去
- 根据 key 计算出 hashcode
- 根据计算出的 hashcode 定位出所在桶
- 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值
- 如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置
2-2 hash方法、indexFor方法一览
当向 HashMap 中 put一对键值时,它会根据 key的 hashCode 值计算出一个位置, 该位置就是此对象准备往数组中存放的位置。计算过程参看如下代码:
transient int hashSeed = 0;
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);
}
/**
* Returns index for hash code h.
*/
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);
}
通过hash计算出来的值将会使用indexFor方法找到它应该所在的table下标。当两个key通过hashCode计算相同时,则发生了hash冲突(碰撞),HashMap解决hash冲突的方式是用链表(拉链法)。当发生hash冲突时,则将存放在数组中的Entry设置为新值的next(这里要注意的是,比如A和B都hash后都映射到下标i中,之前已经有A了,当map.put(B)时,将B放到下标i中,A则为B的next,所以新值存放在数组中,旧值在新值的链表上)。即将新值作为此链表的头节点,为什么要这样操作?据说后插入的Entry被查找的可能性更大(因为get查询的时候会遍历整个链表)。有一种说法就是链表查找复杂度高,可插入和删除性能高,如果将新值插在末尾,就需要先经过一轮遍历,这个时间复杂度高,开销大,如果是插在头结点,省去了遍历的开销,还发挥了链表插入性能高的优势。
2-3 addEntry、createEntry方法一览
找到数组下标后,会先进行key判重,如果没有重复,就准备将新值放入到链表的表头。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经有元素了,那么要扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 扩容
resize(2 * table.length);
// 扩容以后,重新计算 hash 值
hash = (null != key) ? hash(key) : 0;
// 重新计算扩容后的新的下标
bucketIndex = indexFor(hash, table.length);
}
// 往下看
createEntry(hash, key, value, bucketIndex);
}
// 这个很简单,其实就是将新值放到链表的表头,然后 size++
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
2-4 putForNullKey方法一览
private V putForNullKey(V value) {
for (Entry 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;
}
当size>=threshold( threshold等于“容量 * 负载因子”)时,会发生扩容。
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);
}
- 特别提示:jdk1.7中resize,只有当 size>=threshold并且 table中的那个槽中已经有Entry时,才会发生resize。即有可能虽然size>=threshold,但是必须等到相应的槽至少有一个Entry时,才会扩容,可以通过上面的代码看到每次resize都会扩大一倍容量(2 * table.length)。
3.get方法
3-1 get方法一览
public V get(Object key) {
if (key == null) //判断key是否为空
return getForNullKey(); //key为空则直接返回table[0]
Entry entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key); //根据 key 计算出 hashcode,然后定位到具体的桶中
for (Entry 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)))) //key、key 的 hashcode 是否相等来返回值
return e;
}
return null;
}
- 首先也是根据 key 计算出 hashcode,然后定位到具体的桶中
- 判断该位置是否为链表
- 不是链表就根据 key、key 的 hashcode 是否相等来返回值
- 为链表则需要遍历直到 key 及 hashcode 相等时候就返回值
- 啥都没取到就直接返回 null