【Java】浅谈HashMap
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的更多相关文章
- Java重点之小白解析--浅谈HashMap与HashTable
这是一个面试经常遇到的知识点,无论什么公司这个知识点几乎是考小白必备,为什么呢?因为这玩意儿太特么常见了,常见到你写一百行代码,都能用到好几次,不问这个问哪个.so!本小白网罗天下HashMap与Ha ...
- JAVA容器-浅谈HashMap的实现原理
概述 HashMap是通过数组+链表的方式实现的,由于HashMap的链表也是采用数组方式,我就修改直接利用LinkedList实现,简单模拟一下. 1.Key.Value的存取方式. 2.HashM ...
- 浅谈HashMap的内部实现
权衡时空 HashMap是以键值对的方式存储数据的. 如果没有内存限制,那我直接用哈希Map的键作为数组的索引,取的时候直接按索引get就行了,可是地价那么贵,哪里有无限制的地盘呢. 如果没有时间限制 ...
- 【JDK源码分析】浅谈HashMap的原理
这篇文章给出了这样的一道面试题: 在 HashMap 中存放的一系列键值对,其中键为某个我们自定义的类型.放入 HashMap 后,我们在外部把某一个 key 的属性进行更改,然后我们再用这个 key ...
- 浅谈HashMap的实现原理
1. HashMap概述: HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变 ...
- 浅谈HashMap原理,记录entrySet中的一些疑问
HashMap的底层的一些变量: transient Node<K,V>[] table; //存储数据的Node数组 transient Set<java.util.Map.Ent ...
- 浅谈HashMap与线程安全 (JDK1.8)
HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型.HashMap 继承自 AbstractMap 是基于哈希表的 Map 接口的实现,以 Key-Value 的形式存在,即 ...
- java 浅谈web系统当中的cookie和session会话机制
一 Cookie: 1. Cookie翻译为小甜饼,有一种特殊的味道.cookie主要用来在(浏览器)客户端做记号用的.Cookie不属于java,Cookie是一种通用的机制,属于HTTP协议的一部 ...
- [Android&Java]浅谈设计模式-代码篇:观察者模式Observer
观察者,就如同一个人,对非常多东西都感兴趣,就好像音乐.电子产品.Game.股票等,这些东西的变化都能引起爱好者们的注意并时刻关注他们.在代码中.我们也有这种一种方式来设计一些好玩的思想来.今天就写个 ...
随机推荐
- POJ 2250 Compromise【LCS】+输出路径
题目链接:https://vjudge.net/problem/POJ-2250 题目大意:给出n组case,每组case由两部分组成,分别包含若干个单词,都以“#”当结束标志,要求输出最长子序列. ...
- vuex数据持久化存储
想想好还是说下vuex数据的持久化存储吧.依稀还记得在做第一个vue项目时,由于刚刚使用vue,对vue的一些基本概念只是有一个简单的了解.当涉及到非父子组件之间通信时,选择了vuex.只是后来竟然发 ...
- Python3函数式编程
Python函数式编程 函数式编程可以使代码更加简洁,易于理解.Python提供的常见函数式编程方法如下: map(函数,可迭代式)映射函数 filter(函数,可迭代式)过滤函数 reduce(函数 ...
- [BZOJ3674]可持久化并查集加强版&[BZOJ3673]可持久化并查集 by zky
思路: 用主席树维护并查集森林,每次连接时新增结点. 似乎并不需要启发式合并,我随随便便写了一个就跑到了3674第一页?3673是这题的弱化版,本来写个暴力就能过,现在借用加强版的代码(去掉异或),直 ...
- git 添加远程仓库
你已经在本地创建了一个Git仓库后,又想在GitHub创建一个Git仓库,并且让这两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得. 首先, ...
- db2执行计划介绍
在数据库调优过程中,SQL语句往往是导致性能问题的主要原因,而执行计划则是解释SQL语句执行过程的语言,只有充分读懂执行计划才能在数据库性能优化中做到游刃有余. 常见的关系型数据库中,虽然执行计划的表 ...
- windows 64位环境下php执行环境部署配置
1.下载安装包 地址可以网上找,我下载的是php-5.6.27-Win32-VC11-x64.zip 2.解压安装包,我的解压到D:\tools\php5.6 3.配置php.ini 在解压的目录中, ...
- mybatis学习之路----mysql批量新增数据
原文:https://blog.csdn.net/xu1916659422/article/details/77971867 接下来两节要探讨的是批量插入和批量更新,因为这两种操作在企业中也经常用到. ...
- 版本视图找不到数据 EDITIONING VIEW
Oracle database 12 以后的版本,特别在EBS R12.2.X加入了版本视图这种技术,跟MOAC有点像. CREATE OR REPLACE FORCE EDITIONING VIEW ...
- 【面试 SQL】【第十六篇】SQL相关面试
=================================================================================== 1.一张表,姓名,科目,成绩,一 ...