HashMap源码分析
HashMap是很重要的数据结构,也是面试的宠儿,这里就HashMap的源码进行分析,以便理解HashMap的实现。
散列表
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。 也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。 这个映射函数称做散列函数,存放记录的数组称做散列表。
散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快定位。散列函数有直接定址法、数字分析法、平方取中法、折叠法、随机数法、除留余数法等。
解决哈 希冲突的方法一般有:开放定址法、链地址法(拉链法)、再哈希法、建立公共溢出区等方法。在开放定址法中解决冲突的方法有:线行探查法、平方探查法、双散列函数探查法等。开放定址法的缺点在于删除元素的时候不能真的删除,否则会引起查找错误,只能做一个特殊标记。只到有下个元素插入才能真正删除该元素。
数据结构
在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值时,将链表转换为红黑树,这样大大减少了查找时间。
类结构
类常量
// 默认初始容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量为2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子是0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表的数量 大于等于8个并且桶的数量大于等于64时链表树化
// hash表某个节点链表的数量小于等于6时树拆分
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
实例变量
// hash桶。在第一次放置元素时初始化,必要时进行扩容(不会缩容),长度一直是2的幂次方。
transient Node<K,V>[] table;
// 键值对的数量
transient int size;
// HashMap结构修改的次数
transient int modCount;
// 扩容的阈值,当键值对的数量超过这个阈值会产生扩容
int threshold;
// 负载因子
final float loadFactor;
构造函数
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;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
HashMap有4个构造函数。hash桶没有在构造函数中初始化,而是在第一次存储键值对的时候进行初始化。
tableSizeFor
这里重点看下tableSizeFor(initialCapacity)
方法,这个方法的作用是,将你传入的initialCapacity
做计算,返回一个大于等于initialCapacity
最小的2的幂次方。
所以这个操作保证无论你传入的初始化Hash桶长度参数是多少,最后hash表初始化的长度都是2的幂次方。比如你输入的是6,计算出来结果就是8。
【源码】
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
【分析】
先来分析有关n位操作部分:先来假设n的二进制为01xxx...xxx。接着
对n右移1位:001xx...xxx,再位或:011xx...xxx
对n右移2为:00011...xxx,再位或:01111...xxx
此时前面已经有四个1了,再右移4位且位或可得8个1
同理,有8个1,右移8位肯定会让后八位也为1。
综上可得,该算法让最高位的1后面的位全变为1。