129面试必刷:看完这篇,再也不怕面试被问HashMap了~

 

总所周知 HashMap 是面试中经常问到的一个知识点,也是判断一个候选人基础是否扎实的标准之一,因为通过 HashMap 可以引出很多知识点,比如数据结构(数组、链表、红黑树)、equals 和 hashcode 方法。

除此之外还可以引出线程安全的问题,HashMap 是我在初学阶段学到的设计的最为巧妙的集合,里面有很多细节以及优化技巧都值得我们深入学习,话不多说先看看相关的面试题:

•  默认大小、负载因子以及扩容倍数是多少

•  底层数据结构

•  如何处理 hash 冲突的

•  如何计算一个 key 的 hash 值

•  数组长度为什么是 2 的幂次方

•  扩容、查找过程

如果上面的都能回答出来的话你就不需要看这篇文章了,那么开始进入正文。

数据结构

•  在 JDK1.8 中,HashMap 是由数组+链表+红黑树构成

•  当一个值中要存储到 HashMap 中的时候会根据 Key 的值来计算出他的 hash,通过 hash 值来确认存放到数组中的位置,如果发生 hash 冲突就以链表的形式存储,当链表过长的话,HashMap 会把这个链表转换成红黑树来存储。

在看源码之前我们需要先看看一些基本属性

//默认初始容量为16  
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  
//默认负载因子为0.75  
static final float DEFAULT_LOAD_FACTOR = 0.75f;  
//Hash数组(在resize()中初始化)  
transient Node<K,V>[] table;  
//元素个数  
transient int size;  
//容量阈值(元素个数超过该值会自动扩容)  
int threshold;

table 数组里面存放的是 Node 对象,Node 是 HashMap 的一个内部类,用来表示一个 key-value,源码如下:

static class Node<K,V> implements Map.Entry<K,V> {  
    final int hash;  
    final K key;  
    V value;  
    Node<K,V> next;  

    Node(int hash, K key, V value, Node<K,V> next) {  
        this.hash = hash;  
        this.key = key;  
        this.value = value;  
        this.next = next;  
    }  

    public final K getKey()        { return key; }  
    public final V getValue()      { return value; }  
    public final String toString() { return key + "=" + value; }  
    public final int hashCode() {  
        return Objects.hashCode(key) ^ Objects.hashCode(value);//^表示相同返回0,不同返回1  
        //Objects.hashCode(o)————>return o != null ? o.hashCode() : 0;  
    }  

    public final V setValue(V newValue) {  
        V oldValue = value;  
        value = newValue;  
        return oldValue;  
    }  

    public final boolean equals(Object o) {  
        if (o == this)  
            return true;  
        if (o instanceof Map.Entry) {  
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;  
            //Objects.equals(1,b)————> return (a == b) || (a != null && a.equals(b));  
            if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue()))  
                return true;  
        }  
        return false;  
    }  
}

总结:

•  默认初始容量为 16,默认负载因子为 0.75

•  threshold = 数组长度 * loadFactor,当元素个数超过threshold(容量阈值)时,HashMap 会进行扩容操作

•  table 数组中存放指向链表的引用

这里需要注意的一点是 table 数组并不是在构造方法里面初始化的,它是在 resize(扩容)方法里进行初始化的。

table 数组长度永远为 2 的幂次方

总所周知,HashMap 数组长度永远为 2 的幂次方(指的是 table 数组的大小),那你有想过为什么吗?

首先我们需要知道 HashMap 是通过一个名为 tableSizeFor 的方法来确保 HashMap 数组长度永远为2的幂次方的,源码如下:

/*找到大于或等于 cap 的最小2的幂,用来做容量阈值*/  
static final int tableSizeFor(int cap) {  
    int n = cap - 1;  
    n |= n >>> 1;  
    n |= n >>> 2;  
    n |= n >>> 4;  
    n |= n >>> 8;  
    n |= n >>> 16;  
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;  
}

tableSizeFor 的功能(不考虑大于最大容量的情况)是返回大于等于输入参数且最近的 2 的整数次幂的数。比如 10,则返回 16。

该算法让最高位的 1 后面的位全变为 1。最后再让结果 n+1,即得到了 2 的整数次幂的值了。

让 cap-1 再赋值给 n 的目的是另找到的目标值大于或等于原值。例如二进制 1000,十进制数值为 8。如果不对它减1而直接操作,将得到答案 10000,即 16。显然不是结果。减 1 后二进制为 111,再进行操作则会得到原来的数值 1000,即 8。通过一系列位运算大大提高效率。

那在什么地方会用到 tableSizeFor 方法呢?

答案就是在构造方法里面调用该方法来设置 threshold,也就是容量阈值。

这里你可能又会有一个疑问:为什么要设置为 threshold 呢?

因为在扩容方法里第一次初始化 table 数组时会将 threshold 设置数组的长度,后续在讲扩容方法时再介绍。

/*传入初始容量和负载因子*/  
public HashMap(int initialCapacity, float loadFactor) {  

    if (initialCapacity < 0)  
        throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);  
    if (initialCapacity > MAXIMUM_CAPACITY)  
        initialCapacity = MAXIMUM_CAPACITY;  
    if (loadFactor <= 0 || Float.isNaN(loadFactor))  
        throw new IllegalArgumentException("Illegal load factor: " +loadFactor);  

    this.loadFactor = loadFactor;  
    this.threshold = tableSizeFor(initialCapacity);  
}

那么为什么要把数组长度设计为 2 的幂次方呢?

我个人觉得这样设计有以下几个好处:

1. 当数组长度为 2 的幂次方时,可以使用位运算来计算元素在数组中的下标

HashMap 是通过 index=hash&(table.length-1) 这条公式来计算元素在 table 数组中存放的下标,就是把元素的 hash

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值