Java集合框架详解(二)Map-HashMap

一、Map概述

Map是键值对集合,以key-value的形式存储元素,在内部实现上,是通过类Entry<K,V>存储键值对。普遍认为其查找复杂度为O(1)。下面是摘取Map接口中定义的比较常用的方法列表。

方法名

 

size

返回Map大小,包含的元素数

isEmpty

判断Map是否是空,内部实现size==0

get

返回key对应的value

put

如果Map中有key对应的值,返回被覆盖的值,否则返回null

containsKey

是否包含某个Key

containsValue

是否包含某个value

keySet

返回键集合

values

返回值集合

entrySet

返回键值集合,遍历Map常用的方法,推荐

二、HashMap

HashMap是Map接口最重要的一个实现,也是实际使用中用的最多的一个实现。HashMap的底层实现是数组加链表的方式(JDK8之后链表会在元素到达一定数目转为红黑树结构)。元素key的hash值决定了元素在数组中的位置,多个hash值相同的key会在数组的同一个位置形成链表。

2.1 hashMap的几个关键参数

initialCapacity:hahsmap的初始容量,底层数组的初始长度。

loadFactor:负载因子,用于扩容时机判断。

threshold:hashMap的实际承载容量,通过initialCapacity*loadFactor计算得到。

2.2 hashmap的元素结构

前面已经提到,Map中的元素是键值对的形式存在的,在Hashmap中,是使用了一个继承自Entry<K,V>的一个内部类Node<K,V>实现的,根据命名能看出,这是一个用来实现链表元素的数据结构,Node<K,V>的参数有:

int hash;key的hash值

K key;key

V value;value

Node<K,V> next;链表中的下一个元素

引申:这里引申出一个编程经常讲到的问题,为什么提倡使用entrySet方法遍历Map而不是使用keySet遍历。原因是Map中的Key和Value本身就是存储在同一个Node对象中的,获取KeySet和获取EntrySet的时间复杂度是一致的。而且获取到EntrySet之后可以直接获取到key和value,不需要再单独通过key获取value。减少了查询的复杂度。

2.3 Hash值的计算

Hash值的计算是调用key的hashCode方法得到,hashCode方法是Object中定义的方法,所有的类都有hash值计算方法,

当往一个HashMap中放入元素时,首先会根据key的hash函数计算key的hash值。之后拿hash值对底层数组长度取模得到键值对在数据组中的位置。这其中jdk做了很多的优化,其中包括对原始hash函数得到的hash值进行高位和低位的异或运算,目的是为了尽量保证整个hash值的所有位都能够参与位置的计算中,此时得到的计算后的hash值再对数组长度取模。

hashmap的底层数组的长度也有玄机,hashmap在初始化数组长度时会将数组长度设置为第一个大于设置大小的2的整数次幂,而且真正对hash值取模的时候也不是实际的数组长度,而是数组长度-1.这样做的目的是,2的整数次幂-1得到的结果低位会全部是1,这样取模的结果会完全取决于hash值,数组长度这里只是限制取模后的结果不会超出数组长度,这就降低了两个hash值不同的元素取模后得到相同值的几率。

有兴趣的可以看下hashmap的源码中关于如何寻找第一个大于n的2的整数次幂的数的方法。很巧妙的设计。

2.4 Hash碰撞

根据上面对元素位置的计算过程可以知道,那当两个元素经过计算具有相同的结果时,这就是所谓的hash碰撞,典型的情况就是数组位置a上已经有了元素A,此时放入新的元素B,而B经过hash计算得到的数组位置也是a,此时就需要通过equals方法判断B和A是否相等,如果相等,就会在当前位置覆盖A,否则就会在当前位置形成链表,将B追加的链表末尾。

引申:hash值相同和equals为true,这两部分的关系?重写hashcode方法为什么要求重写equals方法?

答:通过上面的介绍很容易知道,不同对象的hash值有可能相同。但相同对象的hash值一定相同。重写hashcode方法的同时重写equals方法的目的就是为了保证后者。否则就会出现两个相同的对象经过计算在hashmap中却位于不同的位置。

2.5 HashMap的扩容

普遍认为hashmap的get方法的时间复杂度是O(1),但从上面的介绍能看出这个复杂度是不能完全保证的。理论上,hashMap的get方法的时间复杂度要大于O(1),get方法的过程是先根据key的hash值得到元素在在数组中的位置,这个数组查找的时间复杂度是O(1),如果此时数组该位置只有一个元素或者正好链表中的第一个元素和key的equals比较相等,那直接返回当前元素,这时整体时间复杂度为O(1),除此之外,就需要线性遍历数组位置上的链表。而在最不理想的情况下,所有元素都在同一个位置,那查找就变成了链表的查找,时间复杂度就是O(n)。

因此为了尽可能的保证HashMap的O(1)查询复杂度,就必须要尽可能避免元素在数组的位置上形成链表。Hashmap的做法是当元素数到达一定阈值的时候对底层数组进行扩容,超过这个阈值,元素的hash碰撞比较频繁,形成链表的可能性比较大。这个阈值就是前面提到的threshold属性,通过capacity*loadFactor等到。数组的扩容是非常耗时的操作,这里不再讲解,有兴趣的可以研究一下源码。因此在使用hashmap的时候尽量根据预判的大小指定hashmap的容量。避免扩容。

也正是因为Hashmap的上述机制,才保证了hashmap的查找复杂度是O(1)。因为本身不同对象的hash值相同的概率比较低。加上扩容机制的存在,基本避免了出现链表(至少是长链表)的情况。

引申:如果需要存放20个元素的hashMap,容量声明为多少合适?

答:为了避免扩容,需要满足n*loadFactor>20;loadFactor默认为0.75,因此得出n>26.6。而hashmap在确定数组长度时是大于容量的2的整数次幂的,也就是32。因此只需要声明大小为17-32之间的数值即可。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值