2.1 HashMap
2.1.1 HashMap介绍
先看看HashMap类头部的源码:
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
此实现假定哈希函数将元素适当地分布在各个桶(数组元素)之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将负载因子设置得太低)。
HashMap 的实例有两个参数影响其性能:初始容量和负载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数HashMap 类的操作中,包括get 和put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生rehash 操作。
如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
注意,此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:
Map m = Collections.synchronizedMap(new HashMap(...));
由所有此类的“collection 视图方法”所返回的迭代器都是快速失败的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不会在将来不确定的时间发生任意不确定行为的风险。
注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
2.1.2 HashMap存储结构图
这里先给出HashMap的存储结构,在后面的源码分析中,我们将更加详细的对此作介绍。HashMap采取数组加链表的存储方式来实现。亦即数组(散列桶)中的每一个元素都是链表,如下图:
图2-1
说明:下面针对HashMap的源码分析中,所有提到的桶或散列桶都表示存储结构中数组的元素,桶或散列桶的数量亦即表示数组的长度,哈希码亦即散列码。
2.1.3 属性分析
先来看看HashMap有哪些属性,HashMap没有从AbstractMap父亲中继承任何属性,下面这些都是HashMap的属性:
static final int DEFAULT_INITIAL_CAPACITY = 16;
DEFAULT_INITIAL_CAPACITY是HashMap默认的初始化桶数量,如图2-1中所示。对于HashMap中桶数量的值必须是2的N次幂,而且这个是HashMap强制规定的。这样做的原因就是因为计算机进行2次幂的运算是非常高效的,仅通过位移操作就可以完成2的N次幂的运算。
static final int MAXIMUM_CAPACITY = 1 << 30;
MAXIMUM_CAPACITY是HashMap中散列桶数量的最大值,从上面的代码可知这个最大值为2的32次幂,即1073741824。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认的负载因子,如果在在创建HashMap的构造函数中没有指定负载因子,则指定该HashMap的默认负载因子为0.75,这意味着当HashMap中条目的数量达到了条目数量75%时,HashMap将进行resize操作以增加桶的数量。对于桶的扩展,等分析到下面的具体时会作更详细的介绍。
transient Entry<K,V>[] table;
table就是HashMap的存储结构,显然这是一个数组,数组的每一个元素都是一个条目(Entry),Entry是HashMap中的一个内部类,它有如下4个属性:final K key;V value;Entry<K,V> next;int hash。分别为键、值、指向下一个链表结点的指针、散列(哈希)值。这就是图2.1中HashMap存储结构的代码实现。
transient int size;
size表示HashMap中条目(即键-值对)的数量。
int threshold;
threshold是HashMap的重构阈值,它的值为容量和负载因子的乘积。在HashMap中所有桶中条目的总数量达到了这个重构阈值之后,HashMap将进行resize操作以自动扩容。
final float loadFactor;
loadFactor表示HashMap的负载因子,它和容量一样都是HashMap扩容的决定性因素。
transient int modCount;
modCount表示HashMap被结构化更新的次数,比如插入、删除、清空等会更新HashMap结构的操作次数。
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT
= Integer.MAX_VALUE;
ALTERNATIVE_HASHING_THRESHOLD_DEFAULT表示在对字符串键(即key为String类型)的HashMap应用备选哈希函数时HashMap的条目数量的默认阈值。备选哈希函数的使用可以减少由于对字符串键进行弱哈希码计算时的碰撞概率。
transient boolean useAltHashing;
useAltHashing表示是否要对字符串键的HashMap使用备选哈希函数。
transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);
hashSeed表示一个与当前实例关联并且可以减少哈希碰撞概率应用于键的哈希码计算的随机种子。
2.1.4 构造分析
HashMap提供了4个构造方法,按照它们在源码中的位置顺序从上至下列出:
HashMap(int initialCapacity, float loadFactor)
HashMap(int initialCapacity)
HashMap()
HashMap(Map<? extends K, ? extends V> m)
(1) 我们先来分析第一个同时传递初始化容量参数和负载因子参数的源码,因为其它的3个构造方法都会调用这个构造方法,下面给出这个方法的代码及分析:
public HashMap(int initialCapacity, float loadFactor) {
//部分构造参数容错处理的源码已省略...
/**
* 根据传入的初始化容量计算该HashMap的容量(即桶的数量)
* 算法为:将capacity进行不断的左移,直至capacity大于或等于初始化容量
*/
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
//负载因子初始化
this.loadFactor = loadFactor;
/**
* 条目阈值的计算
* 算法:超出条目最大容量前取容量与负载因子的乘积作为条目阈值
*/
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建数组(散列桶)
table = new Entry[capacity];
//计算是否对字符串键的HashMap使用备选哈希函数
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();//调用初始化方法,默认情况下什么也没做
}
(2) 下面是只传初始化容量参数的构造方法:
public HashMap(int initialCapacity) {
//初始化容量传入,加载因子为默认值0.75f
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
(3) 下面是无参构造方法:
public HashMap() {
//初始化容量为默认值16,加载因子也为默认值0.75f
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
(4) 下面是根据已有Map构造新HashMap的构造方法:
public HashMap(Map<? extends K, ? extends V> m) {
/**
* 取下面两个值的较大的值作为当前要构造的HashMap的初始容量
* 第1个值:用传入的Map的条目数量除以默认加载因子再加上1
* 第2个值:默认的初始化容量
*/
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
/**
* 把传入的map里的所有条目放入当前已构造的HashMap中
* 关于putAllForCreate方法后面会作分析
*/
putAllForCreate(m);
}
2.1.5 hash方法
hash方法的源码及分析如下:
final int hash(Object k) {
int h = 0;
/**
* 如果useAltHashing的值为true
* 并且键的类型为String,则对字符串键使用备选哈希函数
* 否则,返回用于对键进行哈希码计算的随机种子hashSeed
* 关于hashSeed在2.1.3.1小节中已介绍过,这里不再赘述
*/
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
/**
* 对h和键的哈希码进行抑或并赋值运算
* 等价于h = h ^ k.hashCode();
*/
h ^= k.hashCode();
//下面两步的运算过程如图2-2所示
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
假设h=0x7FFFFFFF,则上面最后两步对h的运算过程如下图:
图2-2
2.1.6 indexFor方法
/**
* h表示通过hash(Object k)方法计算得来的哈希码
* length表示桶的数量(即数组的长度)
*/
static int indexFor(int h, int length) {
/**
* 将哈希码和length进行按位与运算
* 所有的h值都会在映射在闭区间[0,length-1]内
* 不同的h值可能映射到闭区间[0,length-1]内同一个值上
*/
return h & (length-1);
}
2.1.7 put方法
/**
* 在HashMap中存储一个键值对,若指定的键已经存在于HashMap中
* 则将新的值替换掉旧值,否则新添加一个条目来存储这个键值对
* @param key 指定的键
* @param value 指定的值
* @return 若该键已经存在则返回该键对应的旧值,否则返回null
*/
public V put(K key, V value) {
if (key == null)
/**
* 若键为null,则调用putForNullKey方法进行插入
* putForNullKey的源码这里不再分析,读者有兴趣可以自行分析它的源码
*/
return putForNullKey(value);
//下面这两个方法在前面两小节中已经分析过
int hash = hash(key);//计算键对应的哈希码
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))) {
//桶中已经存在指定的键,替换指定键对应的旧值并返回该旧值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
/**
* 桶中不存在指定键,则调用addEntry方法添加向桶中添加新结点
* addEntry方法下一小节将会详细介绍
*/
addEntry(hash, key, value, i);
return null;
}
2.1.8 addEntry方法
/**
* 向HashMap的指定桶中添加一个新的键对值
* 若要对HashMap扩容(即增加桶的数量),则下面的方法可能会修改传入的桶索引
* @param hash 指定键对应的哈希码
* @param key 指定键
* @param value 指定值
* @param bucketIndex 桶索引
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//如果HashMap中条目的数量达到了重构阈值且指定的桶不为null,则对HashMap进行扩容(即增加桶的数量)
/**
* 调用resize方法对HashMap进行扩容
* 对于resize方法,下面会有专门的一小节来作介绍,这里先不介绍
*/
resize(2 * table.length);
//扩容后,桶的数量增加了,故需要重新对键进行哈希码的计算
hash = (null != key) ? hash(key) : 0;
//根据新的键哈希码和新的桶数量重新计算桶索引值
bucketIndex = indexFor(hash, table.length);
}
/**
* 在指定的桶中创建一个新的条目以存储我们传入的键值对
* 对于createEntry方法,读者若有兴趣可以自行阅读其源码
*/
createEntry(hash, key, value, bucketIndex);
}
2.1.9 resize方法
/**
* 重新调整HashMap中桶的数量
* @param newCapacity 新的桶数量
*/
void resize(int newCapacity) {
/**
* 下面的这段代码对新值进行判断
* 如果新值超过了条目(Entry)数量的最大值
* 则新int最大值赋值给重构阈值然后,然后直接返回而不会进行扩容
*/
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//若newCapacity合法,则新建一个桶数组。
Entry[] newTable = new Entry[newCapacity];
//计算是否需要对键重新进行哈希码的计算
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
/**
* 将原有所有的桶迁移至新的桶数组中
* 在迁移时,桶在桶数组中的绝对位置可能会发生变化
* 这就是为什么HashMap不能保证存储条目的顺序不能恒久不变的原因
* 读者若有兴趣,可以自行阅读transfer方法的源码
*/
transfer(newTable, rehash);
//将新的桶数组的引用赋值给旧数组
table = newTable;
//像构造方法中一样来重新计算重构阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
2.1.10 get方法
/**
* 根据指定键获取该键对应的值
* @param key 指定键
* @return 若该键存在于HashMap中,则返回该键对应的值,否则返回null
*/
public V get(Object key) {
if (key == null)
//若键为null,则返回null键对应的值
return getForNullKey();
//根据键获取条目,下一小节会单独介绍getEntry方法
Entry<K,V> entry = getEntry(key);
//返回条目的值,若条目为null,则返回null
return null == entry ? null : entry.getValue();
}
相关推荐
HashMap是Java中非常常见的一种数据结构,主要用于存储键值对,其核心原理是通过哈希算法将键映射到数组中的位置来实现快速访问。本文将详细介绍HashMap的底层原理,包括其内部实现结构、关键字段的作用、以及JDK ...
java 集合和泛型 1. Map接口 2. HashMap底层实现 3. Hash数据结构和算法 4. 红黑树数据结构和算法
在编程领域,尤其是Java开发中,理解并熟练运用数据结构是非常关键的一项技能。"Java数据结构实例"这个主题,旨在通过具体的代码实例帮助初学者掌握数据结构的基本概念和使用方式,以此来提升编程思维和问题解决能力...
HashMap的底层原理主要依赖于哈希表,这是一种数据结构,它通过计算键的哈希码来实现高效的查找操作。 在HashMap中,每个元素都是一个键值对,存储在一个Entry对象中。当向HashMap添加键值对时,首先会计算键的哈希...
HashMap是Java集合框架中的一种重要数据结构,主要用于存储键值对。它基于哈希表实现,提供O(1)的平均时间复杂度进行插入、删除和查找操作。在JDK1.8之前,HashMap的数据结构是由数组和链表组成的,而在JDK1.8之后,...
HashMap是Java编程语言中的一种重要数据结构,它是`java.util.HashMap`类的实例,实现了`Map`接口。HashMap主要用于存储键值对,其中键是唯一的,且键和值之间通过哈希函数建立关联,使得在理想情况下,查询、插入和...
在Java中,TreeSet和TreeMap使用了红黑树作为底层数据结构,提供了高效的查找、插入和删除操作。 7. **图**:由顶点和边构成,用于表示对象之间的关系。Java中没有内置的图数据结构,但可以通过自定义类或者使用...
在本实验报告中,我们将深入探讨Java编程语言中的核心数据结构。数据结构是计算机科学的基础,它涉及到如何高效地组织和存储数据,以便于访问和处理。Java版本的数据结构实验旨在帮助学生理解并掌握这些概念,并能...
《HashMap底层实现原理》 HashMap是Java编程语言中一个非常重要的数据结构,它在实际开发中被广泛应用。HashMap提供了一种高效、灵活的方式来存储和检索键值对数据,它的核心特性是快速查找,通常时间复杂度为O(1)...
在Java编程中,HashMap是一个非常常用的集合类,用于存储键值对数据。然而,它存在一个重要的特性,那就是线程不...在设计和编写多线程程序时,要始终关注数据结构的选择和操作的同步控制,以保证程序的正确性和效率。
总的来说,HashMap是Java中一个高效的数据结构,其设计考虑了空间和时间效率。JDK1.8的改进使HashMap在处理大量数据时表现得更加出色,尤其是在解决哈希冲突时引入了红黑树,显著提高了性能。理解和掌握HashMap的...
Java集合,HashMap底层实现和原理(1.7数组+链表与1.8+的数组+链表+红黑树) 在Java集合中,HashMap是一个常用的数据结构,它基于Map接口实现,元素以键值对的方式存储,并且允许使用null键和null值。由于key不允许...
Java HashMap 是一个非常重要的数据结构,它在面试中经常被问到,因为它涉及到许多底层实现细节和并发问题。以下是对给定的Java HashMap面试题的详细解析: 1. **HashMap的内部实现原理**: HashMap基于哈希表,...
HashMap hashMap = new HashMap(); hashMap的无参构造方法非常简单,内部只是默认值的初始化,加载因子 0.75f public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } ...
Java HashMap 是一种常用的数据结构,用于存储键值对儿,但是在遍历和删除元素时,需要注意一些特殊的情况,否则可能会出现异常或错误。本文将介绍 Java HashMap 遍历和删除元素的正确方法。 一、HashMap 遍历方法 ...
C语言是一种底层语言,对于内存管理有直接的控制权,因此在实现数据结构时可以更深入地理解内存布局和操作。C中的数据结构通常包括数组、链表、栈、队列、树、图等。数组是最基础的数据结构,提供了随机访问的能力;...
HashMap是Java中广泛使用的数据结构之一,它主要用于存储键值对(key-value pairs),具备快速存取的能力。本文将详细探讨HashMap的底层原理,包括其数据结构、存取实现以及与Hashtable的区别。 1. HashMap的数据...
HashMap是Java中常用的一种数据结构,它基于哈希表实现,提供快速的键值对存储和检索。HashMap的核心原理是通过散列函数将键对象转换为哈希码,然后使用这个哈希码来确定键值对在内部数组中的位置。哈希函数的设计...