Map接口

Map接口

Map不属于Collection接口,属于独立接口,可以通过entrySet()方法转化为Set集合但Set需要实现Entry接口。

哈希表就是一种以键-值(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中所存放的数据将给封装为一个一个的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

另外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
在这里插入图片描述
forEach和Lambda表达式实现输出
在这里插入图片描述

Map实现类

HashMap、TreeMap、LinkedHashMap、Hashtable等

HashMap

类定义
在这里插入图片描述
相关的常量值

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中存放数据
在这里插入图片描述
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


在构造器中并没有创建任何用于存储数据的集合—延迟加载,第一次存储数据时才进行空间分配


为什么使用2的n次方?

  • 追加元素需要计算允许存储元素个数上限的问题

求余的实现方法:
2%8=2 | 2 & (8-1) | 0010 & 0111 = 0010
9%8=1 | 9 & (8-1) |1001 & 0111 = 0001
15%8=7 | 15 & (8-1) | 1111 & 0111=0111

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冲突,两次就够了,已经达到了高位低位同时参与运算的目的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值