HashMap的源码分析
来源:互联网 发布:淘宝卖家如何上货 编辑:程序博客网 时间:2024/06/05 17:11
上篇综述了一下Java的集合类,
本篇主要是看一下HashMap的源码,看下HashMap到底是个什么东西。
1.成员变量简要说明
/** * 最小容量 */ private static final int MINIMUM_CAPACITY = 4; /** * 最大容量 */ private static final int MAXIMUM_CAPACITY = 1 << 30; /** *初始值 */ private static final Map.Entry[] EMPTY_TABLE = new HashMapEntry[MINIMUM_CAPACITY >>> 1]; /** * 默认加载因子 */ static final float DEFAULT_LOAD_FACTOR = .75F; /** * 这个才是HashMap的真实内容 */ transient HashMapEntry<K, V>[] table; /** * HashMap允许Key为Null,Key为Null时就存在这里 */ transient HashMapEntry<K, V> entryForNullKey; transient int size; /** * 修改次数,和线程安全有关 */ transient int modCount; /** * 阈值, 阈值 = 容量 * 加载因子 */ private transient int threshold; private transient Set<K> keySet;//存了所有的Key private transient Set<Map.Entry<K, V>> entrySet; //存了所有的<KEY, VALUE> private transient Collection<V> values; //存了所有的Value
HashMap的值是一个数组, HashMapEntry[] table。
这个数组的单元是HashMapEntry,实际上是一个单向链表,结构如下
static class HashMapEntry<K, V> implements Entry<K, V> { final K key; V value; final int hash; HashMapEntry<K, V> next;典型的链表,有数据域(key、value、hash)和一个指针next。
也就是说,HashMap实际上就是个单向链表组成的数组。
modCount和Fail-Fast机制
每次对HashMap进行修改的时候都会增加这个值
在迭代器遍历的时候,一旦发现modCount的值被改变了,就会 throw new ConcurrentModificationException()。
这就是Fail-Fast机制。
private abstract class HashIterator { int nextIndex; HashMapEntry<K, V> nextEntry = entryForNullKey; HashMapEntry<K, V> lastEntryReturned; int expectedModCount = modCount; HashIterator() { if (nextEntry == null) { HashMapEntry<K, V>[] tab = table; HashMapEntry<K, V> next = null; while (next == null && nextIndex < tab.length) { next = tab[nextIndex++]; } nextEntry = next; } } public boolean hasNext() { return nextEntry != null; } HashMapEntry<K, V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (nextEntry == null) throw new NoSuchElementException(); HashMapEntry<K, V> entryToReturn = nextEntry; HashMapEntry<K, V>[] tab = table; HashMapEntry<K, V> next = entryToReturn.next; while (next == null && nextIndex < tab.length) { next = tab[nextIndex++]; } nextEntry = next; return lastEntryReturned = entryToReturn; } public void remove() { if (lastEntryReturned == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); HashMap.this.remove(lastEntryReturned.key); lastEntryReturned = null; expectedModCount = modCount; } }
2.方法说明
扩容
先要知道一个公式,加载因子、阈值、容量的关系: 阈值 = 容量 * 加载因子,HashMap的size超过阈值的时候,会对HashMap进行扩容。
if (size++ > threshold) { tab = doubleCapacity(); index = hash & (tab.length - 1); }扩容代码如下
private HashMapEntry<K, V>[] doubleCapacity() { HashMapEntry<K, V>[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { //达到最大容量就不进行扩容了 return oldTable; } int newCapacity = oldCapacity * 2;//每次扩容大小都是原来的2倍 HashMapEntry<K, V>[] newTable = makeTable(newCapacity); if (size == 0) { //size为0的时候不需要进行rehash操作 return newTable; } /** * 扩容后需要进行rehash操作 */ for (int j = 0; j < oldCapacity; j++) { HashMapEntry<K, V> e = oldTable[j]; if (e == null) { continue; } int highBit = e.hash & oldCapacity; HashMapEntry<K, V> broken = null; newTable[j | highBit] = e; for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) { int nextHighBit = n.hash & oldCapacity; if (nextHighBit != highBit) { if (broken == null) newTable[j | nextHighBit] = n; else broken.next = n; broken = e; highBit = nextHighBit; } } if (broken != null) broken.next = null; } return newTable; }
get()、put()方法
取值的复杂度,以前校招的时候好像不少这种题目,那时只是死背O(1),但是真的是O(1)吗?
public V get(Object key) { if (key == null) { HashMapEntry<K, V> e = entryForNullKey; return e == null ? null : e.value; } int hash = Collections.secondaryHash(key); HashMapEntry<K, V>[] tab = table; for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { K eKey = e.key; if (eKey == key || (e.hash == hash && key.equals(eKey))) { return e.value; } } return null; }
get()方法的步骤是这样的:
(1) key == null的情况,直接从entryForNullKey中取值。O(1)
(2) key != null的情况,
第一步。算hash值。
第二步。根据hash值从数组中取出链表,遍历链表。
遍历链表的时间复杂度是O(n),那么说HashMap的取值复杂度O(1)只有在 n = 1的时候满足了。
实际上,这个n不为1时也是不大的,基本上是接近O(1)的。所以说HashMap的取值复杂度是接近O(1)的。
为什么n不为1时一般页是不大的,可以从put()方法中找到原因。
put()方法
public V put(K key, V value) { if (key == null) { return putValueForNullKey(value); } int hash = Collections.secondaryHash(key); HashMapEntry<K, V>[] tab = table; int index = hash & (tab.length - 1); for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) { if (e.hash == hash && key.equals(e.key)) { preModify(e); V oldValue = e.value; e.value = value; return oldValue; } } // No entry for (non-null) key is present; create one modCount++; if (size++ > threshold) { tab = doubleCapacity(); index = hash & (tab.length - 1); } addNewEntry(key, value, hash, index); return null; }和get()是相近的
(1) key == null, 直接往entryForNullKey里放值。
(2) key != null, 算hash值,取链表遍历。
a.对应的key有value,则覆盖;
b.对应的key没value, 超过阈值则扩容,最后要调用addNewEntry
void addNewEntry(K key, V value, int hash, int index) { table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]); }从这里可以看出,如果HashMap足够分散的话(table的Size足够大),那么几乎每一次add都会在table上单独占用一个位置。
每次HashMap的大小超过阈值时,都会Double一下容量,再进行rehash操作,因此只要没达到容量上限,hashmap是足够分散的。
这就解释了为什么get()的时间复杂度是趋于O(1)。
remove
remove操作也是相似的,找到链表后和链表的删除节点是相同的。实际上,似乎知道了HashMap的结构后,所有的操作不用看代码心中都是清楚的,都是一个套路。
containsKey和containsValue(时间复杂度)
这个也是我曾经碰到过的面试题,先说答案,containsKey的复杂度是趋于O(1),containsValue的复杂度是趋于O(n)。
先看containsKey
@Override public boolean containsKey(Object key) { if (key == null) { return entryForNullKey != null; } int hash = Collections.secondaryHash(key); HashMapEntry<K, V>[] tab = table; for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { K eKey = e.key; if (eKey == key || (e.hash == hash && key.equals(eKey))) { return true; } } return false; }是不是和get很像,算出hash值根据hash值取链表遍历!
containsValue
@Override public boolean containsValue(Object value) { HashMapEntry[] tab = table; int len = tab.length; if (value == null) { for (int i = 0; i < len; i++) { for (HashMapEntry e = tab[i]; e != null; e = e.next) { if (e.value == null) { return true; } } } return entryForNullKey != null && entryForNullKey.value == null; } // value is non-null for (int i = 0; i < len; i++) { for (HashMapEntry e = tab[i]; e != null; e = e.next) { if (value.equals(e.value)) { return true; } } } return entryForNullKey != null && value.equals(entryForNullKey.value); }这个只能遍历数组和链表了,所以是接近O(n)。
enteySet、keySet、values解惑
private final class KeySet extends AbstractSet<K> { public Iterator<K> iterator() { return newKeyIterator(); } public int size() { return size; } public boolean isEmpty() { return size == 0; } public boolean contains(Object o) { return containsKey(o); } public boolean remove(Object o) { int oldSize = size; HashMap.this.remove(o); return size != oldSize; } public void clear() { HashMap.this.clear(); } }对KeySet的操作实际上就是对table的操作。
- HashMap的源码分析
- HashMap的源码分析
- HashMap 的源码分析
- hashmap的源码分析
- HashMap的containskey源码分析
- 关于HashMap的源码分析
- 【源码分析】HashMap的数据结构
- HashMap的简单源码分析
- 从HashMap到LruCache的源码分析
- 从HashMap到LruCache的源码分析
- 史上最详细的HashMap详解--源码分析
- java源码分析--HashMap的工作原理
- java HashMap的插入操作源码分析
- JDK1.8的HashMap源码分析
- HashMap源码分析,为什么是无序的?
- 源码分析---HashMap的底层结构
- HashMap的底层原理及源码分析
- 源码分析:HashMap
- opencv-python 基本例子3个---显示图片,绘制图形,人脸识别
- idea初使用之自动编译
- iOS开发基础pch设置
- C++:Windows下return,exit和ExitProcess的区别和分析
- 半双工串口
- HashMap的源码分析
- JavaScript代码应该放在HTML不同位置的区别
- golang在linux和windows下的安装以及环境变量的配置
- xcode智能提示失效
- Android 自定义View 之 圆形头像
- Navicat中MySQL server has gone away错误怎么办
- 【ActiveMQ】ActiveMQ在Windows的安装,以及点对点的消息发送案例
- 单链表的实现(python)
- OpenMAX IL接口/头文件