HashMap的概要
HashMap基于哈希表的Map接口的实现。
允许null值,bull键。
不同步,线程不安全。
想要获得线程安全的HashMap可以用Collections的静态方法synchronizedMap方法。
HashMap的底层设计
底层主要使用数组和链表实现的。
因为它实际算散列码来决定存储位置,所以,查询速度相当快。
HashMap主要通过key的hashCode来计算hash值,只要hashCode值相同,计算出来的hash值就是一样的。
如果存储对象多了,可能引起hash值相同,这就是所谓的hash冲突,HashMap底层是通过链表来解决hash冲突。
hash链表的每个元素都横向产生了一个单链表,如果key映射到了链表同一位置,那就放入单链表中。
先比较hashCode值,然后equals比较,一样的话覆盖,不一样则添加到单链表的头,所以最近加入的最容易被访问。
HashMap底层数组的维护对象Entry
public class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
Entry(int h,K k,V v,Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
HashMap的构造函数
构造函数的两个参数,一个是默认的初始化容量,另一个是默认的负载因子。
负载因子:就是Map实际存放对象的个数和它所申请的空间大小的比值,如果超过这个比值,Map就会自动扩容,自动扩容的时候以2的指数的方式进行扩容。
放置对象我们要Entry空间,但是我们只在put方法后才创建,用到才创建,所以叫做懒汉法。
/*几个重要的参数,常看见的
*aka 16默认初始化的大小
*static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
*默认负载因子
*static final float DEFAULT_LOAD_FACTOR = 0.75f;
*扩容的临近值
*int threshold;
*/
//HashMap第一种构造方法,不带参数,默认值16,0.75
public HashMap() {
this(DEFAULT_INITIAL,DEFAULT_LOAD_FACTOR);
}
//HashMap的第二种构造方法,自己定义Map长度,0.75
public HashMap(int initialCapacity) {
this(initialCapacity,DEFAULT_LOAD_FACTOR);
}
//HashMap的第三种构造方法,这跟的是Map空间和时间效率
public HashMap(int initialCapacity,float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
// 在Map中也定义了容量的最大值,跟ArrayList一样
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
// threshold 为临界值,当集合中元素个数达到时就进行2的指数倍扩容
threshold = initialCapacity;
init();
}
// 将整个集合添加到Map中, 这个过程我们好好分析一下,首先传进来有个Map, 我们可以看到它之后调用了上面的带两个参数的构造函数HashMap(int,float),第一个数据是 (程序定义的最大容量)和(map的容量/负载因子 +1)中的最大值,比如m.size()=21, 那么初始的容量就为 21/0.75+1=29, 执行HashMap(29,0.75), 临界值 threshold =29;
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);
// 将m创建到新的Map中来
putAllForCreate(m);
}
// 扩容函数,为什么数组的长度必须定义为2的整数次幂呢,原因在后面
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize 首先找到一个数据是 2的幂次方 而且 大于toSize, 传进来的是29 意味着得到的capacity 是32
int capacity = roundUpToPowerOf2(toSize);
// 修改扩容的临界值,根据传进来的值,临界值为24,
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 申请一个32 个空间大小的Entry数组,table就是一个Entry类型的数组
table = new Entry[capacity];
// 初始化需要的hash种子
initHashSeedAsNeeded(capacity);
}
}
增加了对象的put()方法
这个会引发很多问题。
比如如果Key值为空的时候,它会直接将放到数组下标为0的位置,并没有计算hash值,这个地址计算有关,所有的Null键都是放在数组中的下表为table[0]的位置的。
如果key值不为空,计算key对象的hashCode()值,然后防置。
数组长度要为2的整数次幂的原因就是数组下标确定的时候的返回值,return h&(length-1);
扩容
HashMap是2的整数次幂扩容,ArrayList是1.5.
为什么HashMap不能保持元素的顺序
a):插入是对元素进行哈希处理,不同元素分配到不同位置
b):容量扩展的时候进行了hash处理
c):复制原表内容的时候链表倒置
get(key)方法和remove(key)方法
显示判断getEntry(key)的结果是不是null,返回boolean值。
clear()方法
这里就不用一个个删除节点,而是一起全部删了
快速失败机制
不是线程安全,在使用迭代器过程中修改了map,那么就会抛出异常,这就是快速失败策略。
本文来自给于