文章目录
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 Type | Method and Description |
---|---|
void | clear() Removes all of the mappings from this map. |
Object | clone() Returns a shallow copy of this HashMap instance: the keys and values themselves are not cloned. |
V | compute(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). |
V | computeIfAbsent(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. |
V | computeIfPresent(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. |
boolean | containsKey(Object key) Returns true if this map contains a mapping for the specified key. |
boolean | containsValue(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. |
void | forEach(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. |
V | get(Object key) Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key. |
V | getOrDefault(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. |
boolean | isEmpty() Returns true if this map contains no key-value mappings. |
Set | keySet() Returns a Set view of the keys contained in this map. |
V | merge(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. |
V | put(K key, V value) Associates the specified value with the specified key in this map. |
void | putAll(Map<? extends K,? extends V> m) Copies all of the mappings from the specified map to this map. |
V | putIfAbsent(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. |
V | remove(Object key) Removes the mapping for the specified key from this map if present. |
boolean | remove(Object key, Object value) Removes the entry for the specified key only if it is currently mapped to the specified value. |
V | replace(K key, V value) Replaces the entry for the specified key only if it is currently mapped to some value. |
boolean | replace(K key, V oldValue, V newValue) Replaces the entry for the specified key only if currently mapped to the specified value. |
void | replaceAll(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. |
int | size() Returns the number of key-value mappings in this map. |
Collection | values() 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,我们最常使用的是两个方法:Get 和 Put。
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来演示整个过程:
- 计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。
- 假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。
- 把以上两个结果做与运算,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着 这是重点!!!!