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。
内存泄漏
内存泄漏是指在对象被垃圾回收和释放时,如果得不到及时的释放,就会一直占用内存,造成内存泄漏。
区别
看上面定义