Java-HashMap

1. 主要用途

HashMap是基于Map接口实现的一种键-值对的存储结构,允许null值,同时非有序,非同步(线程不安全)。HashMap底层实现是数组+链表+红黑树(JDK1.8增加了红黑树部分)。它存储和查找数据时,是根据键的hashCode的值计算出具体的存储位置。HashMap最多只允许一条记录的键位null,HashMap增删改查等常规操作都有不错的执行效率,是ArrayList和LinkedList等数据结构的一种折中实现。

2. 源码分析

2.1 部分成员变量分析

int size;//用于记录HashMap实际存储元素的个数float loadFactor;//负载因子(默认是0.75)int threshold;//下一次扩容时的阈值,达到阈值便会触发扩容机制resizeNode<K,V>[] table;//底层数组,充当哈希表的作用,用于存储对应hash位置的元素Node<K,v>,此数组长度总是2的N次幂
  • Node<K,V>[] table;

    哈希表存储的核心元素是Node<K,V>,Node<K,V>包含:

    final int hash:元素的哈希值,决定元素存储在table哈希表中的位置。

    final K key:键

    V value:值

    Node<K,V> next:记录下一个元素节点(单链表结构,用于解决hash冲突)

2.2 put(K key,V value);

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;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) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}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;if (!onlyIfAbsent || oldValue == null)e.value = value;//更新value值afterNodeAccess(e);return oldValue;//返回oldValue,则证明是更新操作}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;//返回null,则证明是新增操作
}

首先判断哈希表Node<K,V> table是否为null或者长度为0,是则进行resize()方法进行扩容。

然后根据插入的键值key的hash值,通过(n - 1) & hash]当前元素的hash值&hash表长度-1(实际上就是hash值%hash表长度)计算出存储位置table[i]。如果存储位置没有元素存放,则将新增节点存储在此位置table[i]。

如果存储位置已有键值对元素存在,则判断当前位置的hash值和key值是否和当前操作元素一致,一致则证明是修改value操作,覆盖value即可。

如果当前存储位置既有元素,又不和当前操作元素一致,则证明此位置table[i]已经发生了hash冲突,则通过判断头节点是否是treeNode,如果是treeNode则证明此位置的结构是红黑树,以红黑树的方式新增节点。如果不是红黑树,则证明是单链表,将新增节点插入至链表的最后位置,随后判断当前链表长度是否大于等于8,是则将当前存储位置的链表转化为红黑树。遍历过程中如果发现key已经存在,则直接覆盖value。

插入成功后,判断当前存储键值对的数量大于阈值threshold,是则扩容。

2.3 get(Object 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;
}

首先调用hash(key)方法计算出key的hash值。

根据查找的键值key的hash值,通过(n-1)&hash计算出存储位置table[i],判断当前位置是否有元素存在。

如果存储位置有元素存放,则首先比较头节点元素,如果头节点的key的hash值和要获取的key的hash值相等,并且头结点的key和要获取的key相等,则返回该位置的头节点。

如果存储位置没有元素存放,则返回null。

如果存储位置有元素但不是要查找的元素,则需要遍历该位置进行查找。

先判断头节点是不是treeNode,如果是treeNode则证明此位置的结构是红黑树,以红黑树的方式遍历查找该节点,没有则返回null。

如果不是红黑树,则证明是单链表。遍历单链表,逐一比较链表节点,链表节点的key的hash值和要获取的key的hash值相等,并且链表节点的key和要获取的key相等,则返回该节点,遍历结束仍未找到对应key的节点,则返回null。

2.4 remove(Object key);

public V remove(Object key) {Node<K,V> e;return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
}final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;else if ((e = p.next) != null) {if (p instanceof TreeNode)node = ((TreeNode<K,V>)p).getTreeNode(hash, key);else {do {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {if (node instanceof TreeNode)((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);else if (node == p)tab[index] = node.next;elsep.next = node.next;++modCount;--size;afterNodeRemoval(node);return node;}}return null;
}

首先调用hash(key)方法计算出key的hash值。

根据查找的键值key的hash值,通过(n-1)&hash计算出存储位置table[i],判断存储位置是否有元素存在。

如果存储位置有元素存放,则首先比较头节点元素,如果头节点的key的hash值和要获取的key的hash值相等,并且头节点的key本身和要获取的key相等,则该位置的头节点即为要删除的节点,记录此节点至变量node中。

如果存储位置没有元素存放,则没有找到对应要删除的节点,则返回null。

如果存储位置有元素存放,但是头节点不是要删除的元素,则需要遍历该位置进行查找。

先判断头节点是不是treeNode,如果是treeNode则证明此位置的结构是红黑树,以红黑树的方式遍历查找并返回该节点至node。

如果不是红黑树,则证明是单链表。遍历单链表,逐一比较链表节点,链表节点的key的hash值和要获取的key的hash值相等,并且链表节点的key和要获取的key相等,则为要删除的节点,记录此节点为node。

如果找到要删除的节点node,则判断是否需要比较value也是否一致(boolean matchValue),如果value是否一致或者不需要比较value值,则执行删除节点操作,删除操作根据不同的情况进行不同的处理。

如果当前的节点是treeNode,则证明当前位置的链表已变成红黑树,通过红黑树的方式删除对应节点。

如果不是红黑树,则证明是单链表。如果要删除的是头节点,则当前存储位置table[i]的头节点指向下一个节点。

如果要删除的节点不是头节点,则将要删除的节点的后继节点node.next赋值给要删除结点的前驱节点的next。

2.5 replace(K key,V value);

public V replace(K key, V value) {Node<K,V> e;if ((e = getNode(hash(key), key)) != null) {V oldValue = e.value;e.value = value;afterNodeAccess(e);return oldValue;}return null;
}

首先调用hash(key)方法计算出key的hash值。

然后调用getNode方法获取对应key所映射的value值。

记录元素旧值,将新值赋值给元素,返回元素旧值,如果没有找到元素,则返回null。

3. hash冲突

hash冲突:当我们调用put(K key,V value)操作添加key-value键值对,这个key-value键值对存放在的位置是通过hash()来计算key的hash值,然后将这个hash值%哈希表的长度,得到具体的存放位置。所以put(K key,V value)多个元素,是有可能计算出相同的存放位置。此现象就是hash冲突或者叫hash碰撞。

hash冲突的避免:既然会发生hash冲突,我们就应该想办法避免此现象的发生,解决这个问题最关键的就是如何生成元素的hash值。Java是使用“扰动函数”生成元素的hash值。

示例代码:

/***JDK 8 的hash方法*/
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

Java8只做一次16位右位移异或混合。例子如下:

右位移16位正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和地位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留了下来。

4 HashMap的容量为什么一定是2的n次方?

因为调用put(K key,V value)操作添加key-value键值对时,具体确定此元素的位置是通过hash值%哈希表table的长度计算的。但是”模“运算的消耗相对较大,通过位运算h&(length-1)也可以得到取模后的存放位置,而且位运算的效率高,但只有length的长度是2的n次方时,h&(length-1)==h%length。而且当数组长度为2的n次方时,不同的key算出的index相同的几率较小,那么数据在数组上的分布就比较均匀,也就是说碰撞的几率小。

5. HashMap的负载因子

例子如下:

底层哈希表Node<K,V>[] table的容量大小capacity为16,负载因子loadFactor为0.75,则当存储元素个数size=capacity16*loadFactor0.75=12时,则会触发HashMap的扩容机制,调用resize方法进行扩容。

当负载因子越大,则HashMap的装载程度越高。也就是能容纳更多的元素,元素多了,发生hash碰撞的几率就会加大,从而链表就会拉长,此时的查询效率就会降低。

当负载因子越小,则链表中的数据量就越稀疏,此时会对空间造成浪费,但是此时查询效率高。

我们可以在创建HashMap时根据实际需要适当的调整loadFactor的值,如果程序比较关心空间开销,内存比较紧张,可以适当的增加负载因子;如果程序比较关心时间开销,内存比较宽裕则可以适当的减少负载因子。

6. HashMap和Hashtable的区别

  1. 容器整体结构

    HashMap的key和value都允许为null,HashMap遇到key为null的时候,hash(key)返回0,所以存储在下标为0的位置。

    Hashtable的key和value都不允许为null,Hashtable遇到null,直接返回NullPointerException。

  2. 容量设定与扩容机制

    HashMap默认初始化容量为16,并且容器容量一定是2的n次方,扩容时,是以原容量2倍的方式进行扩容

    Hashtable默认初始化容量为11,扩容时,是以原容量2倍再加1的方式进行扩容。

  3. 散列分布方式(计算存储位置)

    HashMap是先将key键的hashCode经过hash函数后得到的hash值,然后再利用hash&(length-1)的方式代替取余,得到元素的存储位置。

    Hashtable则是除留余数法进行计算存储位置的(因为其默认容量也不是2的n次方,所以也无法用位运算替代模运算),int index = (hash&0x7FFFFFFF)%tab.length;。

  4. 线程安全

    HashMap不是线程安全,如果想线程安全,可以通过synchronized使线程安全。但是使用时的运行效率会下降,所以建议使用ConcurrentHashMap容器以此来达到线程安全。

    Hashtable则是线程安全的,每个操作方法都有synchronized修饰使其同步,但运行效率也不高,所以还是建议使用ConcurrentHashMap容器达到线程安全。

7. HashMap不是线程安全的,如果多线程下,如何处理?

HashMap不是线程安全的,如果多个线程同时对一个HashMap更改数据时,会导致数据不一致或者数据污染。如果出现线程不安全的操作时,HashMap会尽可能的抛出ConcurrentModificationException防止数据异常,当我们在遍历一个HashMap期间,是不能对HashMap仅从添加,删除等更改数据的操作的,否则也会抛出ConcurrentModificationException异常,此为fast-fail(快速失败)机制。从源码上分析,我们在put,remove等更改HashMap数据时,都会导致modCount的改变,当expectedModCount!=modCount时,则抛出ConcurrentModificationException异。如果想要线程安全,可以考虑使用ConcurrentHashMap。

8. 在使用HashMap时最好选取不可变对象最为key

首先解释可变对象:指创建后自身状态能改变的对象。换句话说,可变对象是该对象在创建后它的哈希值可能被改变。

我们在使用HashMap时最好选择不可变对象作为key。例如String,Integer等不可变类型作为key是非常明智的。

如果key对象是可变的,那么key的哈希值就可能改变。在HashMap中可变对象作为key会造成数据丢失。因为我们再进行hash&(length-1)取模运算时计算位置,位置可能已经发生改变,导致数据丢失。

Java-HashMap详解相关推荐

  1. Java 遍历HashMap详解

    Java 遍历HashMap详解 遍历KeySet() //遍历hashmap的keySetHashMap<String, Object> mapForKey = new HashMap& ...

  2. Java集合详解4:HashMap和HashTable

    <Java集合详解系列>是我在完成夯实Java基础篇的系列博客后准备开始写的新系列. 这些文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查 ...

  3. [Java 8 HashMap 详解系列]7.HashMap 中的红黑树原理

    [Java 8 HashMap 详解系列] 文章目录 1.HashMap 的存储数据结构 2.HashMap 中 Key 的 index 是怎样计算的? 3.HashMap 的 put() 方法执行原 ...

  4. Java集合详解6:TreeMap和红黑树

    <Java集合详解系列>是我在完成夯实Java基础篇的系列博客后准备开始写的新系列. 这些文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查 ...

  5. 【Java-Java集合】Java集合详解与区别

    [Java-Java集合]Java集合详解与区别 1)概述 2)集合框架图 2.1.总框架图 2.2.Iterable 框架图 2.3.Map 框架图 3)List 3.1.ArrayList 类继承 ...

  6. Java集合排序及java集合类详解

    Java集合排序及java集合类详解 (Collection, List, Set, Map) 摘要内容 集合是Java里面最常用的,也是最重要的一部分.能够用好集合和理解好集合对于做Java程序的开 ...

  7. java map详解

    java map详解 Map 是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键对象和一个值对象.其中,键对象不允许重复,而值对象可以重复,并且值对象还可以是 Map 类 ...

  8. Java集合详解5:深入理解LinkedHashMap和LRU缓存

    <Java集合详解系列>是我在完成夯实Java基础篇的系列博客后准备开始写的新系列. 这些文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查 ...

  9. Apache Thrift - java开发详解

    2019独角兽企业重金招聘Python工程师标准>>> Apache Thrift - java开发详解 博客分类: java 架构 中间件 1.添加依赖 jar <depen ...

  10. Java泛型详解-史上讲解最详细的,没有之一

    目录 1. 概述 2. 一个栗子 3. 特性 4. 泛型的使用 4.1 泛型类 4.2 泛型接口 4.3 泛型通配符 4.4 泛型方法 4.4.1 泛型方法的基本用法 4.4.2 类中的泛型方法 4. ...

最新文章

  1. 屏幕截图在网页设计中应用的30个优秀案例
  2. 浅谈在Java开发中的枚举的作用和用法
  3. c++ getline 读不到东西_C++ getline()函数问题
  4. ITK:笛卡尔方位角高程
  5. python安装opencv出现错误_Python3安装OpenCV出错,如何解决?
  6. 6.4.3树和森林的遍历
  7. Mysql Group Replication(MGR)搭建
  8. map的四种遍历方式
  9. 获得iframe中的对象的方法
  10. 2013、2014 U.S.NEWS美国大学排名榜
  11. VMware ESXi-虚拟化平台的搭建
  12. openCV专栏(二):基础计算实战+色彩空间转换
  13. 阿里云服务器docker安装网心云容器魔方
  14. 复合型网络拓扑结构图_网络拓扑结构大全和图片(星型、总线型、环型、树型、分布式、网状拓扑结构)....
  15. 全新来客码智能纳客营销系统免费使用功能效果
  16. am335x开发板的疑问以及解答
  17. Python学习笔记(一)压缩与解压缩文件
  18. PHPstudy小白起步
  19. 如何在工作中提升自己的学习能力
  20. 在有M1芯片的Mac上安装微信、抖音等软件

热门文章

  1. Python 在Online Judge上自动挂题脚本
  2. 宽和窄俯卧撑哪个更难_在俯卧撑的训练中,宽距和窄距有什么不同?哪种效果更好?...
  3. 神奇的null 请输出结果并进行解释 console.log([typeof null, null instanceof Object])
  4. 我这三年---mugen篇(2) by 皇峰尖
  5. 给微电子专业学生的一些建议,如何避免踩坑?
  6. Unity3D实现通用的Image动态切换显示图标
  7. Hadoop 真的要死了吗?
  8. C 使用fread读取文件
  9. 蚂蚁管网参数化三维建模数据规格V1.0
  10. LeetCode 2302. 统计得分小于 K 的子数组数目(前缀和+二分查找)