目录

5. Java 校招复习(持续更新~)

多线程

HashMap

实现原理

Hash冲突/碰撞解决方法

常见面试题

HashMap和ConcurrentHashMap区别?(2020蘑菇街面试真题)

线程安全?

HashMap的Value(还是Key?)能不能为null?(2020蘑菇街面试真题)

Key: 源码中的hash(Object key)函数里有一句

1
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

可以看出,当key == null时,hashCode为0,而不是抛出异常,所以是key是可以为null

Value: HashMap源码中并没有对value做限制,所以是value是可以为null

ArrayMap 的优势?

String 相关

常见面试题

String、StringBuffer 和 StringBuilder 的区别和应用场景

区别

  1. 是否可以改变?

    • String是字符串常量,由String创建的字符内容是不可改变的。我们对字符串进行拼接或重新赋值,是在字符串池中创建了新的字符串,原来那个字符串的值并没有改变。
    • StringBufferStringBuilder是字符串变量,由StringBufferStringBuilder创建的字符内容是可以改变的。而且在字符串拼接的情况下,不会产生临时的字符串。
  2. StringBuffer是线程安全的,而StringBuilder是非线程安全。StringBuilder是从 JDK5 开始,为StringBuffer补充的一个单线程的等价类。我们应该在使用时优先考虑 StringBuilder,因为它支持StringBuffer的所有操作,但是因为它不执行同步,不会有线程安全带来的额外系统消耗,所以速度更快。

    实际上StringBuilderStringBuffer的方法是完全等价的,只是StringBuffer的方法加了sychronized描述。

场景

  1. 如果不常去改变String的值,不进行许多字符串拼接等操作,就比较适合使用String,因为String是不可变。
  2. 如果在一个单线程中,有许多字符串拼接操作,使用StringBuilder就可以满足了,并且它性能更好。
  3. 如果在多线程中,要考虑到线程安全问题,就只能用StringBuffer

字符串拼接后地址比较(卓动2020秋招笔试真题)

写出以下3个方法的返回值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public static boolean testV1() {
    String a = "a1";
    String b = "a" + 1;
    return a == b;
}

public static boolean testV2() {
    String a = "ab";
    String bb = "b";
    String ab = "a" + bb;
    return a == ab;
}

public static boolean testV3() {
    String a = "ab";
    String bb = "b";
    String ab = "a" + "b";
    return a == ab;
}
1
2
3
true
false
true

List相关

ArrayList和LinkedList区别?(从实现原理、性能对比阐述)

List迭代时移除元素造成的异常(卓动2020秋招笔试真题)

下面这段代码执行结果是什么?如有异常,请根据此段程序执行的意图给出相应的解决方案并说明原由。

1
2
3
4
5
6
7
8
9
public static void testV1() {
    ArrayList<Integer> ids = new ArrayList<>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
    for (Integer id : ids) {
        if (id == 5) {
            ids.remove(5);
        }
        System.out.println(id);
    }
}

foreach 循环背后的实现原理其实就是 Iterator,所以我们不妨把上面的代码转换一下以便后面分析:

1
2
3
4
5
6
7
8
9
ArrayList<Integer> ids = new ArrayList<>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
Iterator<Integer> iterator = ids.iterator();
while (iterator.hasNext()) {
    Integer id = iterator.next();
    if (id == 5) {
        ids.remove(5);
    }
    System.out.println(id);
}

执行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
0
1
2
3
4
5
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at test.ListTest.testV1(ListTest.java:18)
	at test.ListTest.main(ListTest.java:12)

异常原由

当我们迭代一个ArrayList或者HashMap时,如果尝试对集合做一些修改操作(例如删除元素),可能会抛出java.util.ConcurrentModificationException的异常。

从前面的异常中我们可以知道,报错原因是在java.util.ArrayList$Itr.checkForComodification()这个方法里。

我们先不忙看这个方法的源码,我们先根据程序的代码一步一步看 ArrayList 源码的实现。

查首先看 ArrayList 的iterator()方法的具体实现,看源码发现在 ArrayList 的源码中并没有iterator()这个方法,那么很显然这个方法应该是其父类或者实现的接口中的方法,我们在其父类 AbstractList 中找到了iterator()方法的具体实现:

1
2
3
public Iterator<E> iterator() {
    return new Itr();
}

从这段代码可以看出返回的是一个指向 Itr 类型对象的引用,我们接着看 Itr 的具体实现,在 AbstractList 类中找到了 Itr 类的具体实现,它是 AbstractList 的一个成员内部类,下面这段代码是 Itr 类的所有实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private class Itr implements Iterator<E> {

    int cursor = 0;

    int lastRet = -1;

    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor != size();
    }

    public E next() {
        checkForComodification();
        try {
            int i = cursor;
            E next = get(i);
            lastRet = i;
            cursor = i + 1;
            return next;
        } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException();
        }
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            AbstractList.this.remove(lastRet);
            if (lastRet < cursor)
                cursor--;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException e) {
            throw new ConcurrentModificationException();
        }
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

这里不分析整个 Itr 类,针对本题,我们只需要知道以下几个关键信息:

  1. 执行next()方法的时候,会先调用一次checkForComodification()进行检查;
  2. modCount不等于expectedModCount的时候,就会抛出ConcurrentModificationException异常。

那么modCountexpectedModCount,又是什么呢?下面是科普时间~

变量名描述
modCount当前集合修改次数;ArrayList 的父类 AbstarctList 中有一个成员变量modCount,每次对集合进行修改(增删元素)时都会modCount++
expectedModCount期望的集合修改次数;迭代 ArrayList 的 Iterator(即内部类Itr) 中有一个变量 expectedModCount,该变量会初始化和modCount相等。

那么我们现在来看一下什么时候modCount不等于expectedModCount

既然这里是执行 ArrayList 的remove(),那我们不妨先去看看remove()方法做了什么:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

通过 remove 方法删除元素首先对 modCount 进行加1操作(因为对集合修改了一次),然后接下来就是删除元素的操作,最后将 size 进行减1操作,并将引用置为 null 以方便垃圾收集器进行回收工作。

modCount++就是此案元凶,我们来分析一下删除元素前后的各个成员变量的值:

删除元素前

变量名变量值
Iterator#expectedModCount0
List#modCount0

删除元素后

变量名变量值
Iterator#expectedModCount0
List#modCount1

而删除元素后,程序又执行了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()方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        AbstractList.this.remove(lastRet);
        if (lastRet < cursor)
            cursor--;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException e) {
        throw new ConcurrentModificationException();
    }
}

在这个方法中,删除元素实际上调用的就是list.remove()方法,但是它多了一个操作:

1
expectedModCount = modCount;

这个操作保证了期望值和当前值相等,即保证了不会抛出java.util.ConcurrentModificationException异常。

因此,在迭代器中如果要删除元素的话,需要调用 Itr 类的 remove 方法。

所以代码应修改为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static void testV1Fix() {
    ArrayList<Integer> ids = new ArrayList<>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
    Iterator<Integer> iterator = ids.iterator();
    while (iterator.hasNext()) {
        Integer id = iterator.next();
        if (id == 5) {
            iterator.remove();  // 注意这个地方是 iterator 的 remove 方法
        }
        System.out.println(id);
    }
}

执行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
0
1
2
3
4
5
6
7
8
9

Process finished with exit code 0

但是,这个办法的有两个弊端:

  1. 只能进行 remove 操作,add、clear等 Itr 中没有。
  2. 而且只适用单线程环境。
多线程中的解决方法

简单说明异常情况:异常的原因很简单,通过 Iterator 访问的情况下,每个线程里面返回的是不同的 iterator,也即是说 expectedModCount 是每个线程私有。假若此时有2个线程,线程1在进行修改,线程2在进行遍历;线程1修改后 list 的 modCount 自增了,线程1的 expectedModCount 也自增了,但是线程2的 expectedModCount 由于各线程私有并没有自增,导致线程2迭代时 modCount 与该迭代器的 expectedModCount 不相等。

一般有两种方法:

  1. 在使用 iterator 迭代前加锁,使用 synchronized 或者 Lock 进行同步;解决了多线程问题,但还是不能进行迭代add、clear等操作。
  2. 使用并发容器 CopyOnWriteArrayList 代替 ArrayList 和 Vector;解决了多线程问题,同时可以add、clear等操作。

CopyOnWriteArrayList 也是一个线程安全的 ArrayList,其实现原理在于,每次 add、remove 等所有的操作都是重新创建一个新的数组,再把引用指向新的数组。(用的少不多讨论)

为什么要设置 modCount 与 expectedModCount 变量?

到这里,我们似乎已经理解完这个异常的产生缘由了。

但是,仔细思考,还是会有几点疑惑:

  1. 既然 modCount 与 expectedModCount 不同会产生异常,那为什么还设置这个变量
  2. 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秋招笔试真题)

思路:

  1. 先判断节点存不存在
  2. 然后判断节点是不是文件夹,若是文件夹,则递归遍历该目录下的所有子结点(文件或目录)
  3. 递归后删除结点。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
 * 题目:用Java实现对一个文件夹内所有文件包括子文件夹的删除
 *
 * @param dir 将要删除的文件目录
 * @return 删除状态
 */
public static boolean deleteAllFilesInDir(File dir) {
    if (!dir.exists()) {
        throw new RuntimeException("目录或文件不存在!");
    }
    if (dir.isDirectory()) {
        for (File child : Objects.requireNonNull(dir.listFiles())) {
            deleteAllFilesInDir(child);
        }
    }
    // 此时已经没有文件了,该目录可以删除
    return dir.delete();
}

JVM

GC算法(垃圾回收机制)

内存泄漏和内存溢出

内存溢出(OOM)

内存溢出是指应用在申请内存的时候,没有足够的内存可以分配,导致Out Of Memory错误,也就是 OOM。

注意:OOM 会导致应用 Crash。

内存泄漏

内存泄漏是指在对象被垃圾回收和释放时,如果得不到及时的释放,就会一直占用内存,造成内存泄漏。

区别

看上面定义

什么时候会发生?举例场景

Java 类加载机制

双亲委派模型以及意义