HashMap与ConcurrentHashMap面试题

HashMap

1.为什么重写Equals还要重写HashCode方法

规定:

1.两个对象的Hashcode值相等,但是两个对象的内容值不一定相等;—Hash冲突的问题

2.两个对象的值Equals比较相等的情况下,则两个对象的Hashcode值一定相等;

3.重写Equals不重写HashCode容易导致内存泄漏

2.HashMap如何避免内存泄漏问题

  1. 保证作为HashMap 的Key 的对象是不可变的。也就是使用只读多对象来当作Key

  2. 如果要使用一个类当作HashMap的key,同时也要修改它的内容。重写Equals必须重写HashCode

3.HashMap1.7底层是如何实现的

数组+链表

4.HashMapKey为null存放在什么位置

数组为0的位置

5.HashMap如何解决Hash冲突问题

HashMap底层是通过链表来解决hash冲突的

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
((p = tab[i = (n - 1) & hash])

1、保证不会发生数组越界
首先我们要知道的是,在HashMap,数组的长度按规定一定是2的幂。因此,数组的长度的二进制形式是:10000…000, 1后面有偶数个0。 那么,length - 1 的二进制形式就是01111.111, 0后面有偶数个1。最高位是0, 和hash值相“与”,结果值一定不会比数组的长度值大,因此也就不会发生数组越界。一个哈希值是8,二进制是1000,一个哈希值是9,二进制是1001。和1111“与”运算后,结果分别是1000和1001,它们被分配在了数组的不同位置,这样,哈希的分布非常均匀。

6.HashMap底层采用单链表还是双链表

单向链表

7.时间复杂度O(1)、O(N)、O(Logn)区别

时间复杂度为O(n) 从头查询到尾部,查询多次
时间复杂度为O(1) 查询一次 比如根据数组下标查询
时间复杂度为O(logn) 平方查询 比如红黑树

8.HashMap根据key查询的时间复杂度

时间复杂度 o(n) 从头遍历到尾部;
时间复杂度o(1) 直接查询一次;

9.HashMap8扩容底层原理

将原来的链表拆分两个链表存放; 低位还是存放原来index位置 高位存放index=j+原来长度
我们在扩充HashMap的时候,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”

10.HashMap底层是有序存放的吗

无序

11.LinkedHashMap 和 TreeMap底层如何实现有序

LinkedHashMap基于双向链表实现
HashMap基本单向链表实现
TreeMap基于红黑树底层实现

12.HashMap底层如何降低Hash冲突概率

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
((p = tab[i = (n - 1) & hash])

13.谈一下HashMap中hash函数是怎么实现的?还有哪些hash函数的实现方式?

(1)hash函数的实现:对key的hashCode做hash操作,与高16位做异或运算。
(2)其他hash函数实现方式:还有平方取中法,除留余数法,伪随机数法。

14.为什么不直接将key作为哈希值而是与高16位做异或运算?

因为如果直接使用key作为哈希值的话,当产生key的哈希值高位变化大低位变化小的情况时,会发生严重的哈希冲突,可能需要扩容或转化为红黑树结构,影响HashMap的性能。而如果和高16位做异或运算,就能同时用上高十六位和第十六位,增加了随机性,减少哈希冲突的次数。

15.HashMap如何存放1万条key效率最高

参考阿里巴巴官方手册:
在这里插入图片描述(需要存储的元素个数 / 负载因子) + 1
10000/0.75+1=13334
正常如果存放1万个key的情况下 大概扩容10次=16384

16.modCount的作用

我们知道 java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对HashMap 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 Map:

17.HashMap在这里插入代码片1.8如何避免多线程扩容死循环问题

 /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
    	//新数组长度
        int newCapacity = newTable.length;
        //table为旧表,此处为遍历Hash表,落在桶上的所有节点!也就是链表头结点!
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;//此处为多线程下,发生问题的点!非常重要,切记!
                if (rehash) {//重新进行Hash计算,这个if方法不用管!
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);//重新计算这个节点放在新newTable表的位子!
                //这3段代码, 通过下面的图来解释!
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

扩容形成环形列表,get时导致死旋环。

18.什么情况下,需要从红黑树转换成链表存放?

JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

当链表的长度>=8且数组长度>=64时,会把链表转化成红黑树。

当链表长度>=8,但数组长度<64时,会优先进行扩容,而不是转化成红黑树。

当红黑树节点数<=6,自动转化成链表。

18.为什么加载因子是0.75而不是1

产生背景:减少Hash冲突index的概率;
查询效率与空间问题;

简单推断的情况下,提前做扩容:
1.如果加载因子越大,空间利用率比较高,有可能冲突概率越大;
2.如果加载因子越小,有可能冲突概率比较小,空间利用率不高;
空间和时间上平衡点:0.75
统计学概率:泊松分布是统计学和概率学常见的离散概率分布

19.ConcurrentHashMap底层实现的原理

https://blog.youkuaiyun.com/u014401141/article/details/81258753

20.hashMap中put是如何实现的?

分为三大步,一是计算出元素在数组中的存储索引位置,二是将数据保存到table数组中,三是修改某些成员变量的值并根据情况判断是否扩容。

①首先要根据键值key计算出哈希值,然后通过位运算实现哈希值的取模,以得到元素在数组中的索引位置。
②如果是第一次插入数据(在jdk1.8之后),还要先通过resize()方法对数组进行初始化。
①判断将要存储的数组位置是否已经存在元素,不存在则说明没有发生哈希碰撞,直接将元素添加到数组中。
②存在则说明发生了哈希碰撞,需要依次进行以下判断:
a.是否是相同的key值,是则覆盖掉旧值替换为新值;
b.是否是红黑树结构,是则直接插入;
c.以上判断均不符合,说明是链表结构,则遍历链表在尾部进行插入,在这个过程中如果检查到相同key值元素则直接替换旧值,如果插入元素后链表长度大于阈值则尝试转化为红黑树。
代表元素个数的size加一,代表修改次数的modCount加一,size超过临界值则进行扩容。

关键词:计算索引位置,保存数据到数组(依次判断:数组索引位置有无元素;key值是否相同;节点类型),扩容。
在这里插入图片描述
(图来自https://www.cnblogs.com/LiaHon/p/11149644.html)

21.HashMap中什么时候需要进行扩容,扩容resize()又是如何实现的?

(1)扩容时机:

初始化数组(jdk1.8之后,默认在第一次插入数据的时候才进行数组初始化,初始化是通过调用扩容方法实现的);
当元素个数超过临界值(临界值=装载因子*数组容量);
当链表长度超过默认阈值8,尝试转化为红黑树,但发现数组长度不到64时。

(2)扩容机制:

如果数组未初始化过,会将数组的容量和装载因子都设置为默认值,并将数组创建出来。

如果数组初始化过,扩容会分配一个新的数组,新的数组长度翻倍,然后遍历整个老结构将元素重新哈希映射到新的数组里。

HashMap在进行扩容时,使用的重新哈希的方式非常巧妙,因为每次扩容都是翻倍,与原来计算的 (数组长度-1)&hash 的结果相比,只是多了一个bit位,所以结点要么就在原来的位置,要么就被分配到"原位置+旧容量"这个位置。

22.Arraylist集合源码解读

在这里插入图片描述

Arraylist底层是基于数组实现

 System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);

Object src : 原数组
int srcPos : 从元数据的起始位置开始
Object dest : 目标数组
int destPos : 目标数组的开始起始位置
int length : 要copy的数组的长度

/**
 * @ClassName MayiktArraylist
 * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com
 * @Version V1.0
 **/
public class MayiktArraylist<T> {
    /**
     * 存放数据元素
     */
    private Object[] elementData;
    /**
     * 记录存放的个数
     */
    private int size = 0;
    /**
     * 默认容量为10
     */
    private static final int DEFAULT_CAPACITY = 10;


    public void add(T t) {
        if (elementData == null) {
            elementData = new Object[10];
        }
        // 判断是否需要扩容
        if ((size + 1) - elementData.length > 0) {
            // 原来容量10
            int oldCapacity = elementData.length;
            // 新的容量 10+5;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            // 扩容
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
        elementData[size++] = t;
    }

    public T get(int index) {
        return (T) elementData[index];
    }

    public boolean remove(T value) {
        for (int i = 0; i < size; i++) {
            T oldValue = (T) elementData[i];
            if (oldValue.equals(value)) {
                int numMoved = size - i - 1;
                if (numMoved > 0)
                    //remove
                    System.arraycopy(elementData, i + 1, elementData, i,
                            numMoved);
                elementData[--size] = null;
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        MayiktArraylist<String> arraylist = new MayiktArraylist<>();
        for (int i = 0; i < 100; i++) {
            arraylist.add("mayikt" + i);
        }
        arraylist.remove("mayikt2");
//        arraylist.add("mayikt11");
        for (int i = 0; i < arraylist.size; i++) {
            System.out.println(arraylist.get(i));
        }
//
//        System.out.println(arraylist.get(0));
    }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值