HashMap面试题

1.重写equals和hashCoe的原因

hashcode方法:底层是采用c语言编写的,根据对象的内存地址转换成整数类型。

定律:

  • 如果两个对象的hashcode值相等的情况下,对象的内容值不一定相等,hash碰撞问题。
  • 如果使用equals方法比较两个对象内容值相等的情况下,则两个对象的HashCode值相等。

注意:

  • equals方法默认的情况下Object类中采用==比较对象的内存地址值是否相等,只要覆写equals,就必须覆写hashcode
  • 如果自定义对象作为Map的键,那么必须覆写hashcode和equals,因为如果不重写equals和hashcode方法的时候,如果自定义对象的内容值相等的时候,地址值还是不一样,这样两个对象还是作为新的key存到Map中,就会出现内存泄漏,导致内存溢出的问题。
  • String已经重写了hashcode和equals方法,我们可以直接使用String作为key来使用。

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

自定义对象作为key的时候,一定要重写equals和hashcode方法,保证对象key不重复创建。

3.HashMap和HashTable的区别

  • HashMap线程不安全,允许key和value为null,key为null的时候放在数组的第一个位置
  • HashTable线程安全,不允许key和value为null

4.HashMap底层实现

基于ArrayList实现

基于数组+单链表(jdk1.7)

基于数组+单链表+红黑树(jdk1.8)

hash碰撞问题

原因:hashcode值相同,内容值不等。

4.LinkedHashMap

  • 基于了双向链表来保证有序。
  • LinkedHashMap是HashMap的子类,内部采用了一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于双向链表中,LinkedHashMap支持两种顺序 插入顺序、访问顺序
  • 插入顺序:先添加的在前面,后添加的在后面,修改操作不影响顺序
  • 访问顺序:执行get/put操作后,其对应的键值对会移动到链表末尾,所以末尾是最近访问的,越是前面是最久没有访问的
  • 其中参数accessOrder是用来指定是否按访问顺序,如果为true,就是按照访问顺序,false是按照新增顺序,默认是false按照新增顺序。
       LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);
       linkedHashMap.put("a", "a");
       linkedHashMap.put("b", "b");
       linkedHashMap.put("c", "c");
       linkedHashMap.get("b");
       linkedHashMap.get("a");
       linkedHashMap.forEach((k, v) -> {
           System.out.println(k + "=>" + v);
       });
基于LinkedHashMap实现LRU淘汰策略
package com.mayikt;

import java.util.LinkedHashMap;
import java.util.Map;

/**
* @Description:
* @Author: ChenYi
* @Date: 2021/03/09 23:50
**/

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
   private int capacity;

   public LRUCache(int capacity) {
       super(capacity, 0.75f, true);
       this.capacity = capacity;
   }

   @Override
   protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
       return size() > capacity;
   }

   public static void main(String[] args) {
       LRUCache<String, String> lruCache = new LRUCache<>(3);
       lruCache.put("a", "a");
       lruCache.put("b", "b");
       lruCache.put("c", "c");
       lruCache.forEach((k, v) -> System.out.println(k + "=>" + v));
       lruCache.put("d", "d");
       lruCache.forEach((k, v) -> System.out.println(k + "=>" + v));
       lruCache.put("e", "e");
       lruCache.forEach((k, v) -> System.out.println(k + "=>" + v));
   }
}

5.HashMap(1.8)如何降低hash冲突

hash值的计算函数

key == null? 0 : (h = key.hashCode()) ^ (h >>> 16);

6.减少index冲突

数组长度减1,因为数组的长度都是2的幂次方,为偶数,通过减1变成奇数再与哈希值进行&运算,能够减少index冲突

 (n - 1) & hash

7.HashMap的核心参数

初始容量,默认为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
最大的容量
static final int MAXIMUM_CAPACITY = 1 << 30
默认的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
链表转成红黑树时链表的长度阈值,链表长度大于等于8
static final int TREEIFY_THRESHOLD = 8;
红黑树转成链表的时候链表的长度
static final int UNTREEIFY_THRESHOLD = 6;
链表转成红黑树时,数组的容量最小值,也就是说链表要转成红黑树需要,需要数组的长度大于64且链表的长度大于8
static final int MIN_TREEIFY_CAPACITY = 64;
底层采用单向链表
final int hash;
final K key;
V value;
Node<K,V> next;
需要将key的hash保存起来是为了下次扩容的时候,能够计算该key在新的table中index值。
table数组 类型:单向链表
transient Node<K,V>[] table;
数组的实际容量大小
transient int size;
transient不能给被序列化
遍历我们的HashMap集合防止多线程篡改我们的数据
transient int modCount;
加载因子
final float loadFactor;

8.HashMap的put方法

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
 //tab表示数组,p表示当前索引对应的数组中的节点,n表示数组的容量,i表示当前key对应的索引位置
        Node<K,V>[] tab; Node<K,V> p; int n, i;
         //如果是第一次添加的时候就进行扩容,默认为16
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
 //判断当前索引对应的数组位置是否为空,如果为空则直接赋值就可以
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
           //当前索引对应的数组是不为空的,有值,发生了index冲突
            Node<K,V> e; K k;
            //如果要添加的key值是跟当前索引对应的节点的key是一样的,则直接修改value值就可以了
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
            //当前节点属于红黑树
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            //当前节点属于链表
                for (int binCount = 0; ; ++binCount) {
                //当前是否为链表的尾节点,如果是则直接赋值
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //判断当前链表的长度是否大于8,如果是大于8,则进行链表转成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //链表转成红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    //判断要添加的key是否跟链表中的key是否相同,有则修改value值
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果key相同则修改对应的value值即可
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //新增节点才会增加,如果是修改节点的时候没有走到这一步
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //如果当前节点的链表大于8,但是数组的容量小于64则进行扩容而已,还没有进行链表转成红黑树
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

9.modCount参数的作用

modeCount是在新增节点才会进行添加,如果是修改节点是没有增加的,HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程新增了Map,那么将抛出ConcurrentModificationException,这就是所谓的fail-fast策略,该策略在源码中的实现是通过modeCount,修改次数来处理的

    public void forEach(BiConsumer<? super K, ? super V> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.key, e.value);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }

10.扩容原理

1.7扩容存在死循环原理分析图

链接: https://www.processon.com/diagraming/604a3cd31e08537ac5bad2ab.

1.8扩容解决死循环问题

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
        //存在值的扩容,遍历整个数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //用一个临时变量接收每个链表,然后把旧列表置空
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //如果当前链表只有一个节点,那直接放在新的列表的索引位置即可
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                        //如果当前节点是红黑树
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        //当前节点为链表
                    else { // preserve order
                    //低位链表
                        Node<K,V> loHead = null, loTail = null;
                        //高位链表,采用两个链表是为了降低链表的长度
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                        //采用尾插法插入数据
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //低位链表不为空则赋值到新的table中
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //高位链表不为空则赋值到新的table中
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

11.加载因子是0.75的原因

  • 如果加载因子过大,阈值比较大,数组的空间利用率高,但是发生index冲突大
  • 如果加载因子过小,阈值比较小,则扩容比较快,数组的空间利用率低,发生index的概率低

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

核心是要减少数组的扩容次数,所以如果确定要存储1万条数据的情况下,则可以直接初始化数组的大小到指定的大小,不进行扩容,这样效率就是最高的
初始化的容量大小=(存放的key数量/加载因子)+1

为啥HashMap中数组的容量是2的整数次幂

减少哈希冲突,均匀分布元素
参考:蚂蚁课堂

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值