Java基础与集合小压八股
Java基础与集合
HashMap 的底层结构是怎样的?JDK 1.7 和 1.8 有哪些区别?为什么要引入红黑树?(递进提问)
HashMap 底层是基于数组 + 链表 + 红黑树实现的。
在 JDK 1.7 中,它使用数组 + 链表的结构,采用头插法来解决哈希冲突;而从 JDK 1.8 开始,为了优化链表过长导致的查找性能下降问题,引入了红黑树,当链表长度超过 8 且数组长度超过 64 时,会将链表转换为红黑树。
同时,JDK 1.8 改用了尾插法,主要是为了解决多线程环境下扩容时可能出现的环形链表问题。
在索引计算上,JDK 1.8 对 hash 值做了高位扰动函数优化(即让当前 key 的 hashCode 与高十六位异或),来减少哈希冲突。
此外,扩容逻辑也更高效了,不再重新计算每个节点的哈希值,而是利用 (oldCap & hash) 判断节点在新表中的位置。(其实跟数组长度只能是 2 的倍数有关,触发扩容时,当前节点的新索引只需要+16。(n - 1) & hash)
HashMap 在什么情况下会将红黑树退化回链表?为什么要这么做?
当桶中链表长度超过 8 并且数组长度达到 64 时,会触发树化,将链表结构转为红黑树,以提升查找效率。(最坏情况下时间复杂度 O(n)->O(logn)
但当红黑树节点减小到 6 以下时,红黑树就会退化成链表。主要原因是红黑树节点结构复杂,占用更多内存。节点较少时,查找性能反而降低,得不偿失。
这里的阈值 6 和 8 是经过性能测试确定的经验值,保证在性能和内存之间的平衡性。(不选 7,因为会频繁树化和回退,性能开销大)
那如果在多线程环境下同时对 HashMap 进行 put 操作,会发生什么问题?你知道为什么 HashMap 在多线程下不安全吗?
在 JDK1.7 中,HashMap 在多线程下同时执行put方法,可能在 rehash 扩容过程中出现链表成环,导致死循环。
原因是 1.7 的扩容使用的头插法,rehash 扩容时多线程交错修改next指针可能导致节点反转出错。
1.8 时改用了尾插法,但他仍然是线程不安全的,因为多线程下同时put时仍可能出现**数据丢失(覆盖)、数据不可见(写入未同步)**等问题。
本就不应该在多线程环境下使用HashMap
你刚提到并发环境下要用 ConcurrentHashMap,那你能说一下它是如何保证线程安全的吗?
在 JDK1.7 中,ConcurrentHashMap使用Segment分段锁的方式实现线程安全,每个Segment内部使用ReentrantLock控制并发访问,不同Segment之间可以并行操作。相当于每个Segment对应一个HashMap。
在 JDK1.8 中,使用类似HashMap的数组+链表+红黑树的结构,并通过 CAS+Synchronized+volitale 实现并发安全。
那如果在 ConcurrentHashMap 中执行 put 操作时,发生了哈希冲突,会经历哪些步骤?
ConcurrentHshMap在 JDK1.8 中会先通过 CAS 确保初始化,然后计算桶的位置。如果桶为空,那么使用 CAS 插入。如果发生冲突,则用 Synchronized 先锁住该桶,在链表或红黑树中插入该节点。从而保证线程安全。
讲讲 ArrayList 和 LinkedList 的区别,它们在插入、查找时的时间复杂度分别是多少?
底层数据结构不同,ArrayList 是基于动态数组实现,元素在内存中连续存储。
LinkedList 是基于双向链表实现,元素在内存中不连续存储。
性能不同,ArrayList 中间插入的时间复杂度为 O(n),因为需要移动后续元素。查找时间复杂度只有 O(1)。LinkedList 插入的时间复杂度为 O(n),在首尾降为 O(1)。查找时间复杂度为 O(n),因为链表元素内存地址不连续。
equals() 和 hashCode() 的关系是什么?为什么要同时重写?
如果两个对象根据 equals()相等,那么 hashCode()必须相等。反之,如果 hashcode()相等,不要求 equals()相等。在这种情况下,如果只修改 equals(),可能导致两个 equals()的对象的 hashCode() (散列值)不同,导致集合中包含两个元素,而不是一个。
说说 Java 的自动装箱和拆箱机制,有哪些容易出错的地方?
自动装箱是指 Java 编译器自动将基本数据类型转换为对应的包装类型,比如int转为Integer类型。
自动拆箱是指 Java 编译器自动将包装类型转换为对应的基本数据类型,比如Integer转为int类型
包装类对象可能为 null,如果直接拆箱,可能会NPE。
在循环或者高性能场景,频繁拆箱和装箱可能导致性能开销。
