JDK1.8中hashmap的面试题,这一个文章都帮你解决了

1. hashmap的面试题

(google搜索hashmap关键字取前三页的结果)

摘自

https://blog.csdn.net/u012512634/article/details/72735183

https://cloud.tencent.com/developer/article/1508095

https://www.jianshu.com/p/bf703c34072b

https://cloud.tencent.com/developer/article/1508095

https://www.jb51.net/it/713515.html

1.1 HashMap特性

1.2 HashMap的原理,内部数据结构?

1.3 讲一下 HashMap 中 put 方法过程?

1.4 get()方法的工作原理?

1.5 HashMap中hash函数怎么是是实现的?还有哪些 hash 的实现方式?

1.6 HashMap 怎样解决冲突?
1.6.1 扩展问题1:当两个对象的hashcode相同会发生什么?

1.6.2 扩展问题2:抛开 HashMap,hash 冲突有那些解决办法?

1.7 如果两个键的hashcode相同,你如何获取值对象?

1.8 针对 HashMap 中某个 Entry 链太长,查找的时间复杂度可能达到 O(n),怎么优化?

1.9 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

1.10 为什么String, Interger这样的类适合作为键?

1.10.1 可以用自定义对象作为键吗?

1.11.多线程环境下,重新调整HashMap大小存在什么问题?

1.12. 什么是HashSet?

1.13.HashSet与HashMap的区别?

1.14.传统hashMap的缺点为什么引入红黑树?

2、HashMap与HashTable区别

2.2 能否让HashMap同步?
  HashMap可以通过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap);

2.2.1.ConcurrentHashMap的并发机制?

2.把面试题分类

2.1 代码原理,没读过源码你不知道

1.1 HashMap特性

1.2 HashMap的原理,内部数据结构?

1.3 讲一下 HashMap 中 put 方法过程?

1.4 get()方法的工作原理?
1.5 HashMap中hash函数怎么是是实现的?还有哪些 hash 的实现方式?
1.6 HashMap 怎样解决冲突?
1.6.1 扩展问题1:当两个对象的hashcode相同会发生什么?

1.6.2 扩展问题2:抛开 HashMap,hash 冲突有那些解决办法?
1.7 如果两个键的hashcode相同,你如何获取值对象?

1.8 针对 HashMap 中某个 Entry 链太长,查找的时间复杂度可能达到 O(n),怎么优化?

1.9 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

1.10 为什么String, Interger这样的类适合作为键?

1.10.1 可以用自定义对象作为键吗?

1.11.多线程环境下,重新调整HashMap大小存在什么问题?

1.14.传统hashMap的缺点为什么引入红黑树?

2.2 框架对比,没做过技术调研你不知道

1.12. 什么是HashSet?

1.13.HashSet与HashMap的区别?

2、HashMap与HashTable区别

2.2 能否让HashMap同步?
  HashMap可以通过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap);

2.2.1.ConcurrentHashMap的并发机制?

3.开始逐个击破。

3.1代码的原理的解答

我们先来写一个测试程序

public class HashMapTest {
    @Test
    public  void testHashMap() {
        Map map = new HashMap();
        map.put(1,2);
        printMap(map);
        Integer num = (Integer) map.get(1);
        System.out.println(num);
        map.remove(1);
        printMap(map);
        /**
         * 输出
         * 1=2
         * 2
         * 
         *
         */
    }

    private static void printMap(Map map) {
        Iterator iterator = map.entrySet().iterator();

        while ( iterator.hasNext()){
            Map.Entry e = (Map.Entry) iterator.next();
            System.out.println(e);
        }
    }

}

我们将根据测试案例回答这一系列问题

1.1 HashMap特性

导语:了解是什么,对于使用他来说非常重要。

回答特性之前,我们得知道特性是特征的意思,特征就是靠什么来识别他。靠什么识别有几个关键字,一个是外在,一个是内在。

1.外在 使用者看来。使用者看来比较看重1.性能;性能包括 单线程和多线程 下使用,hashmap线程不安全。2.是简单,是put(“a”,“b”);保存的是一个键值对。3.容错,put(null,null)依然可以通过。

2.内在是 hashmap使用JDK1.8 hash算法+链表+红黑树作为存储数据,hash算法是一种类似索引的算法。能达到比较高的速度,但是碰到hash冲突时会有所减慢。而hash冲突JDK1.8的解决方案是拉链法+红黑树,下面会阐述。

1.2 HashMap的原理,内部数据结构?

导语:了解数据结构,就好像了解了表结构,我们写业务都知道,设计好一个表,基本上就是成功的一半。而对于算法而言,也是如此。所以比较重点的考察数据结构。

hashmap的原理。回答原理你可以理解为Hashmap是怎么把你的数据存进数据结构里的和hashmap的生命周期。我们可以回答。我们put的时候,hashmap会帮我们初始化一个table(桶序列),图中蓝色,我们put(“2”,“3”)时,hashmap通过hash算法来 算我们的key值(“2”)应该要放在桶的哪里,hash算法的值是根据key的hashCode()方法和“扰乱函数”决定的。然后最终放进了桶里。

如果put(“2”,“4”);hash算法会找到旧有的数值3,并覆盖成4,如果我们经过hash算法碰撞了2,会形成链表,而链表一旦大于8,会形成红黑树。

我们使用get方法的时候,也会通过hash(key)的值,找到桶的位置,再在红黑树或者链表里查找是否存在响应的key。

内部数据结构如图所示:jdk1.8HashMap底层数据结构

JDK1.8中hashmap的面试题,这一个文章都帮你解决了_第1张图片

底层类结构

JDK1.8中hashmap的面试题,这一个文章都帮你解决了_第2张图片

下面是数据结构参数的含义。(类似表字段含义)

元素 类型 二次元素 含义
threshold 阈值。判断是否需要resize的一个参数值,(容量*负载系数)
loadFactor 负载系数。就是负载系数,默认值是0.75f
DEFAULT_INITIAL_CAPACITY 默认的初始化容量值。默认是1<<4 是16。
MAXIMUM_CAPACITY 最大的容量值。默认值是1 << 30
DEFAULT_LOAD_FACTOR 默认的负荷系数,值是0.75f
TREEIFY_THRESHOLD 树阈值。默认值是8
UNTREEIFY_THRESHOLD 不进行树化的阈值,默认值是6
Node hash Node。第一个节点是hash(key)后的值,注意后面所有的hash冲突节点的hash值都是hash(key)。
key key值
value value值
next 下一个节点
TreeNode TreeNode parent 红黑树。父亲节点
TreeNode left 左子树
TreeNode right 右子树
TreeNode prev 前驱节点
boolean red 是否是红

讲一下hashmap的构造方法?

  HashMap map = new HashMap(5_000_000); 

我们查看源码发现有参数构造函数

   public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

这里的代码的输入参数是initialCapacity(初始容量),loadFactor(加载因子),而加载因子默认值是0.75。

而输出是设置了threshold (阈值)和loadFactor(加载因子)。

  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);
    }

阈值的算法,阈值的目的是计算出 cap容量的最小2的倍数,如果是10,那么会计算出16

参考文章:stackoverflow:tableSizeFor算法的原理

 static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1; //或右移1位的算法使 最高位的"非零0 第1位"变1
        n |= n >>> 2; // 或右移2位的算法使 最高位的"非零0 第1,2位"变1
        n |= n >>> 4;// 或右移2位的算法使 最高位的"非零0 第1~4位"变1
        n |= n >>> 8;// 或右移2位的算法使 最高位的"非零0 第1~8位"变1
        n |= n >>> 16;//或右移2位的算法使 最高位的"非零0 第1~16位"变1
        //算法结束后,得到 cap 的非0位的1+2+4+8+16 = 31位变1 基本上覆盖了最大值Integer.MaxValue;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

我们来试试算法的运行

n=9;    //二进制表示1001
n= 9| (9>>>1)   //  1001 | 0100(右移一位)   结果是 1101    或右移1位的算法使 最高位的非零0首位变1
 *   // n |= n >>> 2   1101 | 0011   结果是 1111   或右移2位的算法使 最高位的非零0两位变1
 *   // n |= n >>>4   1111 | 0000   结果是1111   或右移4位的算法使 最高位的非零0 4个位变1
 *   // n |= n>>>8    1111 |0000  结果是1111    或右移8位的算法使 最高位的非零0 8个位变1
 *

所以会返回

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

n=15,n+1=16

这是一个简单的例子,当我们考虑一下他的接近最大值(最坏情况)是 MAXIMUM_CAPACITY ,你就会知道为什么要>>>16位

  11 0000 0000 0000 0000 0000 0000 0000   //初始数字
| 01 1000 0000 0000 0000 0000 0000 0000   n>>>1
---------------------------------------
  11 1000 0000 0000 0000 0000 0000 0000   //结果数字非0位首位变1
| 00 1110 0000 0000 0000 0000 0000 0000   n>>2
----------------------------------------
  11 1110 0000 0000 0000 0000 0000 0000   //结果数字非0位两位变1
| 00 0011 1110 0000 0000 0000 0000 0000   n>>4
----------------------------------------
  11 1111 1110 0000 0000 0000 0000 0000  //结果非0位4个变1
| 00 0000 0011 1111 1110 0000 0000 0000  n>>8
----------------------------------------
  11 1111 1111 1111 1110 0000 0000 0000  //结果非0位8个变1
| 00 0000 0000 0000 0011 1111 1111 1111   n>>16
-----------------------------------------
  11 1111 1111 1111 1111 1111 1111 1111  //结果非0位16个变1

以上就是tableSizeFor 的算法描述,他的核心思想是生成大于数字的最小2倍数。为什么?因为位操作效率高

以下是位操作的对比,摘自位运算和取模运算的运算效率对比

public class BitAndModulus {
    @Test
    public void bit() {
        int number = 10000 * 10;//分别取值10万、100万、1000万、1亿
        int a = 1;
        
        long start = System.currentTimeMillis();
        for(int i = number; i > 0 ; i++) {
            a &= i;
        }
        long end = System.currentTimeMillis();
        System.out.println("位运算耗时: " + (end - start));
    }
    
    @Test
    public void modulus() {
        int number = 10000 * 1000;//分别取值10万、100万、1000万、1亿
        int a = 1;
        
        long start = System.currentTimeMillis();
        for(int i = number; i > 0; i++) {
            a %= i;
        }
        long end = System.currentTimeMillis();
        System.out.println("取模运算耗时: " + (end - start));
    }
}

测试结果:(时间单位:毫秒)

计算次数     位运算    取模运算   倍数(位运算:取模运算)
  10万:       734      20489    27
  100万:       742      20544    27
  1000万:      735      20408    27
  1亿:       712     19545     27

结论

位运算确实比取模运算快得多,大约快了27倍。

无参数构造函数

 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

这两个构造方法都涉及到一个变量

DEFAULT_LOAD_FACTOR = 0.75

然而这个变量为什么是0.75?空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。参考这篇文章为什么是0.75

懒人一句话概括:因为负载因为会使用在扩容上,0.5会使数组不停的扩容,扩容需要复制内容,牺牲了空间,换取了时间,而1.0会使红黑树一直处于修补树状态,会造成时间的浪费。而取舍是 两边各一半,0.75.

1.3 讲一下 HashMap 中 put 方法过程?

回答:put是hash(key)后把hash(key)值存进hash桶里,如果hash表为空,则会触发扩容函数,如果hash桶里发生了hash冲突,则会首先存进链表,如果冲突的位数多到8位,则会转换成红黑树,如果添加后的长度过大,则会再次触发扩容函数。

以下是详细源码的详细说明

我们看到put方法的源码

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

hash(key)里的hash算法

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

采用的是一种高位异或低位的扰乱方法(注意 int 是32字节,总共32位)

摘抄大佬的文章

这里写图片描述

假设:

设想我们自己重写了一个key的hashCode方法(要注意我们是能重写hashCode方法的)

@Override
public int hashCode(){

   return  pos%n;

}

这样下来我们的hashCode基本是在 0~n里不停的重复,这样子的效率如果在大数据量前提下,会产生恶劣的hash冲突。

(h = key.hashCode()) ^ (h >>> 16)

是调用对象的hashCode()方法,如果没覆盖,就调用Object.hashCode(),返回的是int值在java里是8字节,32位的,h>>>16的话,那么就是高16位和低16位异或。这样就混合了低和高的信息,有效的防止了hashCode的冲突。

put方法的的流程如图所示。

JDK1.8中hashmap的面试题,这一个文章都帮你解决了_第3张图片

可以带着这几个流程图来阅读源码,红黑树我们不深究。

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        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 {
            Node<K,V> e; K k;
            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);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            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;
    }

resize源码

JDK1.8中hashmap的面试题,这一个文章都帮你解决了_第4张图片

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);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

注意几个重点

 if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;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;
                            }

这两个条件有点奇怪,一个是

e.hash & (newCap - 1)  我们知道她等价于 hash%length
但为什么
(e.hash & oldCap) 不用-1? 这我们就要计算一下
    
 以前要确定index的时候用的是(e.hash & oldCap-1),是取模取余,而这里用到的是(e.hash & oldCap),它有两种结果,一个是0,一个是oldCap,

比如oldCap=8,hash是3111927时,(e.hash & oldCap)的结果是0808,这样319组成新的链表,index为3;而1127组成新的链表,新分配的index为3+8;

JDK1.7中重写hash是(e.hash & newCap-1),也就是311192716取余,也是311311,和上面的结果一样,但是index为3的链表是193,index为3+8的链表是

2711,也就是说1.7中经过resize后数据的顺序变成了倒叙,而1.8没有改变顺序。也不用过分的重新hash,实在是高。
摘自:https://www.cnblogs.com/PheonixHkbxoic/p/9972412.html

JDK1.8 重新hash图解

JDK1.8中hashmap的面试题,这一个文章都帮你解决了_第5张图片

get()方法的工作原理

回答:get方法是hash(key)后判断桶是否存在该元素,如果桶里存在冲突元素的联表或者红黑树,则会继续搜索,如果依然找不到,则返回null。

 final Node<K,V> getNode(int hash, Object key) {
     //输入hash,我们知道了他的序列的桶的位置所以tab[hash%length] || tab[n-1&hash]
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
                 //如果桶序列非空   
     if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
        	//检查第一个节点是否命中hash 并且key也相等
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
         	//如果是链表或者树里也有元素的话
            if ((e = first.next) != null) {
                //如果是红黑树,则继续遍历查询
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //否则 如果是链表的话,则遍历链表看是否找到类似元素
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

1.5 HashMap中hash函数怎么是是实现的?还有哪些 hash 的实现方式?

hahs是获取对象的hashcode方法,然后使用扰乱方法用高16位和低16位异或。来得到hash值。

1.6 HashMap 怎样解决冲突?

hash的冲突使用红黑树和链表来存储冲突的元素。

1.6.1 扩展问题1:当两个对象的hashcode相同会发生什么?

会发生hash冲突。

1.6.2 扩展问题2:抛开 HashMap,hash 冲突有那些解决办法?

1.7 如果两个键的hashcode相同,你如何获取值对象?

从发生hash冲突的链表或者红黑树里寻找对应的值

1.8 针对 HashMap 中某个 Entry 链太长,查找的时间复杂度可能达到 O(n),怎么优化?

优化到红黑树,能降低到 O(logn);

1.9 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

会扩容,扩容是就容量的两倍。put方法里的resize有说明。

1.10 为什么String, Interger这样的类适合作为键?

都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

1.10.1 可以用自定义对象作为键吗?

重写hashCode()和equals()方法

重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
重写`equals()`方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;

1.11.多线程环境下,重新调整HashMap大小存在什么问题?

JDK1.7在put方法里会死循环,JDK1.8 put方法不会死循环。

其他的JDK1.7和1.8的put方法和get方法都会脏读和幻读。

1.14.传统hashMap的缺点为什么引入红黑树?

查询的极限恶劣情况下会恶化到O(n),引入红黑树会解决到O(logn)

3.2 技术调研(待更新)

JDK1.8中hashmap的面试题,这一个文章都帮你解决了_第6张图片

1.12. 什么是HashSet?

HashSet 是Set的hash算法实现。

1.13.HashSet与HashMap的区别?

2、HashMap与HashTable区别

2.2 能否让HashMap同步?
  HashMap可以通过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap);

2.2.1.ConcurrentHashMap的并发机制?

你可能感兴趣的:(自我反省)