面试八股文之——Java集合

众所周知,程序员的技术能力考核大部分来源于面试和笔试,少数人可以靠着开源项目或者是证书、个人作品(书籍)等提升求职竞争力而直接获取offer。绝大多数程序员依旧是靠面试来获取offer。因此对待面试题,很多时候,应聘者需要做很多的准备,本文将对Java集合高频面试题目进行分享。

一、ArrayList篇

1.ArrayList是如何实现自动扩容的?是线程安全的吗?如何实现线程安全?

ArrayList默认size是10,当然,也可以在初始化指定长度大小,当数组空间不足时,则会创建一个新数组,长度为原数组的1.5倍,采用Arrays.CopyOf方法将原数组数据复制到新数组中。扩容后,再将待插入的数据放入新数组中。

ArrayList不是线程安全的,在多线程情况下,会发生线程不安全的问题,比如在扩容过程中,A/B两个线程同时插入1条数据,由于扩容过程没有采取同步,容易导致扩容过程中,某个线程插入记录丢失,发生异常。

实现线程安全有很多种方式:

1.最简单的措施,采用线程安全的List------CopyOnWriteArrayList【推荐,简单高效】

2.基于ArrayList改造,在进行扩容中,加入锁设计,比如显示地使用Lock锁,或者使用synchronized关键字。

3.显示地在ArrayList的插入、删除方法中引入锁。

2.ArrayList、Vector、LinkedList的存储性能及特性

1.ArrayList/Vector底层采用数组实现,具备插入慢、读取快的特点,插入慢的原因是,每次插入后,插入位置后的所有数组元素都需要往后移动一位,扩容过程也会降低插入效率,读取快的原因是,只需要知道数组下标即可快速计算出对象的存储地址。

2.Vector是线程安全其他不是,但是由于采用了synchronized关键字来保证同步,性能较差。是早起版本的容器,官方已不在推荐使用。

3.LinkedList基于双向链表实现,具备插入快、读取慢的特点。读取慢是由于每次访问数据,需要通过遍历来获取指定位置的元素,插入快是因为插入过程中,只需要记录元素的前后项,并采用指针指向即可,受影响的元素只有2个。

二、HashMap篇

1.单线程下HashMap工作原理?

1.基本参数:size 元素个数,threshold 扩容阈值,loadFactor 负载因子,modCount 记录hashMap内部结构修改次数,DEFAULT_INITIAL_CAPACITY 初始容量大小,扩容阈值等于负载因此*容量大小

2.数据结构:数组桶+链表(JDK1.8优化后,当链表长度大于等于8则转为红黑树)

3.特征:存储KV数据,数据访问速度快,允许存null值和null键,线程不安全

4.put方法执行步骤:

(1) 计算元素Key的hash(采用Key的hashCode和高16位hashCode经过异或计算得到),将hash与容量长度减一进行按位与操作,等价于与容量长度size进行取模,得到数组的下标。按位与效率更高。

(2) 如当前下标无数据,直接插入即可,如发生hash碰撞,则采用头插法插入,JDK8之后改用尾插法,构建链表结构

(3) 当链表长度大于等于8且map容量大于等于64,改为红黑树进行存储【旨在解决链表过长导致查询时间增加的问题】,当红黑树节点数小于等于6,改为链表存储。

(4) 当插入元素达到扩容阈值threshold,则会发生扩容,采用2倍扩容。

2.HashMap如何解决Hash冲突的?

常见解决hash冲突的方法有四种:

1.开放定址法,也称为线性探测法,从发生冲突的位置开始,按照一定的次序,从Hash表中找到一个空闲的位置,然后将发生冲突的元素插入到这个空闲位置。ThreadLocal采用这种方法来解决Hash冲突。

2.链式寻址法,将发生冲突的Key,用单向链表来存储,HashMap采用这种方式解决Hash冲突。

3.再Hash法,当通过某个Hash函数计算的Key存在冲突时,再另外使用一个Hash方法对Key进行计算,一直运算至不再发生冲突,计算时间增加,性能影响较大。

4.建立公共溢出区,将Hash表分为基本表和溢出表,凡是存在冲突的元素,一律放入溢出表中。

3.谈谈对HashMap扩容机制的理解?

1.当hashMap中的元素数目size到达扩容阈值,也就threshold ,则会动态进行2倍扩容,其中threshold 是负载因子loadFactor 和容量Capacity的乘积,默认负载因子是0.75,容量是16由于动态扩容的存在,实际开发中最好初始化集合的大小,避免频繁扩容带来性能上的消耗

负载因子设计在0~1之间,这个越大,空间利用率越高,同时hash冲突的概率也越大,反之,这个越小,空间利用率越低,hash冲突概率越低。0.75值的选取,链表长度达到8的概率几乎为0,综合考虑了hashMap空间利用率和hash冲突的影响,做到了空间和时间成本的平衡。

4.HashMap为什么会发生死循环?

1.这个只会发生JDK1.7中,JDK1.8,官方彻底解决了这个问题。

2.JDK1.7采用头插法插入元素,扩容后的新Map采用尾插法插入元素,当多个线程并发发生扩容时候,会导致新Map上的链表节点倒序排列,此时其他线程节点引用关系仍然是顺序排列,此时链表起始节点和下一个节点就会发生互相引用,因此发生死循环。在JDK1.8中,HashMap插入节点也改为尾插入,彻底解决这个问题。

3.避免HashMap发生死循环的方法如下:

(1) 使用ConcurrentHashMap替代HashMap ,推荐,简单高效

(2) 使用线程安全的HashTable替代,性能低,不推荐

(3) 在执行插入前手动加入锁机制,比如synchronized/lock锁

5.HashMap和TreeMap的区别?

1.数据结构:HashMap是基于数据+链表实现,TreeMap是基于红黑树实现

2.效率:HashMap效率更高,O(1),TreeMap效率O(logN)

3.线程安全:都是线程不安全的,如要保持线程安全和保证顺序,可以使用Collections.synchronizedMap()方法将其转为线程安全的Map。

4.HashMap无序,TreeMap基于Key排序或者自定义排序。

6.为什么ConcurrentHashMap的Key不为null?

直接原因:JDK ConcurrentHashMap源码设计中设定Key和Value不能为null,否则抛出空指针异常。
本质原因:ConcurrentHashMap大部分应用于多线程场景下,Key或者是Value为null,无法判断是Key本身不存在还是Key的值就是null,容易发生歧义,增加心智负担。

7.ConcurrentHashMap的底层原理及如何保证线程安全的?

1.ConcurrentHashMap本质就是线程安全版的HashMap,底层基于数组+链表(jdk1.8采用红黑树优化查询效率),ConcurrentHashMap在JDK1.8之后对锁进行了优化,抛弃了JDK1.7分段锁Segment的设计,进一步缩小锁粒度,只对桶的头结点进行加锁,提升并发场景下对象操作性能,JDK1.7采用ReentrantLock锁来保证线程安全,JDK1.8采用CAS+volatile+synchronized。相比之下,性能得到进一步提升。

2.除此之外,ConcurrentHashMap引入了多线程并发扩容机制,即每个线程负责一个分片的数据迁移,从而提升扩容中数据迁移的效率。

3.对获取总的元素个数的size()方法进行了优化,如果竞争不激烈,直接采用CAS进行原子递增,否则将拆分为一个数组,如果需要增加元素个数则直接从数组中随机选择一个,再通过CAS进行原子递增,核心思想引入一个数组来降低CAS竞争,提升并发更新的速度

相关推荐
Lee川35 分钟前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川4 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i6 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有6 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有7 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫7 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫8 小时前
Handler基本概念
面试
Wect8 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼9 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼9 小时前
Next.js 企业级落地
前端·javascript·面试