目录
HashMap其实就是存储一系列的链表数组,用链表来解决哈希冲突。
一、构造函数
有四种构造函数,最终调用到可以设置初始化容量initialCapacity和负载因子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;
threshold = initialCapacity;
init();
}
变量 | 默认值 | 备注 |
loadFactor (负载因子) | 0.75 | 用来控制数组的元素的稀疏程度。值越接近1,说明该数组结构中的存放的数据越多,查找效率会越低 |
initialCapacity (初始化容量) | 16 | |
threshold (实际容量) | 16 | 允许HashMap存放的最多元素的个数,当超过该值的时候,就会进行扩容 |
二、添加元素put()
在研究添加元素之前,先了解下存放key和value的Entry<K,V>[] table数组,其实这就是一个链表数组,用来存放添加的key和value。
1.链表
该链表结构的单个节点含有存放的数据的key、value、key对应的hash值以及指向下一个节点。下面是单个节点的含有的属性
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
//省略代码
}
2.存放数据put()
看下put()对应的源码
public V put(K key, V value) {
//1)如果是空table,则进行初始化,创建一个最接近的2的n次方的数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//2)如果key为null,则单独处理。所以HashMap是支持key为null
if (key == null)
return putForNullKey(value);
//3)将key通过哈希函数转换成hash值
int hash = hash(key);
//4)找到该hash值对应的数组的索引值
int i = indexFor(hash, table.length);
//采用链地址法来处理哈希冲突,将所有索引一致的节点构成一个单链表
//5)循环链表节点,若该HashMap中已经存在了该key的hash值,直到找到该数组中的节点的hash值和当前的key的hash值相同的时候,则更新alue
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;
}
}
modCount++;
//6)如果没有找到该key对应的hash值,则进行插入该值
addEntry(hash, key, value, i);
return null;
}
1)判断table是否为空,如果仍为空,则通过inflateTable()进行初始化,创建 一个最接近接近且>=2的N次方的数组的大小。
2)判断传入的key是null,则将该值存放到table[0]对应的链表元素的节点位置。从源码中可以看出,null对应的hash值为0,存放在table[0]的链表节点中。所以HashMap支持key为null
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//若存在key为null的节点,则直接替换原值
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//否则插入该值。key为null的hash值为0
addEntry(0, null, value, 0);
return null;
}
3)将非null的key值转换成hash值
4)找到该hash值在对应的table数组的索引值
5)在该索引值的位置上的链表元素从头节点开始查找是否存在该key值,若存在该key值,则直接替换里面的value对应的值
6)若不存在该key值,则将该元素插入到该链表的对应位置。从源码看下该value是怎么插入到链表中的
/**
* @param hash 哈希值
* @param key 插入的key
* @param value 插入的value
* @param bucketIndex table数组的索引值
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
//(6.1)若现在插入元素的已经超过了实际容量threshold,则进入扩容为目前长度的2倍
if ((size >= threshold) && (null != table[bucketIndex])) {
//(6.2)扩容2倍,并进行对插入的链表进行重排
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
//(6.3)得到新的table数组的索引值
bucketIndex = indexFor(hash, table.length);
}
//(6.4)插入该key和value
createEntry(hash, key, value, bucketIndex);
}
(6.2)进行扩容创建原容量2倍大小的数组,并通过transfer()来调整对应的位置的链表元素
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
//对原有元素在table中的位置进行重排
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
(6.3)得到对应的新的table中对应的索引值
(6.4)插入对应的key和value的节点
void createEntry(int hash, K key, V value, int bucketIndex) {
//找到对应的索引值的链表元素
Entry<K,V> e = table[bucketIndex];
//并将该元素作为当前链表的下一个节点
table[bucketIndex] = new Entry<>(hash, key, value, e);
//统计此时HashMap中元素的个数
size++;
}
- 插播——创建链表
(1)这里采用的链表节点的插入方式为头插法,是采用以下的这种插入方式
每个节点为
public class Node {
public int data;
public Node next;
public Node(int data, Node next) {
this.data = data;
this.next = next;
}
public Node() {
}
}
1为第一个节点,往后在加入节点的时候,要取出当前链表,然后作为当前要插入节点的next节点,header指针每次都向前移动,一行代码就可以实现链表结构:
private Node head1;
public void insert1(int data){
head1 = new Node(data,head1);
}
打印该链表的数据会依次为4,3,2,1
(2)还有一种插入方式为尾插法,如下图
1为第一个节点,往后在插入节点的时候依次作为前一个插入的节点的next节点。就是header不变化,每加入的节点为尾节点。
public void insert(int data) {
if (head == null) {
head = new Node();
head.data = data;
head.next = null;
return;
}
Node node = new Node();
node.data = data;
Node p = head;
while (p.next != null) {
p = p.next;
}
p.next = node;
打印该链表的数据依次为1,2,3,4
二、取元素get()
有了存的过程,取的过程其实也就简单了,就是找到key对应的table数组中的位置,然后找到在位置的链表结构,然后找到该元素。同样key为null的时候,对应的hash值为0
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//如果key为null,则直接hash值为0,否则得到对应key的hash值
int hash = (key == null) ? 0 : hash(key);
//根据hash值找到对应table数组中的索引值,取出对应的链表元素
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//直到找到对应的key值对应的value返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
三、其他方法和知识点
像判断HashMap的长度相关的size()、判断是不是有key相关的containsKey()等,其实都是从创建的链表数组中进行判断的。
我们看到对于HashMap中一些变量使用transient进行修饰。一旦变量被transient修饰,则变量修饰的内容在序列化后无法获得。transient只能修饰变量,不能修饰类和方法。并且该变量所在的类要实现Serializable接口。
JDK 1.7之前的HashMap利用链表寻址法来解决哈希冲突,其实在一种非常极端的情况下,就是创建的table的链表数组中其中一个元素的链表很长,其他的空间却没有利用,所以在1.8的时候进行了调整,后续还会继续去研究下源码。
线程同步 | key/value是否允许null | key/value是否允许重复 | |
HashMap | 不支持 | 允许 | key不允许重复/value允许重复 |
内部维持一个单链表来解决哈希冲突和记录的插入没有任何顺序
四、与HashTable的区别
线程安全 | 父类 | contains() | key/value | table数组初始值 | 扩容 | |
HashMap | 否 | AbstractMap | 否,只有containValue()/containKey() | 都允许 | 默认为16,若设置初始值需要调整为2的n次幂 | 2n |
Hashtable | 是 | Dictionary | 是,三者都有 | 都不允许 | 默认为11,若设置初始值无需调整 | 2n+1 |
当然两者的哈希函数也不相同。