面试官说又逮到一个不会hashmap的

简介: 一. 你知道哪些 map ?HashMap, TreeMap, ConcurrentHashMap, LinkedHashMap 二. HashMap 的特点是什么?允许 Key 和Value 为 null ,不过只能有一条记录 Key 为 null线程不安全无序数据结构是 数组+链表/红黑树(JDK1.8)三. JDK1.8 中 HashMap 为什么要引入红黑树 ?链表 查询时间复杂度 O(n) , 插入时间复杂度O(1)红黑树 查询和插入时间复杂度都是 O(lgn)可以参考下 美团技术团队 的这篇文章 tech.meituan.com/

一. 你知道哪些 map



HashMap,  TreeMap,  ConcurrentHashMap,   LinkedHashMap


二.  HashMap 的特点是什么?


  1. 允许 KeyValuenull ,不过只能有一条记录 Keynull


  1. 线程不安全


  1. 无序


  1. 数据结构是 数组+链表/红黑树(JDK1.8)


三. JDK1.8 中 HashMap 为什么要引入红黑树 ?



链表 查询时间复杂度 O(n) , 插入时间复杂度O(1)


红黑树 查询和插入时间复杂度都是 O(lgn)


可以参考下 美团技术团队  的这篇文章 tech.meituan.com/2016/06/24/…


网络异常,图片无法展示
|


四. HashMap长度为什么只能是2的倍数



  1. 计算 Hash 值时采用位运算来代替取模,能更高效地计算出元素的位置。


网络异常,图片无法展示
|


元素对应的 index 是通过下面代码赋值的 ,即 index = hash & hashmap的table长度-1


if ((p = tab[i = (n - 1) & hash]) == null)
复制代码


  1. 扩容时能更快地计算出 keyindex,提高扩容效率
    比如 map 大小为 16 ,key 为 2 所在的 index 为 2, 18 所在的 index 也是 2
    但是扩容之后变成 32 了,key 为 2 所在的 index 还是为 2, 而18 所在的 index 就变为· 2+16=18 了。


这里 4ye  就直接截取了 resize 中 链表 重新计算 index 的部分,红黑树 TreeNode 中也有类似的代码


网络异常,图片无法展示
|


五. HashMap什么时候进行扩容



当( hash 表的大小),> ( hash表的大小 * 负载因子)的值时,则进行扩容


网络异常,图片无法展示
|


网络异常,图片无法展示
|


六. 负载因子的大小



负载因子大小决定着 哈希表的扩容哈希冲突 ,每次 put **新元素 **后都要检查,看看需不需要扩容,扩容默认是原来的两倍。


调高负载因子,会增加 hash 冲突的概率 ,同样会增加耗时,扩容本身也会耗时。


七. 怎么计算 hash 值



计算 key 的 hashCode 值,然后将这个值与它的高十六位进行异或运算


这么做是为了减少hash冲突


//      >>> 无符号位右移,即不管该数的正负,都在高位补0
//      >> 表示右移,如果该数为正,则高位补0,若为负数,则高位补1
//      << 左移直接在低位补0,无正负之分
复制代码

网络异常,图片无法展示
|


验证下~


int hashCode = "Java4ye".hashCode();
System.out.println((hashCode ^ (hashCode >>> 16))&15);
// 打印出 0
hashMap.put("Java4ye","1");
复制代码


网络异常,图片无法展示
|


八. hash 冲突的解决办法


  1. 开放定址法


  1. 链地址法 (拉链法)  HashMap  采用的


  1. 再哈希法


  1. 公共溢出区域法


九. put 和 get 的实现



put 时


先计算该 key 的 hash 值,并算出它 所在的 bucket 的 index ,如果没有碰撞的话,直接放到数组中


如果碰撞了,先判断,这个 key 是不是同一个key,是的话直接覆盖


不是的话 再去判断当前是 链表 还是 红黑树,再依据不同的情况进行插入,如果 key 一样的话,会根据 onlyIfAbsent 的值 或者 原来的值是否为 null 进行替换或者保留原来的值。


如果是找到对应的key的话,会返回该旧值,不会继续往下执行


如果是增加新元素的话,最后会判断 hash 表的大小,如果 该值  大于 hash表的大小 * 负载因子,则进行扩容;最后返回null


大家可以看看 4ye 给出的这个源码,每一步都做了这个重要的注释了~ ,反正我自己看懂了 哈哈哈哈哈😝


put源码:


/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 创建Hashmap时是没有去初始化 bucket 的容量的,在put操作时才去扩容
    if ((tab = table) == null || (n = tab.length) == 0) 
        n = (tab = resize()).length;
    // 这里看看对应的 index 有没有数据,没有就直接放到这里
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
  //  有数据的话,就进入下面去判断,先判断是不是同个key,再判断属于哪种数据结构,红黑树或者链表
    else {
        Node<K,V> e; K k;
        // 判断是不是同个key
        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 {
            // 链表,这里采用的是尾插法!在链表中查找该key,有的话break,没有的话一直找,直到链表中最后一个元素,并加入链表尾部
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 如果超过这个阈值8,转化成红黑树 
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 判断是不是同个key,是的话跳过
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // e 有值表示 当前 key 有对应的值 如果 onlyIfAbsent 的值为 false 或者当前的 value 为 null 时 则进行覆盖 , 然后 return 该 旧的值。
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 如果 put 的是一个新元素,则会来到这一步,进行 +1 ,判断是否需要扩容
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
复制代码


get 的时候也要先计算 key 的 hash 值,然后算出它的 index,接着判断有没有 hash冲


突,没有冲突的话,直接返回,有的话需要判断当前的数据结构是链表还是红黑树,然


后分别从相应的结构中取出值


这图片来自 美团技术团队 ~


网络异常,图片无法展示
|


十. 怎么判断一个元素是否相同的呢



先判断他们的 hash 值是否相同,相同的话再判断该 key 的 equals 方法 || == 方法是否相同,相同的话则是同一个元素


网络异常,图片无法展示
|


十一.  什么情况下会用到红黑树?


可以参考上面的 put 源码 ,主要代码如图:


网络异常,图片无法展示
|


网络异常,图片无法展示
|


网络异常,图片无法展示
|


可以看到当数组的大小大于64且链表的长度大于8时,会将链表转换成红黑树。


网络异常,图片无法展示
|


当红黑树的大小小于等于6时,会转换成链表,参考 resize 源码


网络异常,图片无法展示
|


十二. 头插法和尾插法



由于 jdk1.7 中 HashMap 使用的是头插法, 那么新元素总会被放在链表的头部


比如 HashMap 大小为 4, key 2,6 ,10  所在的index 都是2


那么当它 扩容的时候,顺序就会变成


index:2,key:10 ,2


index:6,key:6


在多线程环境下,有可能会导致死循环~


比如 线程a 还停留在2.next 6,6.next 10  ,线程b已经完成resize了,变成了10.next2  这时就会进入死循环了。


这也是 HashMap 线程不安全的原因之一。


同样的例子:尾插法不会导致死循环


如:线程a 还停留在2.next 6,6.next 10  ,线程b已经完成resize了,变成了2.next10 了。


使用尾插法并不意味着 HashMap 就是线程安全了,因为你无法保证 put 进去的值,get 出来的还是 put 进去的值


,因为有可能已经被别的线程修改了。



目录
相关文章
|
1月前
|
存储 算法 Java
【Java集合类面试八】、 介绍一下HashMap底层的实现原理
HashMap基于hash算法,通过put和get方法存储和获取对象,自动调整容量,并在碰撞时用链表或红黑树组织元素以优化性能。
|
1月前
|
Java 索引
【Java集合类面试九】、介绍一下HashMap的扩容机制
HashMap的扩容机制包括初始容量16,以2的次方进行扩充,使用负载因子0.75判断是否扩容,以及链表长度达到阈值时转换为红黑树,以优化性能。
【Java集合类面试九】、介绍一下HashMap的扩容机制
|
1月前
|
存储 安全 Java
一天十道Java面试题----第二天(HashMap和hashTable的区别--------》sleep、wait、join)
这篇文章是关于Java面试的第二天笔记,涵盖了HashMap与HashTable的区别、ConcurrentHashMap的实现原理、IOC容器的实现方法、字节码的概念和作用、Java类加载器的类型、双亲委派模型、Java异常体系、GC如何判断对象可回收、线程的生命周期及状态,以及sleep、wait、join、yield的区别等十道面试题。
一天十道Java面试题----第二天(HashMap和hashTable的区别--------》sleep、wait、join)
|
1月前
|
存储 Java
【Java集合类面试七】、 JDK7和JDK8中的HashMap有什么区别?
JDK7中的HashMap使用数组加链表解决冲突,而JDK8增加了红黑树结构以优化链表过长时的性能,提高查找效率。
|
1月前
|
安全 Java
【Java集合类面试十五】、说一说HashMap和HashTable的区别
HashMap和Hashtable的主要区别在于Hashtable是线程安全的,不允许null键和值,而HashMap是非线程安全的,允许null键和值。
|
1月前
|
安全 Java
【Java集合类面试十三】、HashMap如何实现线程安全?
实现HashMap线程安全的方法包括使用Hashtable类、ConcurrentHashMap,或通过Collections工具类将HashMap包装成线程安全的Map。
|
1月前
|
Java
【Java集合类面试十一】、HashMap为什么用红黑树而不用B树?
HashMap选择使用红黑树而非B树,是因为红黑树在内存中实现简单,节点更小,占用内存少,且在插入、删除和查找操作上提供更好的平衡性能。
|
1月前
|
安全 Java
【Java集合类面试十六】、HashMap与ConcurrentHashMap有什么区别?
HashMap是非线程安全的,而ConcurrentHashMap通过减少锁粒度来提高并发性能,检索操作无需锁,从而提供更好的线程安全性和性能。
|
1月前
|
Java
【Java集合类面试十四】、HashMap是如何解决哈希冲突的?
HashMap解决哈希冲突的方法是通过链表和红黑树:当链表长度超过一定阈值时,转换为红黑树以提高性能;当链表长度缩小到另一个阈值时,再转换回链表。
|
1月前
|
Java
【Java集合类面试十二】、HashMap为什么线程不安全?
HashMap在并发环境下执行put操作可能导致循环链表的形成,进而引起死循环,因而它是线程不安全的。