HashMap简单介绍
HashMap的成员属性
相关代码解释:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量 2的30次方
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认的加载因子
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //哈希桶,存放链表。 长度是2的N次方,或者初始化时为0.
transient int size;// 键值对的个数/
int threshold; //16*0.75 =12
final float loadFactor; // 加载因子 0.75 传进来的值
transient int modCount;// map结构修改次数,累加/
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;/** 默认阈值*/
transient int hashSeed = 0;
问题一:HashMap的特点
- 1.继承了AbstractMap类,实现了Cloneable,Serializable,Map接口;
- 2.允许key/value为null,但key只能有一个null;
- 3.非线程安全,多个线程同时操作同一个HashMap实例所做的修改不同步;
- 4.进行遍历时如果执行HashMap的remove(Object key)或者put(Object value)方法时会快速失败,抛出ConcurrentModeificationException。遍历时删除元素只能通过Iterator本身的remove()方法实现。
问题二:什么是哈希冲突,解决哈希冲突的方法有哪些,HashMap使用哪种方法?
哈希冲突就是根据key即经过一个函数f(key)得到的结果作为地址去存放当前的key,value键值对,但是却发现算出来的地址上已经被占用了,这就产生了哈希冲突。
解决哈希冲突的四种方法:
开放定址法
- 线性探测法
- 线性补偿探测法
- 伪随机探测
链地址法
再哈希法
当发生冲突时,使用第二个、第三个哈希函数计算地址,直到无冲突时。
建立一个公共溢出区
假设哈希函数的值域为[0,m-1],则设向量HashTable[0,m-1]为基础表,另外设立存储空间向量OverTable[0,v]用来存储发生冲突的纪录。
问题三:如果自己指定HashMap的初始容量和加载因子,那么容量的大小,和加载因子的大小对HashMap有什么影响?
几种构造函数:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始容量最大不能超过2的30次方
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);
}
根据构造方法可以看出,无论是使用默认的初始容量还是使用指定的初始容量,当你调用HashMap的构造方法时,HashMap是没有进行初始化容量,也就是现在是一个空的HashMap(容量为0),在put(K key,V value)加入第一个元素时调用inflateTable(int toSize)进行扩容,如果初始容量设置为2的幂次方时,就会按照你设置的容量设置;当你设置的初始容量不是2的幂次方时,就会按照你设置的值取大于这个值的最近的2的幂次方。
负载因子的大小决定了HashMap的数据密度。负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或者插入时的比较次数增多,性能下降;负载因子越小,就越容易出发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也越短,查询和插入是的比较次数也越小,性能会越高,但是会造成一定的空间浪费,而且经常扩容也会影响性能,建议初始化预设大一点的空间。
put()加入一个对象
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);//threshold 阈值
}
if (key == null)
return putForNullKey(value);
int hash = hash(key); //扰动处理后的key的hashcode
int i = indexFor(hash, table.length);//值应该存放在哪个下标下面
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k; //key 小明 value 13 key 小明 value 12
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //当有重复的key插入的时候就会替换掉之前的
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);//将传入的容量大小转化为:>传入容量大小的最小的2的次幂
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//阈值
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) { //本身已经存放一个key为null
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//本身没有存放一个key为null
modCount++;
addEntry(0, null, value, 0);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); //2倍扩容
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
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++;
}
问题四:HashMap如何计算key-value结构的存储位置
-
首先根据要查找的key计算出对应的扰动处理后的key的哈希码 int hash = hash(key);
-
然后通过哈希码找到对应哈希表中桶中的地址(值应该存放在哪个下标下面) int i = indexFor(hash, table.length);
-
然后判断桶是链表结构还是红黑树结构
-
普通链表直接遍历链表查找即可
-
红黑树TODO
-
问题五:为什么HashMap的容量要保持2的幂次方
HashMap的容量为什么是2的幂次方,和indexFor(int h, int length)方法中的h & (length-1)有关,符号&是按位与的计算,这是位运算,计算机能直接运算,特别高效,按位与的计算方法是只有当对应位置的数据都是1时,运算结果也为1,当HashMap的容量是2的幂次方时,(n-1)的二进制也就是1111...111,这样与添加的元素的hash进行位运算时,能够得到充分的散列,使得添加的元素均匀地分布在HashMap的每一个位置,减少哈希碰撞。
hash()和indexFor()方法
final int hash(Object k) {
int h = hashSeed; //0
if (0 != h && k instanceof String) { //在我们的系统里hashSeed = 0
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode(); // key 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);
//扰动处理 加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少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);//hash(key) & 16 -1 = hash(key) % 16
}