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数据结构的更多相关文章

  1. HashMap数据结构与实现原理解析(干货)

    HashMap 数据结构解析: HashMap内部使用hash表(本质是一个数组见图一) HashMap使用hash算法计算得到存放的索引位置,以此来加快查询速度,(比ArrayList还要快) 同样 ...

  2. 面试题 HashMap 数据结构 实现原理

    数据结构 HashMap的数据结构 数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端. 数组:数组存储区间是连续的,占用内存严重,故空间复杂的很大.但数组的二分查找时间复杂度小,为O ...

  3. HashMap数据结构的C++实现

    Hash表在计算机的应用编程中是一种很常用的数据结构,很多算法的实现都离不开它.虽然C++11标准模板库中的有hashmap类型的实现,但在工程实践中,若项目本身使用的是较低版本的C++,或是出于性能 ...

  4. 详解HashMap数据结构实现

    HashMap的设计是由数组加链表的符合数据结构,在这里用自己的语言以及结合源码去总结一下,如果有不对的地方希望评论指正,先拱手谢谢. HashMap是日常中非常常用的一种数据结构,我们要想深入了解学 ...

  5. JDK1.8的HashMap数据结构及红黑树

    在JDK1.6,1.7中,HashMap的实现都是用基础的“拉链法”去实现,即数组+链表的形式.如下图:通过不同的hash值,来对数据进行分配存储. 关于HashMap的Entry长度,可以参考htt ...

  6. Java的HashMap数据结构

    标题太大~~~自己做点笔记.别人写得太好了. https://www.cnblogs.com/liwei2222/p/8013367.html HashMap 1.6时代, 使用Entry[]数组, ...

  7. 转发 java数据结构之hashMap详解

    概要 这一章,我们对HashMap进行学习.我们先对HashMap有个整体认识,然后再学习它的源码,最后再通过实例来学会使用HashMap.内容包括:第1部分 HashMap介绍第2部分 HashMa ...

  8. Java中常见数据结构Map之HashMap

    之前很早就在博客中写过HashMap的一些东西: 彻底搞懂HashMap,HashTableConcurrentHashMap关联: http://www.cnblogs.com/wang-meng/ ...

  9. 【集合框架】JDK1.8源码分析之HashMap(一)

    一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...

随机推荐

  1. error-Java-web:20190618

    ylbtech-error-Java-web:20190618 1.返回顶部 1. org.springframework.beans.factory.UnsatisfiedDependencyExc ...

  2. SpringBoot通过maven打包成jar,设定主清单属性。

    文章目录 原来普通的jar包一直是 <build> <plugins> <plugin> <groupId>org.apache.maven.plugi ...

  3. shell脚本实现取当前时间

    shell 实现获取当前时间,并进行格式转换的方法: 1)原格式输出 2018年 09月 30日 星期日 15:55:15 CST time1=$(date) echo $time1 2)时间串输出 ...

  4. 03root密码设置

  5. Spring MVC @PathVariable注解(3)

    下面用代码来演示@PathVariable传参方式 1 @RequestMapping("/user/{id}") 2 public String test(@PathVariab ...

  6. Docker学习のDocker镜像

    一.列出镜像 命令:docker images [optsions] [repositort] -a 标识列出所有 -f  写过滤条件 --no-trunc  不截断id -q 只显示唯一id rep ...

  7. java 迷你DVD管理器

    1.DvdSet类 package dvd_01; /** * 定义dvd的一些属性 * @author Administrator * */ public class DvdSet { String ...

  8. override new 的区别

    override : 方法提供从基类继承的成员的新实现. 通过 override 声明重写的方法称为重写基方法. 重写基方法必须具有与 override方法相同的签名 new : 关键字可以显式隐藏从 ...

  9. c# 给文件/文件夹 管理用户权限

    public class PermissionManager { /// <summary> /// 为文件添加users,everyone用户组的完全控制权限 /// </summ ...

  10. 线性dp,后缀处理——cf1016C好题

    绝对是好题 #include<bits/stdc++.h> using namespace std; #define maxn 300005 #define ll long long ll ...