5. Java 校招复习(持续更新~)
多线程
HashMap
实现原理
Hash冲突/碰撞解决方法
常见面试题
HashMap和ConcurrentHashMap区别?(2020蘑菇街面试真题)
线程安全?
HashMap的Value(还是Key?)能不能为null?(2020蘑菇街面试真题)
Key:
源码中的hash(Object key)函数里有一句
|
|
可以看出,当key == null时,hashCode为0,而不是抛出异常,所以是key是可以为null的
Value:
HashMap源码中并没有对value做限制,所以是value是可以为null的
ArrayMap 的优势?
String 相关
常见面试题
String、StringBuffer 和 StringBuilder 的区别和应用场景
区别
-
是否可以改变?
String是字符串常量,由String创建的字符内容是不可改变的。我们对字符串进行拼接或重新赋值,是在字符串池中创建了新的字符串,原来那个字符串的值并没有改变。- 而
StringBuffer和StringBuilder是字符串变量,由StringBuffer和StringBuilder创建的字符内容是可以改变的。而且在字符串拼接的情况下,不会产生临时的字符串。
-
StringBuffer是线程安全的,而StringBuilder是非线程安全。StringBuilder是从 JDK5 开始,为StringBuffer补充的一个单线程的等价类。我们应该在使用时优先考虑 StringBuilder,因为它支持StringBuffer的所有操作,但是因为它不执行同步,不会有线程安全带来的额外系统消耗,所以速度更快。实际上
StringBuilder和StringBuffer的方法是完全等价的,只是StringBuffer的方法加了sychronized描述。
场景
- 如果不常去改变
String的值,不进行许多字符串拼接等操作,就比较适合使用String,因为String是不可变。 - 如果在一个单线程中,有许多字符串拼接操作,使用
StringBuilder就可以满足了,并且它性能更好。 - 如果在多线程中,要考虑到线程安全问题,就只能用
StringBuffer
字符串拼接后地址比较(卓动2020秋招笔试真题)
写出以下3个方法的返回值:
|
|
|
|
List相关
ArrayList和LinkedList区别?(从实现原理、性能对比阐述)
List迭代时移除元素造成的异常(卓动2020秋招笔试真题)
下面这段代码执行结果是什么?如有异常,请根据此段程序执行的意图给出相应的解决方案并说明原由。
|
|
foreach 循环背后的实现原理其实就是 Iterator,所以我们不妨把上面的代码转换一下以便后面分析:
|
|
执行结果:
|
|
异常原由
当我们迭代一个ArrayList或者HashMap时,如果尝试对集合做一些修改操作(例如删除元素),可能会抛出java.util.ConcurrentModificationException的异常。
从前面的异常中我们可以知道,报错原因是在java.util.ArrayList$Itr.checkForComodification()这个方法里。
我们先不忙看这个方法的源码,我们先根据程序的代码一步一步看 ArrayList 源码的实现。
查首先看 ArrayList 的iterator()方法的具体实现,看源码发现在 ArrayList 的源码中并没有iterator()这个方法,那么很显然这个方法应该是其父类或者实现的接口中的方法,我们在其父类 AbstractList 中找到了iterator()方法的具体实现:
|
|
从这段代码可以看出返回的是一个指向 Itr 类型对象的引用,我们接着看 Itr 的具体实现,在 AbstractList 类中找到了 Itr 类的具体实现,它是 AbstractList 的一个成员内部类,下面这段代码是 Itr 类的所有实现:
|
|
这里不分析整个 Itr 类,针对本题,我们只需要知道以下几个关键信息:
- 执行
next()方法的时候,会先调用一次checkForComodification()进行检查; - 当
modCount不等于expectedModCount的时候,就会抛出ConcurrentModificationException异常。
那么modCount和expectedModCount,又是什么呢?下面是科普时间~
| 变量名 | 描述 |
|---|---|
| modCount | 当前集合修改次数;ArrayList 的父类 AbstarctList 中有一个成员变量modCount,每次对集合进行修改(增删元素)时都会modCount++。 |
| expectedModCount | 期望的集合修改次数;迭代 ArrayList 的 Iterator(即内部类Itr) 中有一个变量 expectedModCount,该变量会初始化和modCount相等。 |
那么我们现在来看一下什么时候modCount不等于expectedModCount。
既然这里是执行 ArrayList 的remove(),那我们不妨先去看看remove()方法做了什么:
|
|
通过 remove 方法删除元素首先对 modCount 进行加1操作(因为对集合修改了一次),然后接下来就是删除元素的操作,最后将 size 进行减1操作,并将引用置为 null 以方便垃圾收集器进行回收工作。
modCount++就是此案元凶,我们来分析一下删除元素前后的各个成员变量的值:
删除元素前:
| 变量名 | 变量值 |
|---|---|
| Iterator#expectedModCount | 0 |
| List#modCount | 0 |
删除元素后:
| 变量名 | 变量值 |
|---|---|
| Iterator#expectedModCount | 0 |
| List#modCount | 1 |
而删除元素后,程序又执行了next()遍历下一个元素;而执行next()方法的时候,又会先调用一次checkForComodification()进行检查;在checkForComodification()方法中,当modCount不等于expectedModCount的时候,就会抛出ConcurrentModificationException异常。
笔/面试简述
说了那么多,要是笔试的时候写那么长不是早就到时间了?这里给一个简述给大家参考一下:
原由是当我们迭代一个ArrayList或者HashMap时,如果尝试对集合做一些修改操作(例如删除元素),可能会抛出java.util.ConcurrentModificationException的异常。
在 ArrayList 迭代器的next()方法中,首先会调用一次checkForComodification()方法对 modCount 和 expectedModCount 进行检查。 modCount 是当前的集合修改次数, expectedModCount 是期望的集合修改次数,当 modCount 不等于 expectedModCount 的时候,checkForComodification()方法就会抛出ConcurrentModificationException异常。
而通过 ArrayList 的remove()方法删除元素会对 modCount 进行加1操作,却没有对 expectedModCount 进行任何修改,导致了 modCount 不等于 expectedModCount。
因此,在执行next()方法遍历下一个元素的时候,就会导致 checkForComodification()方法抛出 ConcurrentModificationException 异常。
解决方法
单线程中的解决方法
既然知道原因了,那么如何解决呢?
其实很简单,细心的朋友可能发现在 Itr 类中也给出了一个remove()方法:
|
|
在这个方法中,删除元素实际上调用的就是list.remove()方法,但是它多了一个操作:
|
|
这个操作保证了期望值和当前值相等,即保证了不会抛出java.util.ConcurrentModificationException异常。
因此,在迭代器中如果要删除元素的话,需要调用 Itr 类的 remove 方法。
所以代码应修改为:
|
|
执行结果:
|
|
但是,这个办法的有两个弊端:
- 只能进行 remove 操作,add、clear等 Itr 中没有。
- 而且只适用单线程环境。
多线程中的解决方法
简单说明异常情况:异常的原因很简单,通过 Iterator 访问的情况下,每个线程里面返回的是不同的 iterator,也即是说 expectedModCount 是每个线程私有。假若此时有2个线程,线程1在进行修改,线程2在进行遍历;线程1修改后 list 的 modCount 自增了,线程1的 expectedModCount 也自增了,但是线程2的 expectedModCount 由于各线程私有并没有自增,导致线程2迭代时 modCount 与该迭代器的 expectedModCount 不相等。
一般有两种方法:
- 在使用 iterator 迭代前加锁,使用 synchronized 或者 Lock 进行同步;解决了多线程问题,但还是不能进行迭代add、clear等操作。
- 使用并发容器 CopyOnWriteArrayList 代替 ArrayList 和 Vector;解决了多线程问题,同时可以add、clear等操作。
CopyOnWriteArrayList 也是一个线程安全的 ArrayList,其实现原理在于,每次 add、remove 等所有的操作都是重新创建一个新的数组,再把引用指向新的数组。(用的少不多讨论)
为什么要设置 modCount 与 expectedModCount 变量?
到这里,我们似乎已经理解完这个异常的产生缘由了。
但是,仔细思考,还是会有几点疑惑:
- 既然 modCount 与 expectedModCount 不同会产生异常,那为什么还设置这个变量
- ConcurrentModificationException 可以翻译成“并发修改异常”,那这个异常是否与多线程有关呢?
源码中 modCount 的注解中频繁的出现了 fail-fast(边幅太大就不贴了,自行查看吧哈哈),那么 fail-fast(快速失败)机制是什么呢?
“快速失败”也就是 fail-fast ,它是 Java 集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。记住是有可能,而不是一定。
例如:假设存在两个线程(线程1、线程2),线程1通过 Iterator 在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生 fail-fast 机制。
看到这里,我们明白了,fail-fast 机制就是为了防止多线程修改集合造成并发问题的机制嘛。
之所以有 modCount 这个成员变量,就是为了辨别多线程修改集合时出现的错误。而java.util.ConcurrentModificationException就是并发异常。
但是单线程使用不当时也可能抛出这个异常。
文件相关
常见面试题
用Java实现对一个文件夹内所有文件包括子文件夹的删除(卓动2020秋招笔试真题)
思路:
- 先判断节点存不存在
- 然后判断节点是不是文件夹,若是文件夹,则递归遍历该目录下的所有子结点(文件或目录)
- 递归后删除结点。
|
|
JVM
GC算法(垃圾回收机制)
内存泄漏和内存溢出
内存溢出(OOM)
内存溢出是指应用在申请内存的时候,没有足够的内存可以分配,导致Out Of Memory错误,也就是 OOM。
注意:OOM 会导致应用 Crash。
内存泄漏
内存泄漏是指在对象被垃圾回收和释放时,如果得不到及时的释放,就会一直占用内存,造成内存泄漏。
区别
看上面定义