Java数据结构篇二:HashMap详解

前言

HashMap 又叫 Hash 表或散列表,是基于哈希表的 Map 接口实现。此实现提供了基于 Key-Value 映射结构数据的所有可选操作,如:增、删、改、查等。HashMap 并不保证映射顺序,特别是它不保证插入顺序恒久不变。HashMap是面试常考的知识点,对该知识点有必要进行一下系统的学习。

首先简单介绍一下和 HashMap 有亲戚关系的三个类,分别为 LinkedHashMap、TreeMap 和 Hashtable。类的继承关系如下图所示:

HashTable是继承自Dictionary类,而HashMap是继承自AbstractMap类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。HashMap性能要好过Hashtable。

HashMap:(1)允许使用null键null值(key和value)都可以。这样的键只有一个,可以有一个或多个键所对应的值为null。(2)非线程安全 (3)HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍

ConCurrentHashMap:(1)不允许null键null值(key和value)都不可以。(2)线程安全

HashTable:(1)不允许null键null值  (2)线程安全 (3)Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1

LinkedHashMap: (1) LinkedHashMap 是 HashMap 的一个子类 (2) 保存了记录的插入顺序,在用 Iterator 遍历时,先得到的记录肯定是先插入的。

TreeMap: (1) 实现了 SortedMap 接口,当用 Iterator 遍历时,得到的记录是默认按键值升序排序的。(2) 底层是红黑树

HashMap常用方法

网上找了一篇常用方法 不做过多介绍:https://blog.csdn.net/lzx_cherry/article/details/98947819

HashMap源码解析

网上找了一篇源码解析写的很详细 不做过多介绍 :https://www.jianshu.com/p/003256ce41ce

HashMap存储结构

1.Hashmap 概念理解

变量

术语

说明

size

大小

HashMap的存储大小

threshold

临界值

HashMap大小达到临界值,需要重新分配大小。

loadFactor

负载因子

HashMap大小负载因子,默认为75%。

modCount

统一修改

HashMap被修改或者删除的次数总数。

Entry

实体

HashMap存储对象的实际实体,由Key,value,hash,next组成。

2.存储结构  

JDK 1.8 对 HashMap 进行了比较大的优化,底层实现由之前的 “数组+链表” 改为 “数组+链表+红黑树”, 当链表长度超过阈值8时,将链表转换为红黑树。当数组的元素个数大于 容量*扩容因子时,会进行扩容操作。对于 HashMap 及其子类而言,它们采用 Hash 算法(将任意长度的二进制映射为较短的固定长度的二进制值机哈希值)来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity (默认的容量是16)的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素(Entry对象: key-value形式)。结构如图:

HashMap的存储结构是由数组和链表共同完成的HashMap是Y轴方向是数组,X轴方向就是链表的存储方式。(数组的存储方式在内存的地址是连续的,大小固定,一旦分配不能被其他引用占用。它的特点是查询快插入和删除的操作比较慢链表的存储方式是非连续的,大小不固定,特点与数组相反,插入和删除快,查询速度慢。HashMap可以说是一种折中的方案

3.HashMap基本原理

1)对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置,首先判断Key是否为Null,如果为null,直接查找Enrty[0],如果不是Null,先计算Key的HashCode,然后经过二次Hash。得到Hash值,这里的Hash特征值是一个int值。

2)根据Hash值,调用indexFor 方法获取索引。indexFor方法其实主要是将hashcode换成链表数组中的下标即找到存放的桶。

3)通过索引找到bucket(hash桶,就是找到了所在的链表,然后按照链表的操作对Value进行插入、删除和查询操作。

// HashMap In Java 7
// hash :该方法主要是将Object转换成一个整型。
final int hash(Object k) {
   int h = hashSeed;
   if (0 != h && k instanceof String) {
       return sun.misc.Hashing.stringHash32((String) k);
   }
   h ^= k.hashCode();
   h ^= (h >>> 20) ^ (h >>> 12);
   return h ^ (h >>> 7) ^ (h >>> 4);
}
// indexFor :该方法主要是将hash生成的整型转换成链表数组中的下标。
static int indexFor(int h, int length) {
   return h & (length-1);
}

4.HashMap 初始化过程 

当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity (默认的容量是16)的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素(Entry对象: key-value形式)。

无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是:HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链

注1当两个key通过hashCode计算相同时,则发生了hash冲突(碰撞),HashMap解决hash冲突的方式是用链表。当发生hash冲突时,则将存放在数组中的Entry设置为新值的next(这里要注意的是,比如A和B都hash后都映射到下标i中,之前已经有A了,当map.put(B)时,将B放到下标i中,A则为B的next,所以新值存放在数组中,旧值在新值的链表上)

在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。这就是JDK7与JDK8中HashMap实现的最大区别。

注2哈希碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中。Hashmap里面的bucket出现了单链表的形式,散列表要解决的一个问题就是散列值的冲突问题,通常是两种方法:链表法和开放地址法。减小碰撞方式:1.扩大容量2.优化hash算法。

链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。
见详解:https://www.jianshu.com/p/379680144004

5.讲一下 HashMap 中 put 方法过程

1)对key的hashCode做hash操作,然后再计算在bucket中的index(1.5 HashMap的哈希函数); 

2)如果没碰撞直接放到bucket里; 

3)如果碰撞了,说明两个 Entry的 key的 hashCode()返回值相同,那它们的存储位置相同。此时分两种情况:                     

3.1首先判断这两个 Entry的 key通过equals比较返回 true,新添加 Entry的 value将覆盖集合中原有 Entry的 value,但key不会覆盖(保证key的唯一性) 。                                                                                                                                                                 

3.2如果这两个 Entry的 key通过equals比较返回 false,新添加的 Entry将与集合中原有 Entry形成 Entry链,而且新添加的 Entry位于 Entry链的头部。

4)如果bucket满了(超过阈值,阈值=loadfactor*current capacity,load factor默认0.75),就要resize。

注1:HashMap里面没有出现hash冲突时,没有形成单链表时,hashmap查找元素很快,get()方法能够直接定位到元素,但是出现单链表后,单个bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。通过上面可知如果多个hashCode()的值落到同一个桶内的时候,这些值是存储到一个链表中的。最坏的情况下,所有的key都映射到同一个桶中,这样hashmap就退化成了一个链表——查找时间从O(1)到O(n)

注2:扩容。这个过程也叫作rehashing,因为它重建内部数据结构,并调用hash方法找到新的bucket位置。大致分两步: 
1.扩容:容量扩充为原来的两倍(2 * table.length); 
2.移动:对每个节点重新计算哈希值,重新计算每个元素在数组中的位置,将原来的元素移动到新的哈希表中。 

6.讲一下get()方法的工作原理

  通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表中查找对应的节点。

常见问题

1.能否让HashMap同步?

HashMap可以通过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap);

2.HashMap负载因子能不能设置成1?

https://mp.weixin.qq.com/s/kbLASf0lcF4PDJ3qBsFyUg

3.HashMap初始化默认为啥容量为16?

HashMap作为一种数据结构,元素在put的过程中需要进行hash运算,目的是计算出该元素存放在hashMap中的具体位置。hash运算的过程其实就是对目标元素的Key进行hashcode,再对Map的容量进行取模,而JDK 的工程师为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂。而作为默认容量,太大和太小都不合适,所以16就作为一个比较合适的经验值被采用了。为了保证任何情况下Map的容量都是2的幂,HashMap在两个地方都做了限制。首先是,如果用户制定了初始容量,那么HashMap会计算出比该数大的第一个2的幂作为初始容量。另外,在扩容的时候,也是进行成倍的扩容,即4变成8,8变成16。详见:hashmap容量

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMapJava中最常用的哈希表实现之一,它基于哈希表实现了Map接口。以下是HashMap源码的详细解释: HashMap内部是由一个数组和链表组成的,数组的每个元素称为桶,每个桶存储一个链表(可能为空),链表中的每个节点都是一个键值对(key-value pair)。 以下是HashMap的主要属性: ```java transient Node<K,V>[] table; // 存储元素的数组 transient int size; // 元素大小 int threshold; // 扩容阈值 final float loadFactor; // 负载因子 ``` 其中,table是一个transient修饰的Node数组,存储HashMap中的元素;size表示HashMap中元素的个数;threshold表示HashMap的扩容阈值,即当元素个数达到这个值时就需要扩容;loadFactor是负载因子,用于决定HashMap何时需要扩容。 以下是HashMap的主要方法: 1. put(K key, V value) :将指定的键值对添加到HashMap中,如果键已经存在,则更新对应的值。 2. get(Object key):获取指定键对应的值,如果键不存在则返回null。 3. remove(Object key):从HashMap中删除指定的键值对,如果键不存在则返回null。 4. clear():从HashMap中删除所有的键值对。 5. resize():扩容HashMap,将table的大小增加一倍。 6. hash(Object key):计算键的哈希值。 7. getNode(int hash, Object key):获取指定键的节点。 8. putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict):实际执行put操作的方法,会根据指定的参数决定是否更新已有键的值、是否删除过期键等。 HashMap的put方法实现如下: ```java public V put(K key, V value) { // 计算键的哈希值 int hash = hash(key); // 计算键在table数组中的索引 int i = indexFor(hash, table.length); // 遍历桶中的链表,查找指定键 for (Node<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(hash, key, value, i); return null; } ``` 在这个方法中,我们首先计算键的哈希值,然后计算键在table数组中的索引。接着,我们遍历桶中的链表,查找指定键,如果键已经存在,则更新对应的值。否则,我们创建新的节点,并将其添加到桶的链表中。 HashMap的get方法实现如下: ```java public V get(Object key) { // 计算键的哈希值 int hash = hash(key); // 计算键在table数组中的索引 int i = indexFor(hash, table.length); // 遍历桶中的链表,查找指定键 for (Node<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 如果找到指定键,则返回其对应的值 return e.value; } } // 如果指定键不存在,则返回null return null; } ``` 在这个方法中,我们首先计算键的哈希值,然后计算键在table数组中的索引。接着,我们遍历桶中的链表,查找指定键,如果找到指定键,则返回其对应的值。 HashMap的remove方法实现如下: ```java public V remove(Object key) { // 计算键的哈希值 int hash = hash(key); // 计算键在table数组中的索引 int i = indexFor(hash, table.length); // 遍历桶中的链表,查找指定键 Node<K,V> prev = table[i]; Node<K,V> e = prev; while (e != null) { Node<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { modCount++; size--; if (prev == e) { table[i] = next; } else { prev.next = next; } e.recordRemoval(this); return e.value; } prev = e; e = next; } // 如果指定键不存在,则返回null return null; } ``` 在这个方法中,我们首先计算键的哈希值,然后计算键在table数组中的索引。接着,我们遍历桶中的链表,查找指定键,如果找到指定键,则从链表中删除节点,并返回其对应的值。否则,我们返回null。 以上就是HashMap源码的详细解释。HashMap是一个非常常用且实用的数据结构,它的实现原理也非常值得深入学习和理解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值