前言:Map实现类用于保存具有映射关系的数据,每项数据保存的都是key,value键值对。Map里面的key是不可重复的,key是用于标识集合里的每项数据,是由Set来组织起来的,通过Set也就意味着有去重功能。而Map里面的value是可以重复的,是通过Collection组织起来的。
HashMap(Java8以前):数组+链表
数组的特点:查询速度快,增删较慢;链表的特点:查询速度慢,增删较快。
因此HashMap结合了两者的优势,同时HashMap的操作是非synchronized的,因此效率比较高,存储的内容是键值对映射。
HashMap(Java8及以后):数组+链表+红黑树
java8及以后是使用一个常量(TREEIFY_THRESHOLD)是否将链表转换成红黑树来存储它们,
来看看HashMap的内部结构:HashMap可以看作是数组 Node<k,v>[] table 和链表来组成的复合结构,在Java8以前数组里面的元素叫Entry,无论是树还是链表,里面的元素都是节点。
Node的组成:
注:next是指向下一个节点
HashMap的成员变量:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 填充因子
final float loadFactor;
}
HashMap的构造函数:
// 默认构造函数。
HashMap()
// 指定“容量大小”的构造函数
HashMap(int capacity)
// 指定“容量大小”和“加载因子”的构造函数
HashMap(int capacity, float loadFactor)
// 包含“子Map”的构造函数
HashMap(Map<? extends K, ? extends V> map)
Put方法核心解读(添加元素):
put调用了putval ,当put调用,tabel为空的时候,就会调用resize()方法来初始化tab,继续进行hash运算,算出具体的tab位置,resize不仅可以初始化还可以扩容。
逻辑总结:
- 如果HashMap未被初始化,则初始化;
- 对key求hash值,再计算出tab的下标;
- 如果没有碰撞(tab数组里面对应的位置没有相应的键值对,则将键值对直接放入到相应的数组位置中),直接放进桶中;
- 如果有碰撞了(数组位置已经有元素了),就以链表的形式链接到后面;
- 判断链表的长度(一旦超过阈值,就会转化成红黑树);
- 判断链表长度低于6,就会把红黑树转化成链表;
- 如果桶需要扩容就调用resize()进行扩容;
Get方法解析:
当你传递一个key从HashMap总获取value的时候,对key进行null检查。如果key是null,table[0]这个位置的元素将被返回。key的HashCode()方法被调用,然后计算hash值。
indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。
在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
要牢记以下关键点:
- · HashMap有一个叫做Entry的内部类,它用来存储key-value对。
- · 上面的Entry对象是存储在一个叫做table的Entry数组中。
- · table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。
- · key的hashcode()方法用来找到Entry对象所在的桶。
- · 如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。
- · key的equals()方法用来确保key的唯一性。
- · value对象的equals()和hashcode()方法根本一点用也没有。
如何减少碰撞?
- 扰动函数:促使元素位置分布均匀,减少碰撞几率;
- 使用final对象,并采用合适的equals()和hashCode()方法将会减少碰撞的发生;
影响HashMap性能的有两个参数:初始容量(initialCapacity) 和加载因子(loadFactor)。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
1、key值可以为空吗?
hashMap把Null当做一个key值来存储,看源码
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//源码中判断了,如果key值未空的时候,没有返回错误信息,也是允许存储的
if (key == null)
return putForNullKey(value);
int hash = hash(key);
.............
}
2、如果Hash key重复了,那么value值会覆盖吗?
不会覆盖。在Entry类中,有个Entry< K,V > next 实例变量;它是来存储hashKey冲突时,存放就的value值。不会覆盖。上源码
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int 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))) {
//这里实际上是先获取原来的value,保存老的备份,可以通过 xxx.get("Jack_wu").next.getValue()获取。
V oldValue = e.value; // 假设原来的是30,传进来的是31 。 目前还是30
//然后在把当前的value赋给它
e.value = value; // 31
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
说说hashmap如何处理碰撞的,或者说说它的扩容?
先说碰撞吧,由于hashmap在存值的时候并不是直接使用的key的hashcode,而是通过扰动函数算出了一个新的hash值,这个计算出的hash值可以明显的减少碰撞。还有一种解决碰撞的方式就是扩容,扩容其实很好理解,就是将原来桶的容量扩为原来的两倍。这样争取散列的均匀,比如:原来桶的长度为16,hash值为1和17的entry将会都在桶的0号位上,这样就出现了碰撞,而当桶扩容为原来的2倍时,hash值为1和17的entry分别在1和17号位上,整号岔开了碰撞。
下面说说何时扩容,扩容都做了什么。
1.7中,在put元素的过程中,判断table不为空、切新增的元素的key不与原来的重合之后,进行新增一个entry的逻辑。
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);
}
由源代码可知,在新增元素时,会先判断:
1)当前的entry数量是否大于或者等于阈值(loadfactory*capacity);
2)判断当前table的位置是否存在entry。
经上两个条件联合判定,才会进行数组的扩容工作,最后扩容完成才会去创建新的entry。
而扩容的方法即为:resize()