当前位置:硬件测评 > 【java】HashMap1.7源码详解

【java】HashMap1.7源码详解

  • 发布:2023-10-02 02:07

前言

HashMap是Java API中一个非常强大的工具类,所以有必要分析一下它的源码;强调一下,这篇博文是针对熟悉HashMap基本使用的程序员的。

数据结构

在jdk1.7中,HashMap的基本数据结构是数组+链表的形式。
米) {}}

属性

//默认数组容量,使用位操作来提高速度,16转换成二进制码比1 << 4慢
静态最终 int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//数组的最大容量
静态最终 int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
静态最终浮点DEFAULT_LOAD_FACTOR = 0.75f;
// 空数组,初始化数组时使用
静态最终条目[] EMPTY_TABLE = {};
// 这个表数组用来存储Entry对象,这是HashMap保存数据最关键的一步。
//transient关键字的作用是:不参与对象的序列化
瞬态条目[]表=(条目[])EMPTY_TABLE;
//表中存在的的个数(数组+链表)
瞬态整型大小;
//数组扩容的临界值:threshold=loadFactor*capacity
int 阈值;
// 负载系数
最终浮动负载因子;
// 容器的修改次数:添加新数据(并不意味着修改值)、删除数据或清除数据时,使用modCount++
// 这个数据和ConcurrentModificationException相关,后面会解释。
瞬态 int modCount;
//映射容量默认阈值。当高于默认值时,哈希度会降低,需要选择新的哈希表。静态最终 int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

施工方法

public HashMap(int initialCapacity, float loadFactor) {//容量不能小于0if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);// 容量不能大于最大值 1 << 30if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;//负载因子不能小于等于0或非数值if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("非法负载因子: " +loadFactor);this.loadFactor = loadFactor;// 临界值临时分配给指定容量,并将后面重新计算的threshold=initialCapacity;// LinkedHashMap重写了,init()并没有真正在HashMap中实现;
}
// 使用默认负载因子
公共HashMap(int初始容量){这个(初始容量,DEFAULT_LOAD_FACTOR);
}
// 使用默认容量和负载因子
公共 HashMap() {this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//public HashMap(Map m) {//判断m的Capacity是否小于默认容量。如果小于,则选择默认容量 this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);// 创建一个 Entry[] 数组,具体内容将在稍后详述 inflateTable(threshold);// 遍历所有键值对并将其添加到新的映射中。这里的具体实现这里就不解释了 putAllForCreate(m);
}

HashMap最常见的使用方式就是put(K key, V value)方法,所以本博文就从put()方法入手,深入剖析。

核心方法

输入(K键,V值)

//向容器中添加新的Entry对象。如果存在,则替换该值并返回oldValuepublic V put(K key, V value) {// 延迟加载模式,每次调用put()方法时,都会先判断数组是否已经初始化 if (table == EMPTY_TABLE) {inflateTable(threshold); }// HashMap 可以用 key = null 保存数据 if (key == null) return putForNullKey(value); // hash()方法在HashMap的各个版本中的实现是不同的 // 读者只需要知道这个方法的作用就是获取一个已经尽可能哈希过的int值 int hash = hash(key );//获取数组下标 int i = indexFor(hash, table.length);//根据下标,遍历下标中的链表,如果找到旧值,则替换并返回旧值 for ( Entry e = table[i]; e != null; e = www.sychzs.cn) {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++; // 向数组添加新数据 addEntry(hash, key, value, i); // 如果没有旧值则返回 null return null;
}

inflateTable(阈值)

private void inflateTable(int toSize) {// 取容量为2的指数次方,且大于等于toSize。后面会解释原因 intcapacity = roundUpToPowerOf2(toSize);//最大临界值只能是MAXIMUM_CAPACITY+1//如果不指定capacity和loadFactor,则threshold=12threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);table = new Entry[capacity];//计算hashSeed,不超过MAXIMUM_CAPACITY则保持为0,映射最后一个属性 initHashSeedAsNeeded(capacity);
}

putForNullKey(V值)

private V putForNullKey(V value) {// 这里虽然遍历的是链表,但是在if判断中,e.key == null // 可以得出HashMap只能保存一个key=null的映射 for ( Entry e = table[0]; e != null; e = www.sychzs.cn) {if (e.key == null) {V oldValue = e.value;e.value = value;e. recordAccess(this);返回oldValue;}}modCount++;addEntry(0, null, value, 0);返回null;
}

indexFor(int h, int length)

static int indexFor(int h, int length) {return h & (length-1);
}

这个方法虽然只有一个说法,但是却关系到很多东西。例如,读者一定很好奇为什么默认容量是1 << 4 = 16,为什么会调用roundUpToPowerOf2(toSize)方法?这就给我们带来了HashMap最关键的一点,就是通过哈希值来确定数组下标。因为哈希值的哈希值非常大,而数组的容量有限,所以如何确定下标就非常关键。
程序员最容易想到的方式就是求模运算:hash % length
hash是一个int类型的值。假设length为默认容量16,那么hash % length得到的数组范围为0 - 15,这符合Array存储原理。
然而jdk作者选择的是位运算:hash & (length - 1)
我们通过一个例子来验证一下:假设length=16,那么length-1=15,二进制表示0000 1111

1。哈希= 1001 1010 0101 0111哈希和长度 - 1 = 1001 1010 0101 0111和0000 1111 = 0000 0111 = 71。哈希= 1001 1010 0101 0000哈希和长度 - 1 = 1001 1010 0101 0000& 0000 1111= 0000 0000 = 03. 哈希 = 1111 1111 0101 1111哈希值和长度 - 1 = 1111 1111 0101 1111& 0000 1111= 0000 1111 = 15

从上面验证的结论来看,得到的下标仍然在0-15之间,位运算的运算速度比取模运算要快。影响是容量必须是2的指数次方(二进制数每一位的权重是2的指数次方)。如果不是,那么capacity-1的二进制结果中的低位将不会全部为1,因此会增加哈希冲突的概率;这很容易理解。如果低位中的某个位为0,那么无论该哈希值对应的位是1还是0,和操作的结果都会是0,如果是0,得到的可能性是一样的下标变大。

addEntry(int哈希,K键,V值,intbucketIndex)

void addEntry(int hash, K key, V value, int bucketIndex) {// 如果的个数大于等于临界值且当前数组下标不为空,则扩容数组/ / 注意:这里的大小是映射的数量(包括链表),而不是数组中存储的有效值的数量 if ((size >= Threshold) && (null != table[bucketIndex])) {/ / 因为原来的容量是2的指数次方,所以可以通过resize(2 * table.length)来扩大原来的容量*2;// 扩大后,哈希表可能发生了变化(当超过最大值时value),所以需要重新计算hash值 hash = (null != key)? hash(key) : 0;bucketIndex = indexFor(hash, table.length);}createEntry(hash, key, value, BucketIndex);
}

从扩容条件之一size>=threshold可以看出,临界值threshold与是否需要扩容有关,threshold=capacity * loadFactor,所以真正影响扩容的是负载因子loadFactor。默认负载系数 = 0.75f。有的读者可能会好奇,为什么要取这样的值呢?默认情况下,size = 12时应该扩展容量,因此容量为16的数组会浪费4个存储空间(不一定,理想情况下)。既然造成浪费,为什么不将加载因子设置为1呢? ?原因是数组查找的效率是O(1)。如果数组存储接近满的时候进行扩容,很容易增加哈希冲突的概率。遍历链表会降低查找速度,而使用HashMap是因为它的查找性能优异。使用广泛,需要牺牲空间来换取时间;当然,有些读者会好奇,既然可以牺牲空间来换取时间,为什么不使用较低的loadFactor呢?这也是jdk开发者的一个选择,试图在空间和效率之间取得平衡。在程序开发中,这种平衡选择还是很常见的。

createEntry(int hash, K key, V value, int bucketIndex)

void createEntry(int hash, K key, V value, int bucketIndex) {//获取当前下标的数组元素Entry e = table[bucketIndex];//将获取到的元素作为下一个新的元素值,可以查看Entry的构造方法 table[bucketIndex] = new Entry<>(hash, key, value, e);size++;
}

HashMap中链表节点的添加采用前向插值方法

resize() - 多线程死循环问题

HashMap 1.7中,多线程扩展时可能会出现死循环

void resize(int newCapacity) {Entry[] oldTable = table;int oldCapacity = oldTable.length;//如果旧数组的容量已经是最大,则无法扩容,扩容临界值只能为Changed if (oldCapacity == MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return;}//详细说明:这里并没有调用InflateTable()来让容量变成2的指数幂//因为只会根据原非空表resize操作,且原表必须满足容量条件 Entry[] newTable = new Entry[newCapacity]; // 第二个参数一般为false,下面不是重点。 // 和最后一个属性有关,只有超过默认的最大值才会发生改变hashSeed // 如果改变hashSeed,就会出现新的哈希表,然后需要重新计算哈希值transfer(newTable , initHashSeedAsNeeded(newCapacity));table = newTable;//计算临界值,无需赘述 Threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

transfer(Entry[] newTable, boolean rehash)这个方法是死锁的罪魁祸首

void transfer(Entry[] newTable, boolean rehash) {int newCapacity = newTable.length;// 遍历数组,只遍历数组下标元素 for (Entry e : table) {while(null != e ) {条目下一个= www.sychzs.cn; // 只有当生成新的哈希表时,才需要重新计算哈希值 if (rehash) {e.hash = null == e.key ? 0 : hash(e.key );}//读者可以在这里验证: i = oldIndex 或者 i = oldIndex + oldCapacityint i = indexFor(e.hash, newCapacity);//链表前向插值方法 www.sychzs.cn = newTable[ i];newTable[i] = e;e = 下一个;}}
}

假设有两个线程 A 和 B,两个线程都执行 Entry next = www.sychzs.cn 语句,则出现如下界面,其中 A.e -> www.sychzs.cn; B.e-> www.sychzs.cn

至此,put()方法就基本解释完了。总结一下:

  • put(key, value)
  • int hash = hash(key);
  • int index = hashcode & (length - 1)
  • 在索引位置遍历链表。如果存在相同的 key,则继续 value 覆盖并返回之前的值
  • 当达到数组的临界值时,需要对数组进行扩容
  • 将 key 和 value 封装成节点对象(Entry)
  • 插入将索引位置
  • 的节点放入链表中 移动链表头节点到数组

get(对象键)

public V get(Object key) {//获取key=null的值,可能为空 if (key == null)return getForNullKey();Entryentry = getEntry(key);return null = = 条目? null :entry.getValue();
}

getEntry(对象键)

final Entry getEntry(Object key) {if (size == 0) {return null;}//计算哈希值 int hash = (key == null) ? 0 : hash(key);//遍历指定数组下标的链表,找到满足条件的Entryfor (Entry e = table[indexFor(hash, table.length)];e != null; e = www.sychzs.cn) {Object k;// key 可以是同一个对象,也可以是不同的对象,只要 equals 比较成立 if (e.hash == hash &&((k = e.key) = = key || (key != null && key.equals(k ))))return e;}return null;
}

删除(对象键)

public V remove(Object key) {Entry e = removeEntryForKey(key);return (e == null ? null : e.value);
}

removeEntryForKey(对象键)

最终条目removeEntryForKey(Object key) {if (size == 0) {return null;}int hash = (key == null) ? 0 : hash(key);int i = indexFor(hash, table.length);Entry prev = table[i];Entry e = prev;// 这里是链表删除最基本的遍历while (e != null) {Entry next = www.sychzs.cn;Object k;if (e.hash == hash &&((k = e.key) == key || (key != null && key) .equals(k)))) {//删除原来的table数组发生了变化,所以modCount需要改变 modCount++;size--;if (prev == e)//当数据是链接的头节点时list,直接跳过头节点 table[i] = next;else//当数据不是链表头节点时,前一个节点的下一个节点需要指向被删除节点之后的节点 www.sychzs.cn = 下一个; // HashMap中并没有真正实现,所以不需要考虑e.recordRemoval(this); return e;} prev = e;e = next;}//返回被删除的节点,e可能为nullreturn e;
}

从get()和remove()方法可以得出,它们的实现非常相似,所以博主不再解释类似这些函数的方法。

揭示ConcurrentModificationException异常

相信很多程序员在多线程使用HashMap时都会遇到这个异常。这种异常通常发生在HashMap在遍历keySet、values、entrySet的时候进行增删操作的时候。那么原理是什么呢? ?

//抽象类,用于继承private abstract class HashIterator Implements Iterator {//下一个返回的数据Entry next; int 预期ModCount;整数索引; Entry current;HashIterator() {// 这是在初始化迭代器时确定的 modCountexpectedModCount = modCount;if (size > 0) {Entry[] t = table;// 查找数组中第一个非空元素并将其分配给 nextwhile (index < t.length && (next = t[index++]) == null);}}public final boolean hasNext() {return next != null;}final Entry nextEntry() {/ /确认生成的迭代器是否与当前修改状态一致//如果外部类调用了put()、remove()等方法,则modCount会改变 if (modCount !=expectedModCount) throw new ConcurrentModificationException();Entry  e = next;if (e == null)throw new NoSuchElementException();//哈希桶中,有效元素的下标不一定是连续的,所以还需要找到下一个有效元素 if (( next = www.sychzs.cn) == null) {Entry[] t = table;while (index < t.length && (next = t[index++]) == null);}current = e;return e;}public void删除(){如果(当前==空)抛出新的IllegalStateException();如果(modCount!=预期的ModCount)抛出新的ConcurrentModificationException();对象k =current.key;current = null;HashMap.this.removeEntryForKey(k);//iterator中的remove()方法会更新修改状态,不会产生修改异常//只有理解了这一点,我们才能写出不修改的代码导致异常预期ModCount = modCount;}}

上面代码的分析足以说明异常的原因。下面我将列出一些关于keySet()、values()、entrySet()的源代码。由于代码层次和语法都比较简单,博主将这只是一个列表,而不是解释;如果有任何疑问,可以在下方评论区提问,博文会一一解答。

私有最终类 ValueIterator 扩展了 HashIterator {public V next() {return nextEntry().value;}
}私有最终类 KeyIterator 扩展了 HashIterator {public K next() {return nextEntry().getKey();}
}私有最终类 EntryIterator 扩展了 HashIterator> {public Map.Entry next() {return nextEntry();}
}迭代器 newKeyIterator() {return new KeyIterator();
}
迭代器 newValueIterator() {return new ValueIterator();
}
Iterator> newEntryIterator() {return new EntryIterator();
}

keySet()

public Set keySet() {// 这里的keySet和values一样,是AbstractMap的成员抽象类 //瞬态易失Set keySet = null;Set ks = keySet ;// 延迟加载 return (ks != null ? ks : (keySet = new KeySet()));
}private Final class KeySet extends AbstractSet {public Iterator iterator() {return newKeyIterator();}public int size() {返回 size;}public boolean contains(Object o) {return containsKey(o); } }public boolean remove(Object o) {return HashMap.this.removeEntryForKey(o) != null;}public void clear() {HashMap.this.clear();}
}

值()

public Collectionvalues(){//这里的keySet和values都是AbstractMap的成员抽象类 //瞬态易失SetkeySet = null;Collection vs = value ;// 延迟加载 return (vs != null ? vs : (values = new Values()));}private final class Values extends AbstractCollection {public Iterator iterator() {return newValueIterator();}public int size() {return size;}public boolean contains(Object o) {return containsValue(o); } }public void clear() {HashMap.this.clear();}
}

entrySet()

//这是HashMap自有的属性,因为是特有的
私有瞬态Set>entrySet = null;publicSet>entrySet(){returnentrySet0();
}private Set> entrySet0() {Set> es = entrySet;return es != null ? es : (entrySet = new EntrySet());}private Final class EntrySet extends AbstractSet> {public Iterator> iterator() {return newEntryIterator();}public boolean contains(Object o) {if (!(o instanceof) Map.Entry))return false;Map.Entry e = (Map.Entry) o;Entrycandidate = getEntry(e.getKey());返回candidate != null && Candidate.equals (e);}公共布尔删除(对象o){返回removeMapping(o)!= null;}公共int size(){返回大小;}公共无效清除(){HashMap.this.clear();}
}

总结

本博文仅讲解HashMap1.7源码的重要部分。当然,还有很多细节没有解释清楚;由于空间原因,只能这样做;如果有机会,博主会提供其他细节。部分组织成单独的博客文章。博主认为,在阅读源码时,不仅要掌握源码的整体框架,还要了解jdk作者对很多细节的处理。你不仅要理解它,而且还必须遵循它。

相关文章

最新资讯