一、HashMap介绍

简要:HashMap是什么?

基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap类与Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。 --摘自百度百科

二、先行连接HashMap底层基础

首先要明白在JDK1.7时HashMap的底层是由数组+链表实现的,到了JDK1.8后改成了数组+链表+红黑树实现,接下来我将对这几种数据结构详细讲解,并且

1.数组

特点:

  1. 数组是相同数据类型的元素的集合。
  2. 数组中的各元素的存储是有先后顺序的,它们在内存中按照这个先后顺序连续存放在一起。
  3. 数组元素用整个数组的名字和它自己在数组中的顺序位置来表示。例如,a[0]表示名字为a的数组中的第一个元素,a[1]代表数组a的第二个元素,以此类推。
  4. 下标可以是常量,变量,或表达式,但其值必须是整数(如果是小数将四舍五入为整数)。
  5. 下标必须为一段连续的整数,其最小值成为下界,其最大值成为上界。不加说明时下界值默认为1。

2.单向链表

单向链表是链表的一种,它由节点组成,每个节点都包含下一个节点的指针。

特点:

  1. 新增删除节点速方便、速度快,不需要像线性结构那样移动剩下的数据
  2. 查询较数组慢,需要通过循环或者递归的方法访问到任意数据,平均的访 问效率低于线性表
  3. 只能从头到尾遍历。只能找到后继,无法找到前驱,也就是只能前进。

适用于节点的增加删除。

3.双向链表

双向链表是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。

特点:

  1. 有两个指针,一个指向前一个节点,一个指向后一个节点
  2. 可以找到前驱和后继,可进可退
  3. 增加删除节点复杂,需要多分配一个指针存储空间

4. 红黑树

红黑树是一种特定类型的二叉树,也是一种平衡二叉查找树的变体,它的左右子树高差有可能大于 1,所以红黑树不是严格意义上的平衡二叉树,由于每一棵红黑树都是一颗二叉排序树,因此,在对红黑树进行查找时,可以采用运用于普通二叉排序树上的查找算法。

特点:

  1. 每个节点只能是红色或者黑色。
  2. 根节点必须是黑色。
  3. 红色的节点,它的叶节点只能是黑色。
  4. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树

三、HashMap源码详解

HashMap的存储结构

  1. put操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//1. 如果当前table为空,新建默认大小的tableif ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//2. 获取当前key对应的节点if ((p = tab[i = (n - 1) & hash]) == null)//3. 如果不存在,新建节点tab[i] = newNode(hash, key, value, null);else {//4. 存在节点Node<K,V> e; K k;//5. key的hash相同,key的引用相同或者key equals,则覆盖if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//6. 如果当前节点是一个红黑树树节点,则添加树节点else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//7. 不是红黑树节点,也不是相同节点,则表示为链表结构else {for (int binCount = 0; ; ++binCount) {//8. 找到最后那个节点if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);//9. 如果链表长度超过8转成红黑树if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}//10.如果链表中有相同的节点,则覆盖if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;//是否替换掉value值if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}//记录修改次数++modCount;//是否超过容量,超过需要扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;}
  1. 计算关于key的hashcode值
  2. 如果散列表为空时,调用resize()初始化散列表
  3. 如果没有发生碰撞,直接添加元素到散列表中去
  4. 如果发生了碰撞(hashCode值相同),进行三种判断
    1. 若key地址相同或者equals后内容相同,则替换旧值
    2. 如果是红黑树结构,就调用树的插入方法
    3. 链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。
  5. 如果桶满了大于阀值,则resize进行扩容

2. GET操作

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)//若定位到的节点是 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;}final TreeNode<K,V> getTreeNode(int h, Object k) {return ((parent != null) ? root() : this).find(h, k, null);}
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {TreeNode<K,V> p = this;do {int ph, dir; K pk;TreeNode<K,V> pl = p.left, pr = p.right, q;//首先进行hash 值的比较,若不同令当前节点变为它的左孩子或者右孩子if ((ph = p.hash) > h)p = pl;else if (ph < h)p = pr;//hash 值相同,进行 key值的比较 else if ((pk = p.key) == k || (k != null && k.equals(pk)))return p;else if (pl == null)p = pr;else if (pr == null)p = pl;/hash 值相同,key 值不同 ,若k 是可比较的并且k.compareTo(pk) 返回结果不    为0可进入下面else if   else if ((kc != null ||(kc = comparableClassFor(k)) != null) &&(dir = compareComparables(kc, k, pk)) != 0)p = (dir < 0) ? pl : pr;若 k 是不可比较的 或者 k.compareTo(pk) 返回结果为0则在整棵树中进行查找,先找右子树,右子树没有再找左子树else if ((q = pr.find(h, k, kc)) != null)return q;elsep = pl;} while (p != null);return null;}
  1. 对key的hashCode进行hashing
  2. 与运算计算下标获取bucket位置,如果在桶的首位上就可以找到就直接返回,否则在树中找或者链表中遍历找
  3. 如果有hash冲突,则利用equals方法去遍历链表查找节点

3. RESIZE操作

扩容的时候,HashMap是把长度扩为原来2倍,所以,元素的位置要么是在原位置,要么是在原位置再移动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;/*1、resize()函数在size > threshold时被调用。oldCap大于 0 代表原来的 table 表非空, oldCap 为原表的大小,oldThr(threshold) 为 oldCap × load_factor*/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}/*2、resize()函数在table为空被调用。oldCap 小于等于 0 且 oldThr 大于0,代表用户创建了一个 HashMap,但是使用的构造函数为HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity)或 HashMap(Map<? extends K, ? extends V> m),导致 oldTab 为 null,oldCap 为0,oldThr 为用户指定的 HashMap的初始容量。*/else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;/*3、resize()函数在table为空被调用。oldCap 小于等于 0 且 oldThr 等于0,用户调用 HashMap()构造函数创建的 HashMap,所有值均采用默认值,oldTab(Table)表为空,oldCap为0,oldThr等于0,*/else {               // zero initial threshold signifies using defaultsnewCap = 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;if (oldTab != null) {//把 oldTab 中的节点 reHash 到 newTab 中去for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;//若节点是单个节点,直接在 newTab 中进行重定位if (e.next == null)newTab[e.hash & (newCap - 1)] = e;//若节点是 TreeNode 节点,要进行 红黑树的 rehash 操作else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//若是链表,进行链表的 rehash 操作else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;//根据算法 e.hash & oldCap 判断节点位置 rehash 后是否发生改变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;// rehash 后节点新的位置一定为原来基础上加上 oldCapnewTab[j + oldCap] = hiHead;}}}}}return newTab;}
//这个函数的功能是对红黑树进行 rehash 操作final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {TreeNode<K,V> b = this;// Relink into lo and hi lists, preserving orderTreeNode<K,V> loHead = null, loTail = null;TreeNode<K,V> hiHead = null, hiTail = null;int lc = 0, hc = 0;//由于TreeNode 节点之间存在双端链表的关系,可以利用链表关系进行 rehashfor (TreeNode<K,V> e = b, next; e != null; e = next) {next = (TreeNode<K,V>)e.next;e.next = null;if ((e.hash & bit) == 0) {if ((e.prev = loTail) == null)loHead = e;elseloTail.next = e;loTail = e;++lc;}else {if ((e.prev = hiTail) == null)hiHead = e;elsehiTail.next = e;hiTail = e;++hc;}}//rehash 操作之后注意对根据链表长度进行 untreeify 或 treeify 操作if (loHead != null) {if (lc <= UNTREEIFY_THRESHOLD)tab[index] = loHead.untreeify(map);else {tab[index] = loHead;if (hiHead != null) // (else is already treeified)loHead.treeify(tab);}}if (hiHead != null) {if (hc <= UNTREEIFY_THRESHOLD)tab[index + bit] = hiHead.untreeify(map);else {tab[index + bit] = hiHead;if (loHead != null)hiHead.treeify(tab);}}//end if}//end split

四、HashMap简单手写实现

HyhMap接口

/*** 手写实现Map接口** @Author heyuhua* @create 2021/2/9 15:29*/
public interface HyhMap<K, V> {/*** PUT接口** @param k* @param v*/void put(K k, V v);/*** GET接口** @param k* @return*/V get(K k);/*** 获取map大小接口** @return*/int size();/*** Entry 接口** @param <K>* @param <V>*/interface Entry<K, V> {/*** 获取KEY值** @return*/K getKey();/*** 获取Value值** @return*/V getValue();}}

HyhHashMap实现类

import java.io.Serializable;/*** 手写实现HashMap** @Author heyuhua* @create 2021/2/9 15:31*/
public class HyhHashMap<K, V> implements HyhMap<K, V>, Serializable {/*** 默认容量*/static final int DEFAULT_CAPACITY = 16;int threshold;/*** 当前key索引位置*/int keyIndex;/*** 负载因子*/static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** 保存Node<K,V>节点的数组*/Node<K, V>[] table;/*** 存储当前Map容量的大小*/int size;@Overridepublic void put(K key, V value) {Node<K, V> node;if (table == null) {table = resize();//table里面为空的情况node = new Node<K, V>(hash(key), key, value, null);table[keyIndex] = node;size++;} else {table = resize();//table不为空时Node<K, V> n;//是否hash冲突boolean hashConflict = false;for (int i = 0; i < table.length; i++) {n = table[i];if (n != null) {if (n.hash == hash(key)) {hashConflict = true;//hash相等时while (n != null) {if (n.key.equals(key)) {//hash相等并且key也相等,直接替换原来的值就行了n.value = value;table[i] = n;size++;} else {node = new Node<K, V>(hash(key), key, value, null);node.next = n;table[i] = node;size++;}n = n.next;}}}}if (!hashConflict) {//没有hash冲突,直接putnode = new Node<K, V>(hash(key), key, value, null);table[++keyIndex] = node;size++;}}}@Overridepublic V get(K key) {HyhHashMap.Node<K, V> node;return (node = getNode(key)) == null ? null : node.value;}/*** 获取Node** @param key* @return*/final HyhHashMap.Node<K, V> getNode(Object key) {if (table != null) {for (int i = 0; i < table.length; i++) {Node<K, V> node = table[i];if (node != null) {//hash相等if (node.hash == hash(key)) {while (node != null) {if (node.key.equals(key)) {//hash和key都相等时`return node;}node = node.next;}}}}}return null;}/*** 扩容** @return*/final Node<K, V>[] resize() {Node<K, V>[] newTable;int newCapacity, oldCapacity;if (table == null) {keyIndex = 0;threshold = (int) (DEFAULT_CAPACITY * DEFAULT_LOAD_FACTOR);table = (HyhHashMap.Node<K, V>[]) new HyhHashMap.Node[DEFAULT_CAPACITY];newTable = table;} else {oldCapacity = table.length;if (table.length > threshold) {//扩容两倍newCapacity = threshold *= 2;newTable = (HyhHashMap.Node<K, V>[]) new HyhHashMap.Node[newCapacity];//把原来的table移动到newTableint newIndex = 0;for (int i = 0; i < oldCapacity; i++) {Node<K, V> node = table[i];//咱们这只使用最简单的方式、不考虑其他情况、不涉及红黑树if (node != null) {if (node.next == null)newTable[newIndex] = node;else {HyhHashMap.Node<K, V> loHead = null, loTail = null, hiHead = null, hiTail = null, next;do {next = node.next;if (node.hash == 0) {if (loTail == null)loHead = node;elseloTail.next = node;loTail = node;} else {if (hiTail == null)hiHead = node;elsehiTail.next = node;hiTail = node;}} while ((node = next) != null);if (loTail != null) {loTail.next = null;newTable[newIndex] = loHead;}if (hiTail != null) {hiTail.next = null;newTable[newIndex + oldCapacity] = hiHead;}}}newIndex++;}} else {newTable = table;}}return newTable;}/*** 计算Hash值** @param key* @return*//*** 计算Hash值** @param key* @return*/static final int hash(Object key) {int h;return (key == null) ? 0 : Math.abs((h = key.hashCode()) ^ (h >>> 16));}@Overridepublic int size() {return size;}/*** Node 实现HyhMap Entry接口** @param <K>* @param <V>*/static class Node<K, V> implements HyhMap.Entry<K, V> {//hash值final int hash;// keyfinal K key;// valueV value;// next节点HyhHashMap.Node<K, V> next;public Node(int hash, K key, V value, Node<K, V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey() {return key;}public final V getValue() {return value;}}
}

五、单元测试

这里并没有加入红黑树,性能和JDK的HashMap肯定没得比

import org.junit.Test;/*** HyhHasMap Test** @Author heyuhua* @create 2021/2/9 17:54*/
public class HyhHashMapTest {/*** 普通测试*/@Testpublic void test() {HyhHashMap<String, String> hyhHashMap = new HyhHashMap<>();hyhHashMap.put("name", "heyuhua");hyhHashMap.put("height", "180cm");hyhHashMap.put("name", "hyh");hyhHashMap.put("height", "180");System.out.println("name:" + hyhHashMap.get("name") + ",height:" + hyhHashMap.get("height"));}/*** Hash冲突测试*/@Testpublic void testHashConfilct() {HyhHashMap<String, String> hyhHashMap = new HyhHashMap<>();hyhHashMap.put("轷龚", "heyuhua1");hyhHashMap.put("轸齻", "heyuhua2");System.out.println("轷龚:" + hyhHashMap.get("轷龚") + ",轸齻:" + hyhHashMap.get("轸齻"));}
}

执行结果


六、HashMap常见问题

  1. HashMap的特性?

    • 实现快速存储键值对,允许为null,key值不可重复,若key值重复则覆盖。
    • 线程不安全。
    • 底层是Hash表,不保证有顺序
  2. HashMap底层原理?
    • jdk7时采用数组+链表,jdk8后采用数组+链表+红黑树的数据结构。
  3. HashMap put原理?
    • 当我们给put()方法传递键和值时,先对键做一个hashCode()的计算来得到它在bucket数组中的位置来存储Entry对象。
  4. HashMap get原理?
    • 当获取对象时,通过get获取到bucket的位置,再通过键对象的equals()方法找到正确的键值对,然后再返回值对象。
  5. HashMap扩容机制?
    • 扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。
  6. HashMap默认初始化长度为16,并且每次自动扩展或者是手动初始化容量时,为什么必须是2的次幂?
    • 为了数据的均匀分布,减少哈希碰撞。因为确定数组位置是用的位运算,若数据不是2的次幂则会增加哈希碰撞的次数和浪费数组空间。
    • 输入数据若不是2的幂,HashMap通过一通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字
  7. HashMap大小超过了负载因子(load factor)定义的容量,怎么办?
    • 超过阙值会进行扩容操作,概括的讲就是扩容后的数组大小是原数组的2倍,将原来的元素重新hashing放入到新的散列表中去。

参考:

  1. 红黑树(一)之 原理和算法详细介绍
  2. 【老实李】JDK1.8中HashMap的红黑树
  3. hashcode详解
  4. HashMap底层实现原理及面试问题
  5. 不怕面试再问HashMap,一次彻底地梳理(原理+手写实现)

HashMap知识循序渐进分析相关推荐

  1. 硬核HashMap源码分析,HashMap文章中的圣经

    一 前言 本篇是继硬核ArrayList源码分析,答应我每天看一遍好么之后进行的第二篇源码分析:学完本篇你将对hashMap的结构和方法有个全面的了解:面试自己有多强,超人都不知道:比如HashMap ...

  2. HashMap底层原理分析(put、get方法)

    1.HashMap底层原理分析(put.get方法) HashMap底层是通过数组加链表的结构来实现的.HashMap通过计算key的hashCode来计算hash值,只要hashCode一样,那ha ...

  3. Java类集框架 —— HashMap源码分析

    HashMap是基于Map的键值对映射表,底层是通过数组.链表.红黑树(JDK1.8加入)来实现的. HashMap结构 HashMap中存储元素,是将key和value封装成了一个Node,先以一个 ...

  4. 查询已有链表的hashmap_源码分析系列1:HashMap源码分析(基于JDK1.8)

    1.HashMap的底层实现图示 如上图所示: HashMap底层是由  数组+(链表)=(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value ...

  5. linux hashmap,Java中对HashMap的深度分析与比较

    Java中对HashMap的深度分析与比较 在Java的世界里,无论类还是各种数据,其结构的处理是整个程序的逻辑以及性能的关键.由于本人接触了一个有关性能与逻辑同时并存的问题,于是就开始研究这方面的问 ...

  6. 源码分析系列1:HashMap源码分析(基于JDK1.8)

    1.HashMap的底层实现图示 如上图所示: HashMap底层是由  数组+(链表)+(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value ...

  7. HashMap源码分析(转载)

    一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap  ...

  8. Map接口总结与HashMap源码分析

    Map接口 1.Map,用于保存K-V(双列元素) 2.Map中的Key Value可以是任意引用分类型的数据,会封装到HashMap的Node对象中 3.Map的key不允许重复.原因和HashSe ...

  9. 在参考了众多博客之后,我写出了多达三万字的HashMap源码分析,比我本科毕业论文都要精彩

    HashMap源码分析 以下代码都是基于java8的版本 HashMap简介 源码: public class HashMap<K,V> extends AbstractMap<K, ...

最新文章

  1. Bilibili 笔试扭蛋器----回溯dfs 9.21
  2. 《JavaScript设计与开发新思维》——1.7 JavaScript编程目标
  3. base64/32/16编码
  4. 用tirgger实现主键自增(trigger+sequence)
  5. Jackson 配置 ObjectMapper
  6. Go 定时器内部实现原理剖析
  7. Java常用API(六)Date 日期类介绍及使用
  8. 21行代码AC_【蓝桥杯】承压计算(解题通法+技巧)
  9. Java:ChronicleMap第1部分,精简版
  10. js中setAttribute 的兼容性
  11. 错误调试:GPU 版 TensorFlow failed to create cublas handle: CUBLAS_STATUS_ALLOC_FAILED
  12. iOS开发之使用Runtime给Model类赋值
  13. python中循环结构break_Python编程10:跳出循环结构之break和continue
  14. WPF_界面_图片/界面/文字模糊解决之道整理
  15. 51单片机小车(附源码)
  16. 计算机无法锁定在任务栏,win10系统无法锁定任务栏的处理办法
  17. 谈程序员的职业发展前景
  18. html 360打不开,360浏览器打不开如何解决_360浏览器打不开解决教程
  19. java int和Integer的区别
  20. 如何将Excel一页转PDF变多页?好用的PDF转换工具推荐

热门文章

  1. python图表_使用Python绘制图表大全总结
  2. vs2015最详细的使用教程(有图)
  3. python打印26个英文字母和数字
  4. 径向基函数模型matlab,径向基函数RBF.ppt
  5. 使用context:component-scan base-package= /context:component-scan报500错误
  6. stm32笔记02——ST-Link下载器使用说明
  7. 面向对象,数据库交互的Spring-boot(新手)
  8. 十个励志小故事,一场精彩的人生课!
  9. Android Studio 更换/设置App应用图标Icon
  10. mac电脑如何与手机同步复制粘贴_Mac新手教程:如何将照片和视频从 iPhone、iPad传输到Mac电脑上?...