Map集合(HashMap)

本文详细介绍了Java中的Map接口以及HashMap实现,包括Set接口中的HashSet、LinkedHashSet和TreeSet的特性和性能分析。重点讲解了HashMap的存储结构、put方法流程、扩容机制以及如何解决哈希冲突,强调了两次扰动的重要性。还提及了JDK1.8引入的红黑树优化,以及为何数组长度需要为2的幂次方。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Set接口

无序、不允许重复

具体实现类

HashSet不保证元素的添加顺序,底层采用哈希表算法,查询效率高

  • 判断两个元素是否相等equals() 方法返回为 true- 要求hashCode() 值必须相等
  • 要求存入 HashSet 中的元素要覆盖 equals() 方法和 hashCode()方法

LinkedHashSet是HashSet的子类,在HashSet的基础上添加了额外的链表,底层采用了哈希表算法以及链表算法,既保证了元素的添加顺序,也保证了查询效率。但是整体性能要低于HashSet

  • 判断两个元素是否相等equals() 方法返回为 true要求hashCode() 值必须相等
  • 要求存入 HashSet 中的元素要覆盖 equals() 方法和 hashCode()方法

TreeSet不保证元素的添加顺序,但是会对集合中的元素进行排序。底层采用红-黑树算法(树结构比较适合查询),但是添加的效率较低

  • 存放的元素需要进行大小比较,所以类必须实现Comparable接口。必须同一类型

各种Set集合性能分析

HashSet和TreeSet是Set集合中用得最多的集合。HashSet总是比TreeSet集合性能好,因为HashSet不需要额维护元素的顺序。
LinkedHashSet需要用额外的链表维护元素的插入顺序,因此在插入时性能比HashSet低,但在迭代访问(遍历)时性能更高。因为插入的时候即要计算hashCode又要维护链表,而遍历的时候只需要按链表来访问元素。
EnumSet元素是所有Set元素中性能最好的,但是它只能保存枚举类型的元素。

相关的集合计算

addAll将指定集合中的所有元素都添加到此集合中,如果在进行此操作的同时修改了指定的集合,那么将不能保证操作的正确性
removeAll从指定的集合中移除包含在另一个集合中的元素,返回值为boolean,如果包含了要移除的对象则返回true否则false
retainAll仅仅保留集合中同时包含在指定集合的对象,其它的全部移除
containsAll用来查看在该集合中是否存在在指定集合中的所有对象,返回true表示存在,否则false

Collection和Collections的区别

Collection是java.util下的接口,它是各种集合的父接口,继承于它的接口主要有Set 和List
Collections是个java.util下的类,是针对集合的帮助类,提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作

Map接口

哈希表就是一种以键-值(key-indexed) 存储数据的结构,只要输入待查找的值即key,即可查找到其对应的值。

哈希的思路很简单,如果所有的键hashCode都是整数,那么就可以使用一个简单数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。
简单的计算方法hashcode%数组长度=【0,数组的长度-1】

它提供了一组键值的映射。其中存储的每个数据对象都有一个相应的键key,键决定了值对象在Map中的存储位置。键应该是唯一的,不允许重复,每个key只能映射一个value。

Map接口的定义

定义map对象时需要指定key和value对应的类型,必须是复杂类型,不能使用int
map接口中有一个内部接口为Entry:interface Entry<K,V>封装所存储的key-value对数据

interface Entry<K,V> {
        K getKey();   因为key决定数据的存放位置,一旦存储成功则key不允许修改
        V getValue();    针对value提供get/set方法
        V setValue(V value);
//存储的数据应该是可以进行等值判断的,key不允许重复
        boolean equals(Object o);
        int hashCode();
}

结论:每个Entry对象中封装了一个key和value
另外Entry接口中通过静态方法提供了一组比较器的默认实现

Map接口中常见方法

Object put(Object key,Object value):用来存放一个键-值对Map中 ,如果出现key值冲突则后盖前。允许key值和value值为null,但是key值为null只能有一个,value值为null没有个数限制
size():int用于获取集合中的元素个数

Object remove(Object key):根据key(键),移除键-值对,并将值返回
Object get(Object key) :根据key(键)取得对应的值,如果key值不存在则返回为null

boolean containsKey(Object key) :判断Map中是否存在某键key

void clear() :清空当前Map中的元素

boolean containsValue(Object value):判断Map中是否存在某值value

3种视图

另外Map提供了3种视图,分别是key所组成的set、value所组成的collection、key-value的Entry集合Set
public Set keySet() :返回所有的键key,并使用Set容器存放,获取key值后就可以通过get方法获取key对应的值value
public Collection values() :返回所有的值Value,并使用Collection存放
public Set entrySet() :返回一个实现 Map.Entry 接口的元素 Set

Map实现类

HashMap、TreeMap、LinkedHashMap、Hashtable等

HashMap

类定义

public class HasgMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>,Cloneable,Serializable{

相关的常量值

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  默认初始化容积
static final int MAXIMUM_CAPACITY = 1 << 30;//最大容积值
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认的加载因子值

具体的内部数据存储方式
在这里插入图片描述
静态内部类用于实现Entry,HahMap中存放的key/value对就被封装为Node对象。其中key就是存放的键值,用于决定具体的存放位置;value是具体存放的数据,hash就是当前Node对象的hash值,next用于指向下一个Node节点(单向链表)
具体存储数据的实现采用的是单向链
在这里插入图片描述
Java中采用拉链法实现了Hash表结构

Map中存放数据

transient Node<K,V>[] table;

table就是存放单向链的数组

重要的阈值
static final int TREEIFY_THRESHOLD = 8;//树化阈值:即链表转成红黑树的阈值,在存储数据时,当链表长度 > 该值时,则将链表转换成红黑树
static final int UNTREEIFY_THRESHOLD = 6;//桶的链表还原阈值:即红黑树转为链表的阈值,当在扩容(resize())时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将 红黑树转换成链表

在这里插入图片描述

构造器
负载因子就是用于控制hash表中所允许存储的元素个数占总容积的百分比。值越大hash碰撞的概率越高,但是越节约空间;值越小hash碰撞的概率越低,但是越浪费空间;
控制容器中允许存放的元素个数上限为 [容积*负载因子]

tableSizeFor方法用于获取数组的初始化大小

获取的值为获取2n<初始化容积<2(n+1)时返回2(n+1),返回一个大于等于初始化容积值的2n值
tableSizeFor用于计算容积值,不是设置多少就是多少。就是计算一个2**n值>=设定的容积值.例如初始化容积参数值为7则实际创建容积值为8
注意:在构造器中并没有创建任何用于存储数据的集合—延迟加载,第一次存储数据时才进行空间分配

HashMap的存储结构

HashMap采用的是拉链法实现数据的存储,其中有一个数组Node[],每个元素上存储一个链表Node。每个Node[]数组中的元素被称一个桶bucket,一个桶对应一个hash映射的值,例如0,1等,可能会出现不同的key,但是hash映射的位置相同,例如16、32等,这采用单向链表结构存储hash映射值相同的所有数据(JDK8+在单个链表长度大于阈值8时自动转换为红黑树,删除节点使某单个树节点数小于阈值6时会自动从红黑树退化为链表结构)
相关参数:
capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
loadFactor:负载因子,取值在(0,1)之间,默认为 0.75
threshold:扩容的阈值,等于 capacity * loadFactor
HashMap底层采用的是Entry数组和链表实现。
Map 主要用于存储键key值value对,根据键得到值,因此键不允许重复,但允许值重复。
HashMap是一个最常用的Map,它根据键的hashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。
HashMap最多只允许一条记录的键为null;允许多条记录的值为null
HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致,会出现环形链。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力。

如何判断环型链?

Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。
TREEIFY_THRESHOLD为8,如果新插入的值是链表中的第 9 个会触发下面的 treeifyBin(树化操作,就是将单向链转换为红黑树),也就是将链表转换为红黑树。
JDK8+插入数据到链表的最后面,Java7 是插入到链表的最前面

HashMap的put方法的具体流程

在这里插入图片描述
为什么HashMap中String、Integer这样的包装类适合作为Key
String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
都是final类型,具备不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范,不容易出现Hash值计算错误的情况;
要让自己的Object作为Key应该怎么办呢
重写hashCode()和equals()方法
重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞
重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性

HashMap的扩容操作是怎么实现的

通过分析源码我们知道了HashMap通过resize()方法进行扩容或者初始化的操作
如果HashMap中存储数据的桶数组table长度为0或者为null,则按照初始化配置参数进行数组的创建;如果长度非空,则数组扩容一倍,并重新计算所有元素的存储新位置,rehash计算

HashMap是怎么解决哈希冲突的
在Java中,保存数据有两种比较简单的数据结构:数组和链表。
数组的特点是:寻址容易,插入和删除困难;
链表的特点是:寻址困难,但插入和删除容易;所以将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突
这样就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化
主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动
这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)

  • 总结简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:
    使用链地址法(使用散列表)来链接拥有相同hash值的数据;
    使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
    引入红黑树进一步降低遍历的时间复杂度,使得遍历更快

JDK1.8新增红黑树
通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn)

HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标
hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置
HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均
在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题

为什么数组长度要保证为2的幂次方呢
只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;如果length为2的次幂则length-1转化为二进制必定是11111……的形式,在于h的二进制与操作效率会非常的快,而且空间不浪费;如果length不是2的次幂,比如length为15,则length-1为 14,对应的二进制为1110,在于h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费
为什么是两次扰动
两次扰动就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值