HashMap 学习 及面试小问题


HashMap基础

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。每一个键值对也叫做 Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。

HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。

HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。

HashMap 的实例有两个参数影响其性能:“初始容量 capacity” 和 “加载因子 loadfactor”。容量 是哈希表中存储的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。

初始容量既数组长度设定是2的n次幂,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。 默认长度是16是因为,统计得出16可以满足很多的使用场景,如需要自己设计长度的话使用 HashMap(int capacity) 构造方法手动定义即可。

通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。

hashmap的resize

当hashmap中的数组元素越来越多的时候,碰撞的几率也就越来越高(因为初始数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个通用的操作。

需要注意的是这里的元素个数是数组存储的元素个数,也就是桶的容量。

而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize

那么hashmap什么时候进行扩容呢?
  当hashmap中的数组元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中数组元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.751000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

HashMap数组每一个元素的初始值都是Null。
在这里插入图片描述

HashMap共有4个构造函数,如下:

// 默认构造函数。
HashMap()

// 指定“容量大小”的构造函数
HashMap(int capacity)

// 指定“容量大小”和“加载因子”的构造函数
HashMap(int capacity, float loadFactor)

// 包含“子Map”的构造函数
HashMap(Map<? extends K, ? extends V> map)

HashMap的API

Modifier and TypeMethod and Description
voidclear()
Removes all of the mappings from this map.
Objectclone()
Returns a shallow copy of this HashMap instance: the keys and values themselves are not cloned.
Vcompute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)
Attempts to compute a mapping for the specified key and its current mapped value (or null if there is no current mapping).
VcomputeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)
If the specified key is not already associated with a value (or is mapped to null), attempts to compute its value using the given mapping function and enters it into this map unless null.
VcomputeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)
If the value for the specified key is present and non-null, attempts to compute a new mapping given the key and its current mapped value.
booleancontainsKey(Object key)
Returns true if this map contains a mapping for the specified key.
booleancontainsValue(Object value)
Returns true if this map maps one or more keys to the specified value.
Set<Map.Entry<K,V>>entrySet()
Returns a Set view of the mappings contained in this map.
voidforEach(BiConsumer<? super K,? super V> action)
Performs the given action for each entry in this map until all entries have been processed or the action throws an exception.
Vget(Object key)
Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key.
VgetOrDefault(Object key, V defaultValue)
Returns the value to which the specified key is mapped, or defaultValue if this map contains no mapping for the key.
booleanisEmpty()
Returns true if this map contains no key-value mappings.
SetkeySet()
Returns a Set view of the keys contained in this map.
Vmerge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)
If the specified key is not already associated with a value or is associated with null, associates it with the given non-null value.
Vput(K key, V value)
Associates the specified value with the specified key in this map.
voidputAll(Map<? extends K,? extends V> m)
Copies all of the mappings from the specified map to this map.
VputIfAbsent(K key, V value)
If the specified key is not already associated with a value (or is mapped to null) associates it with the given value and returns null, else returns the current value.
Vremove(Object key)
Removes the mapping for the specified key from this map if present.
booleanremove(Object key, Object value)
Removes the entry for the specified key only if it is currently mapped to the specified value.
Vreplace(K key, V value)
Replaces the entry for the specified key only if it is currently mapped to some value.
booleanreplace(K key, V oldValue, V newValue)
Replaces the entry for the specified key only if currently mapped to the specified value.
voidreplaceAll(BiFunction<? super K,? super V,? extends V> function)
Replaces each entry’s value with the result of invoking the given function on that entry until all entries have been processed or the function throws an exception.
intsize()
Returns the number of key-value mappings in this map.
Collectionvalues()
Returns a Collection view of the values contained in this map.

常用API及解析

//clear() 的作用是清空HashMap
void  clear()
//复制得到新的对象
Object  clone()
//containsKey() 的作用是判断HashMap是否包含key
boolean  containsKey(Object key)
//containsValue() 的作用是判断HashMap是否包含“值为value”的元素
boolean   containsValue(Object value)
//entrySet()的作用是返回“HashMap中所有Entry的集合”,它是一个集合
Set<Entry<K, V>>  entrySet()
//get() 的作用是获取key对应的value
V   get(Object key)
//isEmpty()判断是不是为空
boolean  isEmpty()
//keySet()与entrySet()类似
Set<K>   keySet()
//put() 的作用是对外提供接口,让HashMap对象可以通过put()将“key-value”添加到HashMap中
V   put(K key, V value)
//putAll() 的作用是将"m"的全部元素都添加到HashMap中
void   putAll(Map<? extends K, ? extends V> map)
//remove() 的作用是删除“键为key”元素
V   remove(Object key)
//size()返回HashMap的长度
int   size()
//values()与entrySet()类似
Collection<V>   values()

对于HashMap,我们最常使用的是两个方法:GetPut

1.Put方法的原理

调用Put方法的时候发生了什么呢?

比如调用 hashMap.put(“apple”, 0) ,插入一个Key为“apple"的元素。这时候我们需要利用一个哈希函数来确定Entry的插入位置(index):

index = Hash(“apple”)

假定最后计算出的index是2,那么结果如下:
在这里插入图片描述
但是,因为HashMap的长度是有限的,当插入的Entry越来越多时,再完美的Hash函数也难免会出现index冲突的情况。比如下面这样:
在这里插入图片描述
这时候该怎么办呢?我们可以利用链表来解决。

HashMap数组的每一个元素不止是一个Entry对象,也是一个链表的头节点。每一个Entry对象通过Next指针指向它的下一个Entry节点。当新来的Entry映射到冲突的数组位置时,只需要插入到对应的链表即可:

在这里插入图片描述

需要注意的是,新来的Entry节点插入链表时,使用的是“头插法”。

注意:
在JDK1.8 之后,Put 过程做出了一些修改,如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表

2.Get方法的原理

使用Get方法根据Key来查找Value的时候,发生了什么呢?

首先会把输入的Key做一次Hash映射,得到对应的index:

index = Hash(“apple”)

由于刚才所说的Hash冲突,同一个位置有可能匹配到多个Entry,这时候就需要顺着对应链表的头节点,一个一个向下来查找。假设我们要查找的Key是“apple”:

在这里插入图片描述

第一步,我们查看的是头节点Entry6,Entry6的Key是banana,显然不是我们要找的结果。

第二步,我们查看的是Next节点Entry1,Entry1的Key是apple,正是我们要找的结果。

之所以把Entry6放在头节点,是因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。

HashMap的初始长度是16,每一次自动扩展或者是手动初始化时,长度必须是2的幂。

对于Hash()算法,我们在数据结构中学的可能是很简单的取余操作,但是在实际操作中,效率很低。为了实现高效的Hash算法,HashMap的发明者采用了位运算的方式。

结论就是16是符合要求的长度,同时长度适中,需要扩展空间的时候较少,不会浪费空间,下面的分析不关心的可以跳过


采用位运算的原因

如何进行位运算呢?有如下的公式(Length是HashMap的长度):

index = HashCode(Key) & (Length - 1)

下面我们以值为“book”的Key来演示整个过程:

  1. 计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。
  2. 假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。
  3. 把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。

可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。

假设HashMap的长度是10,重复刚才的运算步骤:
在这里插入图片描述
单独看这个结果,表面上并没有问题。我们再来尝试一个新的HashCode 101110001110101110 1011 :
在这里插入图片描述
还有1111,也是同样的效果

虽然HashCode的倒数第二第三位从0变成了1,但是运算的结果都是1001。也就是说,当HashMap长度为10的时候,有些index结果的出现几率会更大,而有些index结果永远不会出现(比如0111)!

这样,显然不符合Hash算法均匀分布的原则。

因为是与运算,当你的监测位有0时,则与运算一定是0,所以,只有当检测位全为1时,才能最大限度的区分开(在我看来采用位运算单纯是因为位运算在计算机中比取模的效率高,对于分布的是否均匀并没有增益,还是要看输入的是否均匀

反观长度16或者其他2的幂,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。


还有些HashMap的面试题可以参考下面的这篇博客。

我 jio着 这是重点!!!!

HashMap面试夺命连环call

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值