HashMap是常用的集合类,以Key-Value形式存储值。下面一起从代码层面理解它的实现。

构造方法

它有好几个构造方法,但几乎都是调此构造方法:

    public HashMap(int initialCapacity, float loadFactor) { // 初始容量,过载因子
if (initialCapacity < 0) // 初始容量<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;
while (capacity < initialCapacity) // 按初始容量找到最近的2的n次方值,为真实的容量。为什么?个人认为因计算下标用&元素效率较高
capacity <<= 1; this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); // 计算扩容阀值,容量 * 过载因子
table = new Entry[capacity]; // 实例化容量的数组
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init(); // HashMap构造完毕,还没有插入任何元素的回调方法
}

放入元素,put(K key, V value)

实际的逻辑在putVal方法:

    public V put(K key, V value) {
if (key == null)
return putForNullKey(value); // 存储在table[0]
int hash = hash(key); // 计算hash
int i = indexFor(hash, table.length); // 计算数组下标
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))) { // 首先判断hash值是否相等(不同hash有可能映射到同一下标),再判断引用是否相等或equal方法相等
V oldValue = e.value; // 暂存旧值
e.value = value; // 赋予新值
e.recordAccess(this); // 调用覆盖值回调方法
return oldValue; // 返回旧值
}
} modCount++; // 递增变更次数
addEntry(hash, key, value, i); // 构造Entry,添加在i下标的链表中
return null;
}

通过hash和数组长度计算数组下标,indexFor(int h, int length)

    static int indexFor(int h, int length) {
return h & (length-1); // hash和数组长度-1做与运算,得到下标
}

Value被覆盖回调方法,当put(k,v)覆盖原值时调用,recordAccess()

        /**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}

结构变更次数,modCount

此字段记录HashMap结构变更次数,如添加新元素、rehash、删除元素。此字段用于迭代器的快速失败机制。

    /**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;

添加元素,addEntry()

此方法包含数组是否扩容的判断,如需扩容,会调用扩容方法:

    /**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) { // 数组是否扩容的标志:大小是否大于阀值,并且当前下标的链表不为空
resize(2 * table.length); // 两倍扩容
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length); // 重新计算映射到扩容后数组的下标
} createEntry(hash, key, value, bucketIndex);
}

实际的创建元素,createEntry()

    void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex]; // 获取链表首元素
table[bucketIndex] = new Entry<>(hash, key, value, e); // 构建新节点,其下一节点指向链表首元素,再讲链表首元素指向新元素(从前面插入)
size++; // 递增容量
}

数组扩容,resize()

    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]; // 实例化新容量的数组
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing; // 是否重新hash
transfer(newTable, rehash); // 转移所有元素到新数组
table = newTable; // 正式使用新数组
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); // 重新计算阀值
}

转移所有元素到新数组

逐个遍历,映射到新数组的链表中:

    void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) { // 遍历数组
while(null != e) { // 遍历链表
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key); // 重新hash
}
int i = indexFor(e.hash, newCapacity); // 重新计算下标
e.next = newTable[i]; // 当前节点的下一节点指向链表首元素(在链表前插入)
newTable[i] = e; // 链表首元素指向当前节点
e = next;
}
}
}

删除元素,remove()

删除元素的入口如下,其实质调用removeEntryForKey方法:

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

真实的删除元素,removeEntryForKey()

    final Entry<K,V> removeEntryForKey(Object key) {
int hash = (key == null) ? 0 : hash(key); // 计算hash值
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;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++; // 删除元素,也属于结构变化
size--; // 容量减一
if (prev == e) // 如果当前元素是链表首元素
table[i] = next; // 链表首元素指向当前节点的下一节点
else
prev.next = next; // 当前节点的前一节点的next指向当前节点的下一节点(删除当前节点,即跳过当前节点)
e.recordRemoval(this); // 删除后的回调方法
return e;
}
prev = e;
e = next;
} return e;
}

获取元素,get()

    public V get(Object key) {
if (key == null)
return getForNullKey(); // 在table[0]的下标寻找
Entry<K,V> entry = getEntry(key); // 计算下标、遍历链表对比(与之前的put、remove方法找元素类似) return null == entry ? null : entry.getValue();
}

小疑问

计算最接近的2的n次方,roundUpToPowerOf2(int number)

这个方法是计算number最接近的2的N次方数。

其中Integer.highestOneBit()是取最高位1对应的数,如果是正数,返回的是最接近的比它小的2的N次方;如果是负数,返回的是-2147483648,即Integer的最小值。

那为什么要先减1,再求highestOneBit()?

举几个数的二进制就知道了:

00001111 = 15 -> 00011110 = 30 -> highestOneBit(30) = 16

00010000 = 16 -> 00100000 = 32 -> highestOneBit(32) = 32

所以,为了获取number最接近的2的N次方数,就先减一。

private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

计算映射到指定范围的下标,indexFor(int h, int length)

将h映射到length的范围里,效果就像求模。

return h & (length-1);

将h和length - 1和操作就可以了。

比如length为16,那么:

16 = 00010000

15 = 00001111

为什么hash数组的长度要弄成2的N次方?

要将散列值映射到一定范围内,一般来说有2种方法,一是求模,二是与2的N次方值作&运算。而现代CPU对除法、求模运算的效率不算高,所以用第二种方法会效率比较高,所以数组被设计为2的N次方。

拓展:LinkedHashMap

见此类的声明可知其继承自HashMap,而实际的存储逻辑也是由HashMap提供:

public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>

链表的维护顺序

而LinkedHashMap中维护了遍历的顺序,是通过另外的双向链表维护的,比如,链表首元素:

    /**
* The head of the doubly linked list.
*/
private transient Entry<K,V> header;

元素之间的指向:

        // These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after;

用此字段表示链表维护的顺序,true表示访问顺序,false表示插入顺序:

    private final boolean accessOrder;

放入元素

覆盖了HashMap的addEntry和createEntry方法:

    /**
* This override alters behavior of superclass put method. It causes newly
* allocated entry to get inserted at the end of the linked list and
* removes the eldest entry if appropriate.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
super.addEntry(hash, key, value, bucketIndex); // 沿用HashMap的逻辑 // Remove eldest entry if instructed
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) { // 是否删除最老元素(LRU原则)
removeEntryForKey(eldest.key); // 删除最老元素
}
} /**
* This override differs from addEntry in that it doesn't resize the
* table or remove the eldest entry.
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header); // 插入到Header节点前
size++;
}
        /**
* Inserts this entry before the specified existing entry in the list.
*/
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry; // 指定节点的后节点
before = existingEntry.before; // 指定节点的前节点
before.after = this; // 将当前节点赋予前节点的后节点赋值
after.before = this; // 将当前节点赋予后节点的前节点赋值
}

获取元素

    public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this); // 维护链表的顺序
return e.value;
}
        void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) { // 如果按访问顺序记录
lm.modCount++;
remove(); // 删除当前节点
addBefore(lm.header); // 将当前节点加入到列表头
}
}
        /**
* Removes this entry from the linked list.
*/
private void remove() {
before.after = after; // 将“当前节点的后节点”赋予“当前节点的前节点的后节点”
after.before = before; // 将“当前节点的前节点”赋予“当前节点的后节点的前节点”
}

【Java】浅谈HashMap的更多相关文章

  1. Java重点之小白解析--浅谈HashMap与HashTable

    这是一个面试经常遇到的知识点,无论什么公司这个知识点几乎是考小白必备,为什么呢?因为这玩意儿太特么常见了,常见到你写一百行代码,都能用到好几次,不问这个问哪个.so!本小白网罗天下HashMap与Ha ...

  2. JAVA容器-浅谈HashMap的实现原理

    概述 HashMap是通过数组+链表的方式实现的,由于HashMap的链表也是采用数组方式,我就修改直接利用LinkedList实现,简单模拟一下. 1.Key.Value的存取方式. 2.HashM ...

  3. 浅谈HashMap的内部实现

    权衡时空 HashMap是以键值对的方式存储数据的. 如果没有内存限制,那我直接用哈希Map的键作为数组的索引,取的时候直接按索引get就行了,可是地价那么贵,哪里有无限制的地盘呢. 如果没有时间限制 ...

  4. 【JDK源码分析】浅谈HashMap的原理

    这篇文章给出了这样的一道面试题: 在 HashMap 中存放的一系列键值对,其中键为某个我们自定义的类型.放入 HashMap 后,我们在外部把某一个 key 的属性进行更改,然后我们再用这个 key ...

  5. 浅谈HashMap的实现原理

    1.    HashMap概述: HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变 ...

  6. 浅谈HashMap原理,记录entrySet中的一些疑问

    HashMap的底层的一些变量: transient Node<K,V>[] table; //存储数据的Node数组 transient Set<java.util.Map.Ent ...

  7. 浅谈HashMap与线程安全 (JDK1.8)

    HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型.HashMap 继承自 AbstractMap 是基于哈希表的 Map 接口的实现,以 Key-Value 的形式存在,即 ...

  8. java 浅谈web系统当中的cookie和session会话机制

    一 Cookie: 1. Cookie翻译为小甜饼,有一种特殊的味道.cookie主要用来在(浏览器)客户端做记号用的.Cookie不属于java,Cookie是一种通用的机制,属于HTTP协议的一部 ...

  9. [Android&amp;Java]浅谈设计模式-代码篇:观察者模式Observer

    观察者,就如同一个人,对非常多东西都感兴趣,就好像音乐.电子产品.Game.股票等,这些东西的变化都能引起爱好者们的注意并时刻关注他们.在代码中.我们也有这种一种方式来设计一些好玩的思想来.今天就写个 ...

随机推荐

  1. POJ 2250 Compromise【LCS】+输出路径

    题目链接:https://vjudge.net/problem/POJ-2250 题目大意:给出n组case,每组case由两部分组成,分别包含若干个单词,都以“#”当结束标志,要求输出最长子序列. ...

  2. vuex数据持久化存储

    想想好还是说下vuex数据的持久化存储吧.依稀还记得在做第一个vue项目时,由于刚刚使用vue,对vue的一些基本概念只是有一个简单的了解.当涉及到非父子组件之间通信时,选择了vuex.只是后来竟然发 ...

  3. Python3函数式编程

    Python函数式编程 函数式编程可以使代码更加简洁,易于理解.Python提供的常见函数式编程方法如下: map(函数,可迭代式)映射函数 filter(函数,可迭代式)过滤函数 reduce(函数 ...

  4. [BZOJ3674]可持久化并查集加强版&[BZOJ3673]可持久化并查集 by zky

    思路: 用主席树维护并查集森林,每次连接时新增结点. 似乎并不需要启发式合并,我随随便便写了一个就跑到了3674第一页?3673是这题的弱化版,本来写个暴力就能过,现在借用加强版的代码(去掉异或),直 ...

  5. git 添加远程仓库

    你已经在本地创建了一个Git仓库后,又想在GitHub创建一个Git仓库,并且让这两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得. 首先, ...

  6. db2执行计划介绍

    在数据库调优过程中,SQL语句往往是导致性能问题的主要原因,而执行计划则是解释SQL语句执行过程的语言,只有充分读懂执行计划才能在数据库性能优化中做到游刃有余. 常见的关系型数据库中,虽然执行计划的表 ...

  7. windows 64位环境下php执行环境部署配置

    1.下载安装包 地址可以网上找,我下载的是php-5.6.27-Win32-VC11-x64.zip 2.解压安装包,我的解压到D:\tools\php5.6 3.配置php.ini 在解压的目录中, ...

  8. mybatis学习之路----mysql批量新增数据

    原文:https://blog.csdn.net/xu1916659422/article/details/77971867 接下来两节要探讨的是批量插入和批量更新,因为这两种操作在企业中也经常用到. ...

  9. 版本视图找不到数据 EDITIONING VIEW

    Oracle database 12 以后的版本,特别在EBS R12.2.X加入了版本视图这种技术,跟MOAC有点像. CREATE OR REPLACE FORCE EDITIONING VIEW ...

  10. 【面试 SQL】【第十六篇】SQL相关面试

    =================================================================================== 1.一张表,姓名,科目,成绩,一 ...