Java集合之HashMap

概要

HashMap是编程和面试中常见的数据类型。


1.HaspMap概述

HashMap是基于散列表(哈希表)的Map接口的实现,继承自AbstractMap(AbstractMap中实现了Map的许多核心功能,以减少实现Map接口的工作量)。其定义如下:

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

####什么是散列表(哈希表)?

散列表是通过关键码值(key value)直接访问的数据结构,它把关键码值映射到表中的一个位置来访问记录,以加快查找的速度。这个映射函数叫散列函数,存放记录的表则成为散列表。常见的哈希函数有直接定址法、除留余数法 、数字分析法、平方取中法等。

####哈希冲突

当我们选用哈希函数计算哈希值时,有可能不同的key会得到相同的结果,即k1≠k2,但f(k1)=f(k2),这种情况称为哈希冲突(哈希碰撞)。通过构造性能良好的哈希函数,可以减少冲突,但一般不可能完全避免冲突,当发生哈希冲突时,通常有两种方法解决冲突:

  1. 链表法:将所有关键字为同义词的结点链接在同一个单链表中。

  2. 开放定址法:当冲突发生时,使用某种探查(亦称探测)技术在散列表中寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到。

HashMap就是通过链表法构造哈希表,其底层存储结构为二维链表,即链表的数组,在数据结构中又称为“链表散列”。
HashMap底层结构

上图中左边的数组即为哈希表的主干,数组中存放的是链表的头节点(Entry)。在JDK 8 后又增加了红黑树,当链表长度大于8时将链表转化为红黑树。

####HashMap扩容

1.什么时候扩容?

HashMap的容量(capacity,即数组的长度)默认为16,加载因子(loadFactory)默认为0.75,阈值(threshold)默认为 capacity * loadFactory = 12,当向容器添加元素的时候,如果容器中当前元素个数大于等于阈值,并且当前key会发生哈希冲突时,则会自动扩容,容量翻倍。

    // 增加元素
    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);  // 容量翻倍
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

    // 扩容
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    // 根据hash值计算数组下标
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

这里扩容后的容量之所以翻倍,是因为HashMap的容量需要为2的n次幂,这是为什么呢?


观察上述的indexFor方法,我们发现,计算key的hash值对应的数组下标方法,不是常见的取余运算(h%length),而是使用了位运算。
假设当前table的length是13,二进制表示为1101(由于前二十八位均为0,这里的运算都只考虑后四位),那么length-1二进制表示为1100,此时有三个hash值分别为1、2、3的key需要计算索引值:

1的二进制为:0001
1 & (length-1) = 0001 & 1100 = 0000;
2的二进制为:0010
2 & (length-1) = 0010 & 1100 = 0000;
3的二进制为:0011
3 & (length-1) = 0011 & 1100 = 0000;

可见,它们的索引值都为0,这时就会发生哈希冲突(哈希碰撞)。更为严重的情况是,无论我们的key在哈希运算后得到的hash值是多少,在&运算后,得到的结果中最后两位总是为0!也就是说,只要数组下标x的二进制最后两位中有一个1(0001、0010、0011、0101、0110、0111、1001、1010、1011),那么这个下标x所代表的桶将一直是空的(因为计算出的下标后两位不可能为1)。这会造成严重的空间浪费,数组可以使用的位置比数组长度小了很多,极大得加了哈希碰撞的概率,这无疑会造成HashMap效率的降低。

那么,如何避免这种情况的发生呢?


为保证数据在数组上分布均匀,就必须要求indexFor()方法返回的索引二进制任何一位都不能始终为0,那么,length-1的二进制任何一位也不能为0,即lengh-1的二进制表示全部为1(有点像子网掩码255.255.255.0?),length=2^n。只有当数组长度为2的n次方时,才能保证所有的哈希桶均有被使用的可能,才能有效得减少哈希碰撞的发生。

:已知HashMap需要存储的元素数量为100,那么在使用默认加载因子的情况下,初始容量为多少合适?

:设初始容量为x,为了避免扩容,则 x * 0.75 > 100,x取整为 134,同时,x需为2的n次幂,因此,初始容量为256时最合适。

2.为什么要扩容?

HashMap扩容过程中会根据newCapacity重新计算在Entry数组中原先存在的entry的新的散列位置,因此,扩容的成本并不低,需要遍历时间复杂度为O(n)的数组,并且为其中的每个entry进行hash计算。

既然扩容的成本这么大,那么为什么还要扩容?


随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度。因此,为保证HashMap O(1)的查找复杂度,必须扩容。

在实际使用中,若已知需要存储元素的数量x,则应该合理使用HashMap的构造方法创建适合大小(初始化大小为2^n,并且不小于x)的HashMap,使得在不浪费内存的情况下, 尽量减少扩容。

2.HaspMap与HashTable

####底层实现
HashMap和HashTable都使用哈希表来存储键值对,其底层实现基本一致,不同的是,HashMap计算下标时使用&运算,而HashTable使用取余预算。

####初始化
HashTable默认的初始大小为11,之后每次扩充为原来的2n+1。HashMap默认的初始化大小为16,之后每次扩充为原来的2倍。如果在创建时给定了初始化大小,那么HashTable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小。

####线程安全
HashTable是同步的,HashMap不是,也就是说HashTable在多线程使用的情况下,不需要做额外的同步,而HashMap则不行。

####null key / null value
HashMap是支持null键和null值的,而HashTable在遇到null时,会抛出NullPointerException异常。这是因为HashMap在实现时对null做了特殊处理,将null的hashCode值定为了0。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值