面试——HashMap

面试——HashMap

1. JDK1.7版本:

1.1 数据结构:数组+链表

在这里插入图片描述

1.2 存放

  1. 根据hash(key)确定存储在数组上的位置后,以链表的形式在该位置处存数据。此时数组该位置的链表存了多个数据,因此也称为
  2. 存放的数据是用Entry描述
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
  ...

2. JDK1.8版本:

2.1 数据结构:数组+链表+红黑树

在这里插入图片描述

2.2 存放

  1. 根据**hash(key)**确定存储在数组上的位置后,以链表的形式在该位置处存数据
  2. 存放的数据是用Node描述
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;
        }
...
  1. 链表有可能过长,所以在满足以下条件时,链表会转换成红黑树:
    • 链表长度>8 并且 数组大小>=64
    • 1.8版本:当红黑树节点个数<6时转换为链表

3. 1.8相比1.7,做了哪些优化?

  1. 插入方式升级:
    • 1.7的HashMap在添加数据的时候,采用头插法;扩容之后,获得新的index,头插法会导致链表反转,会存在遍历时死循环的情况
    • 1.8插入数据采用的则是尾插法,在扩容时会保持链表元素原先的顺序,因此不会出现链表成环的死循环问题
  2. 扩容后数据存储位置的计算方式升级:
    • 1.7:重新hash运算
    • 1.8:扩容后的位置=原位置 or 原位置 + 旧容量
  3. 存储结构的升级:
    • 1.8的HashMap加入了红黑树存储结构,查询方式性能得到了很好的提升,从原来的是O(n)到O(logn)

4. HashMap如何初始化大小的?

  1. 如果没有指定容量,则默认初始化容量为16,加载因子是0.75

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  1. 若指定了初始化容量,若大于指定容量的,最近的2的整数次方的数。比如传入是10,则会初始化容量为16

    /**
     * Returns a power of two size for the given target capacity.
     */
    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; /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    } /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

在扩容时会保持链表元素原先的顺序,因此不会出现链表成环的死循环问题。

5. HashMap怎么实现扩容的

HashMap执行扩容关系到两个参数:

  • Capacity:HashMap当前容量
  • loadFactor:负载因子(默认是0.75)
  • 1.7版本:先扩容,再插入数据。扩容时会创建一个为原数组的2倍大小的数组,然后将原数组的元素重新hash,存进新数组。
  • 1.8版本:先插入数据,再执行扩容。扩容时会创建一个为原数组的2倍大小的数组,然后将原数组的元素存进新数组。不同的是1.8使用位移操作创建2倍大小的新数组

6. 提到hash函数,你知道HashMap的hash函数是如何设计的?

  1. hash运算的目的是用来定位该数据要存放在数组的哪个位置
  • 1.7版本是这样的
//jdk1.7 相比jdk1.8, jdk1.7做了四次移位和四次异或运算,效率比1.8要低
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
  • 1.8版本如下:

用key的hashCode()与其低16位做异或运算。这个扰动函数的设计有两个原因:

  • 计算出来的hash值尽量分散,降级hash碰撞的概率
  • 用位运算做算法,更加高效
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

7. 那么为什么不能直接用key的hashCode()作为hash值,而一定要^ (h >>> 16)?

  1. 因为如果直接用key的hashCode()作为hash值,很容易发生hash碰撞。
  2. 使用扰动函数^ (h >>> 16),就是为了混淆原始哈希码的高位和低位,以此来加大低位的随机性。且低位中参杂了高位的信息,这样高位的信息也作为扰动函数的关键信息。

8. 插入数据时扩容的重新hash是怎么做的?

  1. 1.7:需要再做一次hash
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);
    }
  1. 1.8 不需要做hash,通过原方式获取存储位置
    现位置 = 原位置(或者 现位置=原位置加原长度)

9. 为什么重写equals方法后还要重写hashCode方法

https://blog.csdn.net/babycan5/article/details/88586821

  1. 重写equals和hashCode方法的目的就是根据对象的属性来进行判断对象是否相同
  2. 如果是需要比较内存地址“==”号就足够了,

10. 红黑树

  1. 红黑树是一个特殊的平衡二叉树。他在查询性能和自旋所带来的性能开销之间找到了一个平衡点。
  2. 可以牺牲平衡性,而不做多余(自旋和不自旋对查询的性能来说差距微小)的自旋带来的系统开销
  3. 特征
    • 根节点是黑色, 叶节点是不存储数据的黑色空节点
    • 任何相邻的两个节点不能同时为红色
    • 任意节点到其可到达的叶节点间包含相同数量的黑色节点
    • 是其查询的性能一定不会超过O(2(logN+1))
  4. 红黑树在插入(删除)数据时要遵循的原则
    • 根节点一定是黑色节点
    • 插入的节点一定是红色节点
    • 父节点如果是红色,则把父节点变成黑色,同时判断父节点的兄弟节点是否是红色,如果是,则一并变成黑色
    • 判断父节点的父节点是否是根节点,如果不是,则变为红色。重复上述过程,直到变成完整的红黑树。

11. HashMap在多线程使用场景下会存在线程安全问题,怎么处理?

使用Collections.synchronizedMap()创建线程安全的map集合
使用
Hashtable

使用ConcurrentHashMap(鉴于效率考虑,推荐使用ConcurrentHashMap)

12. HashMap插入数据的过程

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值