2022年Java基础面试集合会问哪些问题?

面试官要不今天来讲讲Java的List吧,你对List了解多少?

候选者:List在Java里边是一个接口,常见的实现类有ArrayList和LinkedList,在开发中用得最多的是ArrayList

候选者:ArrayList的底层数据结构是数组,LinkedList底层数据结构是链表。


 


面试官那Java本身就有数组了,为什么要用ArrayList呢?

候选者:原生的数组会有一个特点:你在使用的时候必须要为它创建大小,而ArrayList不用。

候选者:在日常开发的时候,往往我们是不知道数组的大小的

候选者:如果数组的大小指定多了,内存浪费;如果数组大小指定少了,装不下。

候选者:假设我们给定数组的大小是10,要往这个数组里边填充元素,我们只能添加10个元素。

候选者:而ArrayList不一样,ArrayList我们在使用的时候可以往里边添加20个,30个,甚至更多的元素

候选者:因为ArrayList是实现了动态扩容的


 


候选者:大概的意思就是:

候选者:当我们new ArrayList()的时候,默认会有一个空的Object数组,大小为0。

候选者:当我们第一次add添加数据的时候,会给这个数组初始化一个大小,这个大小默认值为10

候选者:使用ArrayList在每一次add的时候,它都会先去计算这个数组够不够空间

候选者:如果空间是够的,那直接追加上去就好了。如果不够,那就得扩容

面试官那怎么扩容?一次扩多少?

候选者:在源码里边,有个grow方法,每一次扩原来的1.5倍。比如说,初始化的值是10嘛。

候选者:现在我第11个元素要进来了,发现这个数组的空间不够了,所以会扩到15

候选者:空间扩完容之后,会调用arraycopy来对数组进行拷贝


 


面试官:哦,可以的。

面试官那为什么你在前面提到,在日常开发中用得最多的是ArrayList呢?

候选者:是由底层的数据结构来决定的,在日常开发中,遍历的需求比增删要多,即便是增删也是往往在List的尾部添加就OK了。

候选者:像在尾部添加元素,ArrayList的时间复杂度也就O(1)

候选者:另外的是,ArrayList的增删底层调用的copyOf()被优化过

候选者:现代CPU对内存可以块操作,ArrayList的增删一点儿也不会比LinkedList慢


 


面试官Vector你了解吗?

候选者:嗯,Vector是底层结构是数组,一般现在我们已经很少用了。

候选者:相对于ArrayList,它是线程安全的,在扩容的时候它是直接扩容两倍的

候选者:比如现在有10个元素,要扩容的时候,就会将数组的大小增长到20


 


面试官嗯,那如果我们不用Vector,线程安全的List还有什么?

候选者:首先,我们也可以用Collections来将ArrayList来包装一下,变成线程安全。

候选者:但这肯定不是你想听的,对吧。在java.util.concurrent包下还有一个类,叫做CopyOnWriteArrayList

候选者:要讲CopyOnWriteArrayList之前,我还是想说说copy-on-write这个意思,下面我会简称为cow。

候选者:比如说在Linux中,我们知道所有的进程都是init进程fork出来的

候选者:除了进程号之外,fork出来的进程,默认跟父进程一模一样的。

候选者:当使用了cow机制;子进程在被fork之后exec之前,两个进程用的是相同的内存空间的

候选者:这意味着子进程的代码段、数据段、堆栈都是指向父进程的物理空间

候选者:当父子进程中有更改的行为发生时,再为子进程分配相应物理空间。

候选者:这样做的好处就是,等到真正发生修改的时候,才去分配资源,可以减少分配或者复制大量资源时带来的瞬间延时。

候选者:简单来说,就可以理解为我们的懒加载,或者说单例模式的懒汉式。等真正用到的时候再分配


 


面试官:嗯

候选者:在文件系统中,其实也有cow的机制。

候选者:文件系统的cow就是在修改数据的时候,不会直接在原来的数据位置上进行操作,而是重新找个位置修改。

候选者:比如说:要修改数据块A的内容,先把A读出来,写到B块里面去。

候选者:如果这时候断电了,原来A的内容还在。这样做的好处就是可以保证数据的完整性,瞬间挂掉了容易恢复。

候选者:再回头来看CopyOnWriteArrayList吧,CopyOnWriteArrayList是一个线程安全的List,底层是通过复制数组的方式来实现的。

候选者:我来说说它 的add()方法的实现吧

面试官:好

候选者:在add()方法其实他会加lock锁,然后会复制出一个新的数组,往新的数组里边add真正的元素,最后把array的指向改变为新的数组

候选者:get()方法又或是size()方法只是获取array所指向的数组的元素或者大小。读不加锁,写加锁

候选者:可以发现的是,CopyOnWriteArrayList跟文件系统的COW机制是很像的


 


面试官那你能说说CopyOnWriteArrayList有什么缺点吗?

候选者:很显然,CopyOnWriteArrayList是很耗费内存的,每次set()/add()都会复制一个数组出来

候选者:另外就是CopyOnWriteArrayList只能保证数据的最终一致性,不能保证数据的实时一致性。

候选者:假设两个线程,线程A去读取CopyOnWriteArrayList的数据,还没读完

候选者:现在线程B把这个List给清空了,线程A此时还是可以把剩余的数据给读出来。


 


面试官:嗯,还可以。

面试官今天来讲讲Map吧,你对Map了解多少?就讲JDK 1.8就好咯

候选者:Map在Java里边是一个接口,常见的实现类有HashMap、LinkedHashMap、TreeMap和ConcurrentHashMap

候选者:在Java里边,哈希表的结构是数组+链表的方式。

候选者:HashMap底层数据结构是数组+链表/红黑树

候选者:LinkedHashMap底层数据结构是数组+链表/红黑树+双向链表

候选者:TreeMap底层数据结构是红黑树

候选者:而ConcurrentHashMap底层数据结构也是数组+链表/红黑树


 


面试官我们先以HashMap开始吧,你能讲讲当你new一个HashMap的时候,会发生什么吗?

候选者:HashMap有几个构造方法,但最主要的就是指定初始值大小和负载因子的大小。

候选者:如果我们不指定,默认HashMap的大小为16,负载因子的大小为0.75

候选者:HashMap的大小只能是2次幂的,假设你传一个10进去,实际上最终HashMap的大小是16,你传一个7进去,HashMap最终的大小是8,具体的实现在tableSizeFor可以看到。

候选者:我们把元素放进HashMap的时候,需要算出这个元素所在的位置(hash)。

候选者:在HashMap里用的是位运算来代替取模,能够更加高效地算出该元素所在的位置。

候选者:为什么HashMap的大小只能是2次幂,因为只有大小为2次幂时,才能合理用位运算替代取模。


 


候选者:而负载因子的大小决定着哈希表的扩容和哈希冲突。

候选者:比如现在我默认的HashMap大小为16,负载因子为0.75,这意味着数组最多只能放12个元素,一旦超过12个元素,则哈希表需要扩容。

候选者:怎么算出是12呢?很简单,就是16*0.75。每次put元素进去的时候,都会检查HashMap的大小有没有超过这个阈值,如果有,则需要扩容。


 


候选者:鉴于上面的说法(HashMap的大小只能是2次幂),所以扩容的时候时候默认是扩原来的2倍

候选者:扩容这个操作肯定是耗时的,那能不能把负载因子调高一点,比如我要调至为1,那我的HashMap就等到16个元素的时候才扩容呢。

候选者:是可以的,但是不推荐。负载因子调高了,这意味着哈希冲突的概率会增高,哈希冲突概率增高,同样会耗时(因为查找的速度变慢了)

面试官那我想问下,在put元素的时候,传递的Key是怎么算哈希值的?

候选者:实现就在hash方法上,可以发现的是,它是先算出正常的哈希值,然后与高16位做异或运算,产生最终的哈希值。

候选者:这样做的好处可以增加了随机性,减少了碰撞冲突的可能性。


 



 


面试官了解,你简单再说下put和get方法的实现吧

候选者:在put的时候,首先对key做hash运算,计算出该key所在的index。

候选者:如果没碰撞,直接放到数组中,如果碰撞了,需要判断目前数据结构是链表还是红黑树,根据不同的情况来进行插入。

候选者:假设key是相同的,则替换到原来的值。最后判断哈希表是否满了(当前哈希表大小*负载因子),如果满了,则扩容

候选者:在get的时候,还是对key做hash运算,计算出该key所在的index,然后判断是否有hash冲突

候选者:假设没有冲突直接返回,假设有冲突则判断当前数据结构是链表还是红黑树,分别从不同的数据结构中取出。


 


面试官那在HashMap中是怎么判断一个元素是否相同的呢?

候选者:首先会比较hash值,随后会用==运算符和equals()来判断该元素是否相同。

候选者:说白了就是:如果只有hash值相同,那说明该元素哈希冲突了,如果hash值和equals() || == 都相同,那说明该元素是同一个。

有需要的小伙伴可【点击此处】免费领取!!


 


面试官你说HashMap的数据结构是数组+链表/红黑树,那什么情况拿下才会用到红黑树呢?

候选者:当数组的大小大于64且链表的大小大于8的时候才会将链表改为红黑树,当红黑树大小为6时,会退化为链表

候选者:这里转红黑树退化为链表的操作主要出于查询和插入时对性能的考量。

候选者链表查询时间复杂度O(N),插入时间复杂度O(1),红黑树查询和插入时间复杂度O(logN)


 


面试官你在日常开始中LinkedHashMap用的多吗?

候选者:其实在日常开发中LinkedHashMap用得不多。

候选者:在前面也提到了,LinkedHashMap底层结构是数组+链表+双向链表,实际上它继承了HashMap,在HashMap的基础上维护了一个双向链表

候选者:有了这个双向链表,我们的插入可以是有序的,这里的有序不是指大小有序,而是插入有序。

候选者:LinkedHashMap在遍历的时候实际用的是双向链表来遍历的,所以LinkedHashMap的大小不会影响到遍历的性能

面试官那TreeMap呢?

候选者:TreeMap在现实开发中用得也不多,TreeMap的底层数据结构是红黑树

候选者:TreeMap的key不能为null(如果为null,那还怎么排序呢),TreeMap有序是通过Comparator来进行比较的,如果comparator为null,那么就使用自然顺序


 


面试官再来讲讲线程安全的Map吧?HashMap是线程安全的吗?

候选者:HashMap不是线程安全的,在多线程环境下,HashMap有可能会有数据丢失和获取不了最新数据的问题,比如说:线程Aput进去了,线程Bget不出来。

候选者:我们想要线程安全,可以使用ConcurrentHashMap

候选者:ConcurrentHashMap是线程安全的Map实现类,它在juc包下的。

候选者:线程安全的Map实现类除了ConcurrentHashMap还有一个叫做Hashtable。

候选者:当然了,也可以使用Collections来包装出一个线程安全的Map。

候选者:但无论是Hashtable还是Collections包装出来的都比较低效(因为是直接在外层套synchronize),所以我们一般有线程安全问题考量的,都使用ConcurrentHashMap


 


候选者:ConcurrentHashMap的底层数据结构是数组+链表/红黑树,它能支持高并发的访问和更新,是线程安全的。

候选者:ConcurrentHashMap通过在部分加锁和利用CAS算法来实现同步,在get的时候没有加锁,Node都用了volatile给修饰。

候选者:在扩容时,会给每个线程分配对应的区间,并且为了防止putVal导致数据不一致,会给线程的所负责的区间加锁

面试官你能给我讲讲JDK 7 和JDK8中HashMap和ConcurrentHashMap的区别吗?

候选者:不能,我不会

候选者:我在学习的时候也看过JDK7的HashMap和ConcurrentHashMap,其实还是有很多不一样的地方

候选者 比如JDK 7 的HashMap在扩容时是头插法,在JDK8就变成了尾插法,在JDK7 的HashMap还没有引入红黑树….

候选者:ConcurrentHashMap 在JDK7 还是使用分段锁的方式来实现,而JDK 8 就又不一样了。但JDK 7细节我大多数都忘了。

候选者:我就没用过JDK 7的API,我想着现在最低应该也是用JDK8了吧?所以我就没去仔细看了。

面试官:你这很危险!

全部评论

相关推荐

点赞 1 评论
分享
牛客网
牛客企业服务