文章目录

  • 概述
  • HashMap结构图
  • 构造函数
  • 重点方法源码解读 (1.7)
    • put()
    • get()
    • remove()
  • 1.8版本 HashMap
    • put
    • resize() 扩容
    • get
  • HashSet


概述

  • HashMap实现了Map接口,即允许放入key为null的元素,也允许插入value为null的元素

  • 跟TreeMap不同,HashMap容器不保证元素顺序,根据需要该容器可能会对元素重新哈希,元素的顺序也会被重新打散,因此不同时间迭代同一个HashMap的顺序可能会不同。

  • 根据对冲突的处理方式不同,哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。Java7 HashMap采用的是冲突链表方式。

  • HashSet仅仅是对HashMap做了一层包装,也就是说HashSet里面有一个HashMap(适配器模式)

我们这里将重点分析HashMap。


HashMap结构图

从上图容易看出

  1. 如果选择合适的哈希函数,put()和get()方法可以在常数时间内完成
  2. 对HashMap进行迭代时,需要遍历整个table以及后面跟的冲突链表。因此对于迭代比较频繁的场景,不宜将HashMap的初始大小设的过大

有两个参数可以影响HashMap的性能: 初始容量(inital capacity)和负载系数(load factor)

初始容量指定了初始table的大小,负载系数用来指定自动扩容的临界值。当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。

对象放入到HashMap或HashSet中时,有两个方法需要特别关心: hashCode()和equals()

hashCode()方法决定了对象会被放到哪个bucket里,当多个对象的哈希值冲突时,equals()方法决定了这些对象是否是“同一个对象”。所以,如果要将自定义的对象放入到HashMap或HashSet中,需要重写hashCode()和equals()方法


构造函数

  /*** Constructs an empty <tt>HashMap</tt> with the specified initial* capacity and load factor.** @param  initialCapacity the initial capacity* @param  loadFactor      the load factor* @throws IllegalArgumentException if the initial capacity is negative*         or the load factor is nonpositive*/public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 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);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}/*** Constructs an empty <tt>HashMap</tt> with the specified initial* capacity and the default load factor (0.75).** @param  initialCapacity the initial capacity.* @throws IllegalArgumentException if the initial capacity is negative.*/public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}/*** Constructs an empty <tt>HashMap</tt> with the default initial capacity* (16) and the default load factor (0.75).*/public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}/*** Constructs a new <tt>HashMap</tt> with the same mappings as the* specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with* default load factor (0.75) and an initial capacity sufficient to* hold the mappings in the specified <tt>Map</tt>.** @param   m the map whose mappings are to be placed in this map* @throws  NullPointerException if the specified map is null*/public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}


重点方法源码解读 (1.7)

put()

put(K key, V value)方法是将指定的key, value对添加到map里。

 //定义一个空的Entry数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;public V put(K key, V value) {//如果数组为空则调用inflateTable()方法if (table == EMPTY_TABLE) {inflateTable(threshold);}//如果传入的key是null,调用putForNullKey()方法,把值传入if (key == null)return putForNullKey(value);//如果key不是null,调用hash()得到哈希值int hash = hash(key);//调用indexFor()方法,传入哈希值,与数组的长度,得到该值在数组中的索引位置int i = indexFor(hash, table.length);//对数组进行遍历,得到每一个entryfor (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;//如果entry的哈希值并且键或者值一致,则将原来的值取出来,把当前值赋值进去,调用recordAccess()方法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 () 添加一个entry 参数是哈希值,键,值,和索引addEntry(hash, key, value, i);return null;}//空数组调用私有方法tosizeprivate void inflateTable(int toSize) {// Find a power of 2 >= toSize//得到容量int capacity = roundUpToPowerOf2(toSize);//数组临界值 在数组容量乘负载因子与最大容量+1相比取最小threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//初始化entry数组大小table = new Entry[capacity];//初始化容量大小initHashSeedAsNeeded(capacity);}void addEntry(int hash, K key, V value, int bucketIndex) {//如果size大于临界值并且添加entry计算出来的数组索引的值不为nullif ((size >= threshold) && (null != table[bucketIndex])) {//调用扩容方法,参数是2*数组长度resize(2 * table.length);//key不等于null ,计算出key的哈希值hash = (null != key) ? hash(key) : 0;//得到在数组中位置bucketIndex = indexFor(hash, table.length);}//创建一个entrycreateEntry(hash, key, value, bucketIndex);}void createEntry(int hash, K key, V value, int bucketIndex) {//在冲突链表头部插入新的entry ----- 头插法 Entry<K,V> e = table[bucketIndex];table[bucketIndex] = new Entry<>(hash, key, value, e);size++;}
  • 该方法首先会对map做一次查找,看是否包含该元组,如果已经包含则直接返回

  • 如果没有找到,则会通过addEntry(int hash, K key, V value, int bucketIndex)方法插入新的entry,插入方式为头插法


get()

get(Object key)方法根据指定的key值返回对应的value,该方法调用了getEntry(Object key)得到相应的entry,然后返回entry.getValue()

因此getEntry()是算法的核心。 算法思想是首先通过hash()函数得到对应bucket的下标,然后依次遍历冲突链表,通过key.equals(k)方法来判断是否是要找的那个entry。

上图中hash(k)&(table.length-1)等价于hash(k)%table.length,原因是HashMap要求table.length必须是2的指数,因此table.length-1就是二进制低位全是1,跟hash(k)相与会将哈希值的高位全抹掉,剩下的就是余数了。

//getEntry()方法
final Entry<K,V> getEntry(Object key) {......int hash = (key == null) ? 0 : hash(key);for (Entry<K,V> e = table[hash&(table.length-1)];//得到冲突链表e != null; e = e.next) {//依次遍历冲突链表中的每个entryObject k;//依据equals()方法判断是否相等if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;}return null;
}

remove()

remove(Object key)的作用是删除key值对应的entry,该方法的具体逻辑是在removeEntryForKey(Object key)里实现的。removeEntryForKey()方法会首先找到key值对应的entry,然后删除该entry(修改链表的相应引用)。查找过程跟getEntry()过程类似。

//removeEntryForKey()
final Entry<K,V> removeEntryForKey(Object key) {......int hash = (key == null) ? 0 : hash(key);int i = indexFor(hash, table.length);//hash&(table.length-1)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)))) {//找到要删除的entrymodCount++; size--;if (prev == e) table[i] = next;//删除的是冲突链表的第一个entryelse prev.next = next;return e;}prev = e; e = next;}return e;
}

1.8版本 HashMap

Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。

根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)

为了降低这部分的开销,在 Java8 中,当链表中的元素达到了 8 个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)


注意,上图是示意图,主要是描述结构,不会达到这个状态的,因为这么多数据的时候早就扩容了。

Java7 中使用 Entry 来代表每个 HashMap 中的数据节点Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode

我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的

put

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}// 第四个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
// 第五个参数 evict 我们这里不关心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度// 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {// 数组该位置有数据Node<K,V> e; K k;// 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// 如果该节点是代表红黑树的节点,调用红黑树的插值方法,本文不展开说红黑树else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {// 到这里,说明数组该位置上是一个链表for (int binCount = 0; ; ++binCount) {// 插入到链表的最后面(Java7 是插入到链表的最前面)if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 8 个// 会触发下面的 treeifyBin,也就是将链表转换为红黑树if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 如果在该链表中找到了"相等"的 key(== 或 equals)if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))// 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 nodebreak;p = e;}}// e!=null 说明存在旧值的key与要插入的key"相等"// 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值if (e != null) {V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;// 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}

和 Java7 稍微有点不一样的地方就是,Java7 是先扩容后插入新值的,Java8 先插值再扩容


resize() 扩容

resize() 方法用于初始化数组或数组扩容,每次扩容后,容量为原来的 2 倍,并进行数据迁移

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) { // 对应数组扩容if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 将数组大小扩大一倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)// 将阈值扩大一倍newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候newCap = oldThr;else {// 对应使用 new HashMap() 初始化后,第一次 put 的时候newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;// 用新的数组大小初始化新的数组Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab; // 如果是初始化数组,到这里就结束了,返回 newTab 即可if (oldTab != null) {// 开始遍历原数组,进行数据迁移。for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;// 如果该数组位置上只有单个元素,那就简单了,简单迁移这个元素就可以了if (e.next == null)newTab[e.hash & (newCap - 1)] = e;// 如果是红黑树,具体我们就不展开了else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // 这块是处理链表的情况,// 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序// loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表,代码还是比较简单的Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;// 第一条链表newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;// 第二条链表的新的位置是 j + oldCap,这个很好理解newTab[j + oldCap] = hiHead;}}}}}return newTab;
}


get

  1. 计算 key 的 hash 值,根据 hash 值找到对应数组下标: hash & (length-1)
  2. 判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步
  3. 判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步
  4. 遍历链表,直到找到相等(==或equals)的 key
public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 判断第一个节点是不是就是需要的if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;if ((e = first.next) != null) {// 判断是否是红黑树if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);// 链表遍历do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;
}


HashSet

HashSet是对HashMap的简单包装,对HashSet的函数调用都会转换成合适的HashMap方法,因此HashSet的实现非常简单

//HashSet是对HashMap的简单包装
public class HashSet<E>
{......private transient HashMap<E,Object> map;//HashSet里面有一个HashMap// Dummy value to associate with an Object in the backing Mapprivate static final Object PRESENT = new Object();public HashSet() {map = new HashMap<>();}......public boolean add(E e) {//简单的方法转换return map.put(e, PRESENT)==null;}......
}

Java Review - HashMap HashSet 源码解读相关推荐

  1. Java Review - LinkedHashMap LinkedHashSet 源码解读

    文章目录 Pre 概述 数据结构 类继承关系 构造函数 方法 get() put() remove() LinkedHashSet 使用案例 - FIFO策略缓存 Pre Java Review - ...

  2. java ee是什么_死磕 java集合之HashSet源码分析

    问题 (1)集合(Collection)和集合(Set)有什么区别? (2)HashSet怎么保证添加元素不重复? (3)HashSet是否允许null元素? (4)HashSet是有序的吗? (5) ...

  3. Java集合之HashSet源码分析

    概述 HashSet是基于HashMap来实现的, 底层采用HashMap的key来保存数据, 借此实现元素不重复, 因此HashSet的实现比较简单, 基本上的都是直接调用底层HashMap的相关方 ...

  4. Java经典面试:源码解读及如何保证线程安全

    一面 正式批(别看了都是正式批,提前批就没让我面!)一面.面试时间 08-18,19:53 - 21:08,全程1个小时15分钟.涉及内容:项目.网络.数据库.算法题 1. 自我介绍 2. 项目中的有 ...

  5. Java Review - PriorityQueue源码解读

    文章目录 Pre PriorityQueue 概述 PriorityQueue 继承关系 PriorityQueue通过用数组表示的小顶堆实现 时间复杂度 构造函数 方法 add()和offer() ...

  6. Java Review - Queue和Stack 源码解读

    文章目录 Pre 概述 Queue Deque ArrayDeque 一览 构造函数 属性 方法 addFirst() addLast() pollFirst() pollLast() peekFir ...

  7. Java Review - LinkedList源码解读

    文章目录 Pre 概述 底层数据结构-双向链表 源码解析 构造函数 方法源码分析 getFirst() getLast() remove相关方法 remove(e) remove(index) rem ...

  8. java基本集合源码解读-JDK8/11

    文章目录 前言 详尽的debugger底层查看源码配置 一.集合体系图 二.List类集合 2.1.1 ArrayList 2.1.2ArrayList底层源码分析 结论: 2.1.3 使用Array ...

  9. Java HashSet源码解析

    本解析源码来自JDK1.7,HashSet是基于HashMap实现的,方法实现大都直接调用HashMap的方法 另一篇HashMap的源码解析文章 概要 实现了Set接口,实际是靠HashMap实现的 ...

最新文章

  1. [转]WinForm下Splash(启动画面)制作
  2. java 魔法王国,游历魔法王国
  3. RTX51 tiny系统要注意的问题:(关于时间片)
  4. Java集合中的排序API分析
  5. Matrix Completion with Noise
  6. Http状态码完整说明
  7. python 列表自定义排序_自定义排序的Python列表
  8. mysql列属性auto(mysql笔记四)
  9. 20154319 《网络对抗技术》后门原理与实践
  10. Spring Boot学习总结(10)——SpringBoot打包成Docker镜像
  11. Symbian操作系统
  12. 浪涌保护器ant120_ANT120/530/1P浪涌保护器服务周到漳州
  13. JS睡眠sleep()
  14. 安装pytorch详细教程
  15. Pinterest模式的魅力何在?国内山寨有哪些?
  16. 513、Java Spring Cloud Alibaba -【Spring Cloud Alibaba Nacos】 2021.08.30
  17. Android 12 悬浮通知/横幅通知状态栏应用图标显示不全
  18. 「WinddowInsets(二)」我们能用WindowInsets做什么?
  19. java中mysql分组查询_ES对应mysql的group by分组查询javaApi,多对多关系的分组查询...
  20. EXCEL如何设置,使表格能自动调整列宽以适应文字长度

热门文章

  1. 8.0ble设备 android_【胖猴小玩闹】智能门锁与BLE设备安全Part 4:一次BLE智能手环的小玩闹...
  2. c语言文件查找函数fread,文件函数fread
  3. EditText控件的基本使用(点击Button按钮,Toast提示EditText中的内容)
  4. 【转载】Few-shot learning(少样本学习)和 Meta-learning(元学习)概述
  5. pytorch笔记 torch.clamp(截取上下限)
  6. 报错处理: syntax error near unexpected token `$‘\r‘‘ (linux中的^M)
  7. 可视化应用实战案例:metacoder-相关进化树图的绘制
  8. 怎样更好地使用快捷键?
  9. 非负矩阵分解中基于L1和L2范式的稀疏性约束
  10. java 编译器获得型号_关于编译器构造:如何找到已编译类的目标Java版本?