最近研究了下hashmap的源码,在此记录一下
HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null 建和null 值, 因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。HashMap是线程不安全的。
在jdk1.8以后hashmap改成数组+链表结构+红黑树
什么是链表
链表是由一系列非连续的节点组成的存储结构,简单分下类的话,链表又分为单向链表和双向链表,而单向/双向链表又可以分为循环链表和非循环链表,下面简单就这四种链表进行图解说明。
1.单向链表
单向链表就是通过每个结点的指针指向下一个结点从而链接起来的结构,最后一个节点的next指向null。
2.单向循环链表
单向循环链表和单向列表的不同是,最后一个节点的next不是指向null,而是指向head节点,形成一个“环”。
3.双向链表
从名字就可以看出,双向链表是包含两个指针的,pre指向前一个节点,next指向后一个节点,但是第一个节点head的pre指向null,最后一个节点的tail指向null。
4.双向循环链表
双向循环链表和双向链表的不同在于,第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,也形成一个“环”。而LinkedList就是基于双向循环链表设计的。
初始化hashmap
HashMap<String,String> stringStringHashMap = new HashMap<String, String>();
stringStringHashMap.put("1","1");
进去put源码
我们先对key进行运算
这个方法调用Object.hashCode方法,对我们传入的key进行运算取得一个整数,再把整数进行16异或运算和右移运算,把hashcode返回给上面。
HashMap的数组初始化大小是16,链表固定是8,负载因子是0.75。
当存入数据的大小达到数组大小*负载因子的扩容点时,数组会进行扩容,扩容的倍数是2的n次方,也是当前数组大小*2的大小。
链表超过8后会转红黑树,当树的长度小于6时会又翻转成链表。
这是put方法源码,做了一些注释。
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict)
{
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//resize方法就是初始化数组
n = (tab = resize()).length;
//通过hashcode对数组长度取模计算存储的索引位置,如果没有元素,直接赋值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else
{
Node<K, V> e;
K k;
//节点若是已存在,执行复制操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断是否是红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K, V>)p).putTreeVal(this, tab, hash, key, value);
else
{
//若是链表,遍历
for (int binCount = 0; ; ++binCount)
{
if ((e = p.next) == null)
{
p.next = newNode(hash, key, value, null);
//链表的长度是8,如果大于这个数就转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果key存在,直接覆盖
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果找到key,替换value
if (e != null) // existing mapping for key
{
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//判断是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
这是扩容方法
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//这里初始化HashMap的大小,初始大小是16
newCap = DEFAULT_INITIAL_CAPACITY;
//DEFAULT_LOAD_FACTOR是加载因子0.75,这是加载值,如果HashMap的数组达到这值时会进行扩容
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//给节点初始化大小
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
总结
- HashMap采用hash算法来决定Map中key的存储,并通过hash算法来增加集合的大小。
- hash表里可以存储元素的位置称为桶(bucket),如果通过key计算hash值发生冲突时,那么将采用链表的形式,来存储元素。
- HashMap的扩容操作是一项很耗时的任务,所以如果能估算Map的容量,最好给它一个默认初始值,避免进行多次扩容。
- HashMap的线程是不安全的,多线程环境中推荐是ConcurrentHashMap。
实现原理
- HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。
- 当我们给put(key, value)方法传递键和值时,它先调用key.hashCode()方法,返回的hashCode值,用于找到bucket位置,来储存Entry对象。
- Map提供了一些常用方法,如keySet()、entrySet()等方法。
keySet()方法返回值是Map中key值的集合;entrySet()的返回值也是返回一个Set集合,此集合的类型为Map.Entry。 - “如果两个key的hashcode相同,你如何获取值对象?”答案:当我们调用get(key)方法,HashMap会使用key的hashcode值,找到bucket位置,然后获取值对象。
- “如果有两个值对象,储存在同一个bucket ?”答案:将会遍历链表直到找到值对象。
- “这时会问因为你并没有值对象去比较,你是如何确定确定找到值对象的?”答案:找到bucket位置之后,会调用keys.equals()方法,去找到链表中正确的节点,最终找到要找的值对象。
底层的数据结构
HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。
- HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。
- 如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。
- 学过数据结构的同学都知道,解决hash冲突的方法有很多,HashMap底层是通过链表来解决hash冲突的。
补充知识:
- HashMap是基于哈希表的 Map 接口的实现。
- 此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)
- 此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
- 值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。
Map map = Collections.synchronizedMap(new HashMap());
- HashMap结合了ArrayList与LinkedList两个实现的优点,虽然HashMap并不会向List的两种实现那样,在某项操作上性能较高,但是在基本操作(get 和 put)上具有稳定的性能。
HashMap中的get方法
可以看出get方法里面又调用了getNode方法,如果getNode方法返回的是null,说明没找到这个key。进去getNode方法看一下
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//当table不为空,并且桶中不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//先检查桶中的头节点是否为我们需要get的节点
if (first.hash == hash && // always check first node
//满足key的地址相同或equals,返回头节点
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//桶中不止一个节点
if ((e = first.next) != null) {
//桶中为树
if (first instanceof TreeNode)
//在树中查找,这里有机会再讲吧
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//这里的do-while循环目的是遍历当前桶中的链表
do {
//两个key的hash值相同,并且有相同地址或者equals的key,返回目标节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}