先进行基本操作:
public static void main(String[] args) {
Map<String,Object> map=new HashMap<>();
map.put("key=1","value=100");
Object put=map.put("key=1","value=200");
System.out.println(put);
System.out.println(map.get("key=1"));
}
输出:
value=100
value=200
- value=100说明这put()是有返回值的,返回被覆盖的那个旧值,
- value=200说明key相同会覆盖,一个key对应一个value
Hash Map就是哈希表的操作,HashMap就是解决冲突的链地址法,索引值一样的话,就形链表,jdk1.7用的的是头插法,1.8用的是尾插法,1.7认为头插比较快,因为尾插需要遍历到最后一个链表地址,再插入。 - 1.7会导致并发时死循环,就是在扩容时,由于是反着的,AB两人同时操作扩容,A扩容完了,B是不知道的,在A扩容好的里面,e和e.next反过来了,会导致循环链表,并一直循环。
开始看源码:
1、存储结构
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
2、构造方法
//默认初始化化容量,即16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,即2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态
static final Entry<?,?>[] EMPTY_TABLE = {};
//空的存储实体
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//实际存储的key-value键值对的个数
transient int size;
//阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold
int threshold;
//负载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;
//用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
transient int modCount;
//默认的threshold值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
/*
*initialCapacity传进来的数组容量,loadFactor扩容因子0-1
*先对这两个值进行合法判断
*threshold = initialCapacity;把容量赋值给了一个阈值?
*/
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();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, 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);
inflateTable(threshold);
putAllForCreate(m);
}
threshold = (int)(capacity * loadFactor);1.6
threshold = initialCapacity;1.7
this.threshold = tableSizeFor(initialCapacity);1.8
3.put()添加
/*
* 首先判断table数组是否为空,如果是空的进行初始化 inflateTable()
*/
public V put(K key, V value) {
//如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);//分配数组空间,最小是二
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;//下面返回老值
e.value = value;//直接覆盖
e.recordAccess(this);//调用value的回调函数,其实这个函数也为空实现
return oldValue;
}
}
modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);//新增一个entry
return null;
}
inflateTable的源码如下:
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1
table = new Entry[capacity];//分配空间
initHashSeedAsNeeded(capacity);//选择合适的Hash因子
}
inflateTable 初始化表这个方法用于为主干数组table在内存中分配存储空间,调用的Integer.highestOneBit,通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32。其实现如下:
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;
}
Integer.highestOneBit很巧妙
位运算代替小于等于10的最小2次方数
/*
* 假设我想找到 小于等于10的最小2次方数
* 10------》1010
* 8-------》1000
*
* 现在看这个方法,假设我传进来了一个10( ... 0000 1010)
* 右移一位 ( ... 0000 0101) 进行或运算,有一则为一
* 结果 ( ... 0000 1111)
* 右移两位 ( ... 0000 0011) 进行或运算
* 结果 ( ... 0000 1111)
* 。。。
* 最终i= ( ... 0000 1111)
* i进行右移一位 ( ... 0000 0111)进行相减
* 最终的返回值为(... 0000 1000) 8
*/
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
hash():对key的hashcode进一步进行计算
//用了很多的异或,移位等运算,对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {//这里针对String优化了Hash函数,是否使用新的Hash函数和Hash因子有关
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
从上面的操作看以看出,影响HashMap元素的存储位置的只有key的值,与value值无关。
通过hash函数得到散列值后,再通过indexFor进一步处理来获取实际的存储位置,其实现如下:
indexFor()其实就是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);
}
1,HashMap的初始容量和扩容都是以2的次方来进行的,为啥呢?
- 因为length-1换算成二进制的话,后几位都是1,0000 1111,这样与添加元素的hash值既可以进行位运算,速度快,又能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。就比如数组长度为8,2的3次方为8(1000),length-1的二进制表示就是0111, 而按位与计算的原则是两位同时为“1”,结果才为“1”,否则为“0”。假设计算出来的hash值为135,所以h& (length-1)运算从数值上来讲其实等价于对length取模,也就是h%length。
2.如果不满足前提条件“HashMap的初始容量和扩容都是以2的次方来进行的”,会发生什么问题呢?
- 假设当前table的length是15,二进制表示为1111,那么length-1就是1110,此时有两个hash值为8和9的key需要计算索引值:
这样一来就产生了相同的索引值,也就是说两个hash值为8和9的key会定位到数组中的同一个位置上形成链表,这就产生了碰撞
同时,我们也可以发现,当数组长度为15的时候,hash值会与length-1(1110)进行按位与,那么计算结果最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,会造成严重的空间浪费,更糟的是这种情况下,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率。
因此可以看出,只有当数组长度为2的n次方时,不同的key计算得出的index索引相同的几率才会较小,数据在数组上分布也比较均匀,碰撞的几率也小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
此外,位运算快于十进制运算,hashmap扩容也是按位扩容,这样同时也提高了运算效率。
最后,得到存储的下标位置后,我们就可以将元素放入HashMap中,具体通过addEntry实现:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容,新容量为旧容量的2倍,放的table[index]刚好没有值,就放进去,不扩容了
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);//扩容后重新计算插入的位置下标
}
//把元素放入HashMap的桶的对应位置
createEntry(hash, key, value, bucketIndex);
}
//创建元素
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++;//元素个数+1
}
重点:
Entry<K,V> e = table[bucketIndex];
//获取待插入位置元素,就是拿到3–>2–>1这个链表,给e
table[bucketIndex] = new Entry<>(hash, key, value, e);
//这里执行链接操作,使得新插入的元素指向原有元素。就是Entry那个构造,把新加的 key, value, 和刚拿到的e连接起来了,在放到table的位置。
4、扩容操作
就是resize();
主要是
1.transfer()老表数据拷贝到新表
2.更新table
3.更新阈值(记得和最大值减一比较)
//按新的容量扩容Hash表
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));//1.将老的表中的数据拷贝到新的结构中
table = newTable;//2.修改HashMap的底层数组
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//3.修改阀值
}
如果数组进行扩容,数组长度发生变化,而存储位置 index = h&(length-1),index也可能会发生变化,需要重新计算index,(1.8不管这个,直接按原来的位置加容量)我们先来看看transfer这个方法:
//将老的表中的数据拷贝到新的结构中
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) {//如果是重新Hash,则需要重新计算hash值
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);//定位Hash桶
e.next = newTable[i];//元素连接到桶中,这里相当于单链表的插入,总是插入在最前面
newTable[i] = e;//newTable[i]的值总是最新插入的值
e = next;//继续下一个元素
}
}
}
5、get操作
//获取key值为key的元素值
public V get(Object key) {
if (key == null)//如果Key值为空,则获取对应的值,这里也可以看到,HashMap允许null的key,其内部针对null的key有特殊的逻辑
return getForNullKey();
Entry<K,V> entry = getEntry(key);//获取实体
return null == entry ? null : entry.getValue();//判断是否为空,不为空,则获取对应的值
}
//获取key为null的实体
private V getForNullKey() {
if (size == 0) {//如果元素个数为0,则直接返回null
return null;
}
//key为null的元素存储在table的第0个位置
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)//判断是否为null
return e.value;//返回其值
}
return null;
}
get方法通过key值返回对应value,如果key为null,直接去table[0]处检索,遍历链表。我们再看一下getEntry这个方法:
//获取键值为key的元素
final Entry<K,V> getEntry(Object key) {
if (size == 0) {//元素个数为0
return null;//直接返回null
}
int hash = (key == null) ? 0 : hash(key);//获取key的Hash值
for (Entry<K,V> e = table[indexFor(hash, table.length)];//根据key和表的长度,定位到Hash桶
e != null;
e = e.next) {//进行遍历
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//判断Hash值和对应的key,合适则返回值
return e;
}
return null;
}
HashMap的使用总结
- HashMap是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变(发生扩容时,元素位置会重新分配),并且无序,就是放的时候是随即着地放的。
- 迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。
- HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
- 如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
- HashMap的实现不是同步的。如果在多线程操作下,应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:
Map m = Collections.synchronizedMap(new HashMap(…));
- 由所有此类的“collection 视图方法”所返回的迭代器都是快速失败 的(modcount):在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器(itriter)本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。