目录
第二个问题就是重写equals方法必须重写hashcode方法在HashMap这得到印证。
首先,来看一下jdk5的经典实现吧。
1、HashMap继承了AbstractMap,实现了三个接口:Map,Cloneable,Serializable
数据结构:数组+链表
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{}
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
// 存储hash(key)后key的hash值,避免了重复计算
final int hash;
// 指向下一个Entry的引用,单链表
Entry<K,V> next;
}
2、HashMap一些默认的参数取值
// 以下3个默认大小的字段都声明为final,说明在HashMap中不可变或者在构造函数中赋值
// 默认的初始化容量大小为16,容量大小必须为2的n次方
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大的容量大小为2的30次方,
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子是0.75f,float类型的,主要用作计算阀值大小
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// HashMap的结构就一个Entry类型的数组,不可被序列化
transient Entry[] table;
// size是HashMap的大小,也就是存了多少个key-value对,同样不可序列化
transient int size;
// 阀值大小,没有final修饰,在整个HashMap的操作过程中可变。threshold = capacity * load factor
// 当size++ >= threshold时,就要开始给table扩容2倍
int threshold;
// 负载因子
final float loadFactor;
// 修改次数,不仅不可序列化,而且在多线程情况下,其值对其他线程立即可见
// 这个值主要被用来集合做iterators迭代时,保证集合的快失败机制。也就是iterator迭代集合时,
// 集合被外部修改后,会报ConcurrentModificationException,具体见后面的方法分析
transient volatile int modCount;
3、HashMap的4个构造函数
// 两个入参:初始化容量大小, 负载因子
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);
// Find a power of 2 >= initialCapacity
int capacity = 1;
// 根据初始化容量大小,找到第一个比initialCapacity大的2的n次方的值
// 假设initialCapacity=15,则capacity=16;假设initialCapacity=17,则capacity=32;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
// 计算阀值,(capacity * loadFactor)==>(int*float)计算结果是浮点型的,需要强转成int型
threshold = (int)(capacity * loadFactor);
// 新建一个数组并初始化
table = new Entry[capacity];
init();
}
// 这个构造函数传入初始容量值,然后把默认的负载因子0.75f传入到上面的构造函数并调用
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 无参构造函数,各个参数都用默认值,构造出一个16个容量大小的数组
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
// 这个构造函数主要是用一个已有的Map集合,构造出一个新的HashMap
// 一开始没有判断m的空指针异常感觉有点不应该,虽然在代码注释部分有写NullPointerException
/**
* @param m the map whose mappings are to be placed in this map.
* @throws NullPointerException if the specified map is null.
*/
public HashMap(Map<? extends K, ? extends V> m) {
// 计算一个初始容量因子和默认的0.75f的负载因子去调用第一个构造函数
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
// 把m的值导入到HashMap中,源码见下方
putAllForCreate(m);
}
void putAllForCreate(Map<? extends K, ? extends V> m) {
// 便利m的entrySet,把每个Entry都插入到HashMap
for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i =
m.entrySet().iterator(); i.hasNext(); ) {
Map.Entry<? extends K, ? extends V> e = i.next();
putForCreate(e.getKey(), e.getValue());
}
}
// 这个方法只会在构造函数被调用时执行,不是put方法,它不会扩容,也不会检查修改次数等
private void putForCreate(K key, V value) {
K k = maskNull(key);
// 计算hash值和计算table的索引下文会提到
int hash = hash(k.hashCode());
int i = indexFor(hash, table.length);
/**
* Look for preexisting entry for key. This will never happen for
* clone or deserialize. It will only happen for construction if the
* input Map is a sorted map whose ordering is inconsistent w/ equals.
*/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
if (e.hash == hash && eq(k, e.key)) {
e.value = value;
return;
}
}
createEntry(hash, k, value, i);
}
static final Object NULL_KEY = new Object();
/**
* Returns internal representation for key. Use NULL_KEY if key is null.
*/
static <T> T maskNull(T key) {
return key == null ? (T)NULL_KEY : key;
}
// 创建Entry
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
size++;
}
4、HashMap最重要的方法put()出场了
// 往HashMap里面插入一对键值对,如果key已经存在就返回老的value,如果不存在就返回null
public V put(K key, V value) {
// 如果key是null,那就把Null_Key计算索引后放进HashMap(jdk8是放table[0])
if (key == null)
return putForNullKey(value);
// 计算hash值
int hash = hash(key.hashCode());
// 计算table的索引位置
int i = indexFor(hash, table.length);
// table[i]所处的链表上如果存在相同的key,则把value值替换,并返回旧的value值
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))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 增加修改次数,增加一个Entry在此索引处的链表头节点并返回null
// 保证并发访问时,HashMap内部修改时,能够快速失败
modCount++;
addEntry(hash, key, value, i);
return null;
}
// 放入key=null的键值对
private V putForNullKey(V value) {
// 同样的计算哈希值和数组索引
int hash = hash(NULL_KEY.hashCode());
int i = indexFor(hash, table.length);
// 遍历当前table的链表,如果此链表上已存在key=NULL_KEY的,则直接替换value
// 并return oldvlaue
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
if (e.key == NULL_KEY) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 当前数组节点的链表不存在key=NULL_KEY的,就在table[i]处新增一个Entry,并return null
modCount++;
// 新增一个Entry,当前的table[i]变成新增Entry的next节点
addEntry(hash, (K) NULL_KEY, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 如果当前size达到阀值了,则需要扩容2倍
if (size++ >= threshold)
resize(2 * table.length);
}
// 扩容操作,newCapacity=2*oldCapacity
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 如果旧容量已经达到最大容量了,则把阀值设置为Integer.MAX_VALUE并直接返回
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 建立一个新的数组
Entry[] newTable = new Entry[newCapacity];
// 旧table转到新table
transfer(newTable);
table = newTable;
// 重新计算阀值
threshold = (int)(newCapacity * loadFactor);
}
// 扩容时新旧数组转换
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
// 遍历旧的table
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
// 旧table当前Entry置null
src[j] = null;
// 遍历链表
do {
Entry<K,V> next = e.next;
// 重新哈希散列到新table
// 重新hash后,下一次get的时候怎么找得到呢?
// 因为e的hash值在扩容后没变,newCapacity在下一次扩容前也不变,
//所以get()的时候indexfor计算出来的值就和现在indexfor计算的结果是一样的
int i = indexFor(e.hash, newCapacity);
// 第一次执行时,newTable[i]=null
// 其实就是把e插入到这条链表的头节点处,之前的节点链接到此插入节点后面,
// 最后一个节点的next为null(第一次hash到此索引位置时)
e.next = newTable[i];
newTable[i] = e;
// 下一个链表节点
e = next;
} while (e != null);
}
}
}
// 计算数组索引
static int indexFor(int h, int length) {
return h & (length-1);
}
/**
* 两个hash算法,默认是oldHash
* Set to true only by hotspot when invoked via
* -XX:+UseNewHashFunction or -XX:+AggressiveOpts
*/
private static final boolean useNewHash;
static { useNewHash = false; }
static int hash(int h) {
return useNewHash ? newHash(h) : oldHash(h);
}
static int hash(Object key) {
return hash(key.hashCode());
}
6、HashMap里面的get()方法
// put方法看明白后,get方法就简单了,都是采用一样的算法
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
// 先计算数组索引位置,然后遍历找到key,最后返回value
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
// 得到key=NULL值的value
private V getForNullKey() {
int hash = hash(NULL_KEY.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> e = table[i];
while (true) {
if (e == null)
return null;
if (e.key == NULL_KEY)
return e.value;
e = e.next;
}
}
7、HashMap的其他方法
// 这个方法类似get()方法
public boolean containsKey(Object key) {
Object k = maskNull(key);
int hash = hash(k.hashCode());
int i = indexFor(hash, table.length);
Entry e = table[i];
while (e != null) {
if (e.hash == hash && eq(k, e.key))
return true;
e = e.next;
}
return false;
}
// 输入键key,删除HashMap集合元素,返回value
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
// 删除Entry
Entry<K,V> removeEntryForKey(Object key) {
Object k = maskNull(key);
int hash = hash(k.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
if (e.hash == hash && eq(k, e.key)) {
modCount++;
size--;
// 如果要删除的e刚好是table[i]处的元素,则直接把e的next放到table[i],就删了
if (prev == e)
table[i] = next;
// 如果e是链表中的元素,则把e的前一个元素指向e的下一个元素,就可以删除
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
// 清空table所有元素
public void clear() {
modCount++;
Entry[] tab = table;
for (int i = 0; i < tab.length; i++)
tab[i] = null;
size = 0;
}
然后,开始瞅一瞅JDK7的HashMap实现吧
看完源码后发现JDK7和JDK5的实现并没有什么本质的区别,一些细节方面的实现列举如下:
1、初始化HashMap的时候,不在构造函数里面构建数组了,而是在第一次put的时候,判断table是不是空,如果是空就构建一个table出来
2、放入key=null的位置不一样了,JDK5是new Object(),然后计算hash值,按照正常的key处理。JDK7是把key=null的放在table[0]位置,并且hash值也置0
3、addEntry()的时候,JDK5是先新建Entry,然后判断是否需要扩容。JDK7是先判断是否扩容
(size>=threshold && null != table[i]),扩容后重新计算hash和index,然后新建Entry
最后,对HashMap的两个思考
为什么容量大小一定是2的幂次方?
先想想HashMap中最耗时的操作会发生在什么时候,当然是扩容的时候。
indexfor()函数:
h & (length-1)
其中的length就是table.length();也就是容量大小capacity。
假设capacity=16,h=10,length-1就是15,15的二进制是地位全是1,h和它做与运算的结果就是:
00000000 00000000 00000000 00001010
&
00000000 00000000 00000000 00001111
= 00000000 00000000 00000000 00001010
假设现在扩容了,capacity=32,h=10,length-1就是31,h和它做与运算的结果就是:
00000000 00000000 00000000 00001010
&
00000000 00000000 00000000 00011111
= 00000000 00000000 00000000 00001010
看出来了吗,15和31之间的差别就是低位多了一个1,那么在扩容时,重新indexfor的时候,很多元素位置是不需要动的,这样就能保证新数组和老数组的一致性,减少数据调动。顺便提一句,在老数组copy到新数组时,copy的是引用,因为数组中存的是Entry的引用。
另外一点,上面低位全是1,说明index取决于hash值,而hash值的计算是通过一系列神奇的位运算使得hash值算出来比较均匀,从而使得index比较均匀,也就是数据分布比较均匀。
第二个问题就是重写equals方法必须重写hashcode方法在HashMap这得到印证。
举个例子:
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Main {
public static void main(String[] args) {
Map<Person, String> map = new HashMap<Person, String>();
Person person = new Person(18, "xxx");
map.put(person, "code");
System.out.println(map.get(new Person(18, "xxx")));
}
}
class Person{
int id;
String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()){
return false;
}
Person person = (Person) obj;
//两个对象是否等值,通过id来确定
return this.id == person.id;
}
@Override
public int hashCode() {
return Objects.hashCode(getId()) ^ Objects.hashCode(getName());
}
}
如果不重写hashCode()方法,则输出:null (HashMap在put和get时的索引值不一致导致,索引值由key的hash值决定)
如果重写hashCode()方法,则输出:code
问题:线程不安全
比如多线程情况下,resize()可能会发生死循环(循环链表)