《揭秘互联网大厂Java面试:从基础到进阶的核心知识点大考察》

37 阅读9分钟

第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK8 之后还有红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:嗯……好像是当元素个数达到容量的一定比例,就会扩容,新容量好像是原来的 1.5 倍,然后把旧数组的数据复制到新数组。 面试官:回答得可以。HashMap 在 JDK8 中引入红黑树的阈值是多少? 王铁牛:是 8,当链表长度达到 8 时,会转化为红黑树。

第二轮面试 面试官:接下来聊聊多线程和线程池。创建线程有几种方式? 王铁牛:有三种,继承 Thread 类,实现 Runnable 接口,还有实现 Callable 接口。 面试官:很好。那线程池的核心参数有哪些,分别有什么作用? 王铁牛:呃……有核心线程数、最大线程数,核心线程数就是一开始创建的线程数量,最大线程数就是最多能创建的线程数量,还有个队列,用来存放任务。 面试官:回答得不太完整。那线程池中的拒绝策略有哪些? 王铁牛:好像有 AbortPolicy,直接抛出异常,还有 DiscardPolicy,丢弃任务,其他的不太清楚了。

第三轮面试 面试官:再谈谈框架相关的。Spring 中 Bean 的作用域有哪些? 王铁牛:有 singleton,单例模式,一个 Spring 容器中只有一个实例;还有 prototype,每次请求都会创建一个新的实例。 面试官:还行。Spring Boot 自动配置的原理是什么? 王铁牛:就是 Spring Boot 会根据依赖和配置,自动帮我们配置一些 Bean,具体怎么实现的不太清楚。 面试官:那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,能防止 SQL 注入,{} 是直接拼接 SQL,可能会有 SQL 注入风险。

面试总结:从这三轮面试来看,你对于一些基础的 Java 知识掌握得还可以,像 ArrayList、HashMap 的底层结构,创建线程的方式等都回答得不错。但对于一些稍微深入的问题,比如线程池核心参数的完整解释、Spring Boot 自动配置原理等,回答得不是很清晰全面。后续我们会综合评估所有面试者的情况,你先回家等通知吧,无论结果如何,我们都会在一周内给你回复。

问题答案

  1. ArrayList 和 HashMap 的底层数据结构
    • ArrayList:底层是数组结构,它可以动态扩容。数组的特点是可以根据索引快速访问元素,但插入和删除元素在非尾部位置时效率较低,因为需要移动元素。
    • HashMap:JDK7 及之前,底层是数组加链表结构。数组的每个位置是一个链表头节点。当发生哈希冲突(不同的键计算出相同的哈希值)时,会将新的键值对以链表的形式挂在对应数组位置的链表上。JDK8 之后,当链表长度达到 8 且数组容量大于等于 64 时,链表会转化为红黑树,以提高查找效率。红黑树是一种自平衡的二叉查找树,查找、插入和删除操作平均时间复杂度为 O(log n)。
  2. ArrayList 扩容操作
    • ArrayList 有一个容量(capacity)和实际元素个数(size)。当向 ArrayList 中添加元素时,如果 size 达到了 capacity,就会触发扩容。
    • 扩容时,新的容量是原来容量的 1.5 倍(oldCapacity + (oldCapacity >> 1))。然后会创建一个新的更大的数组,将旧数组中的元素复制到新数组中。这个复制过程使用 System.arraycopy 方法,它是一个本地方法,效率较高。
  3. HashMap 在 JDK8 中引入红黑树的阈值
    • 阈值是 8。当链表长度达到 8 时,并且此时数组容量大于等于 64,链表会转化为红黑树。这是因为链表在长度较短时,查找效率虽然是 O(n),但由于数据量小,性能影响不大。而当链表过长时,查找效率会显著下降,转化为红黑树可以将查找效率提高到 O(log n)。如果数组容量小于 64,优先进行扩容而不是转化为红黑树,因为扩容相对简单且能在一定程度上减少哈希冲突。
  4. 创建线程的方式
    • 继承 Thread 类:创建一个类继承 Thread 类,重写 run 方法,在 run 方法中编写线程执行的逻辑。然后创建该类的实例并调用 start 方法启动线程。例如:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行中");
    }
}
MyThread myThread = new MyThread();
myThread.start();
  • 实现 Runnable 接口:创建一个类实现 Runnable 接口,实现 run 方法。然后创建该类的实例,将其作为参数传递给 Thread 类的构造函数,再调用 start 方法启动线程。例如:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程执行中");
    }
}
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
  • 实现 Callable 接口:创建一个类实现 Callable 接口,实现 call 方法,call 方法可以有返回值并且可以抛出异常。通过 FutureTask 类来包装 Callable 实例,再将 FutureTask 作为参数传递给 Thread 类的构造函数启动线程。可以通过 FutureTask 的 get 方法获取 call 方法的返回值。例如:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 100;
    }
}
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
try {
    Integer result = futureTask.get();
    System.out.println("返回值: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}
  1. 线程池的核心参数
    • 核心线程数(corePoolSize):线程池中会一直存活的线程数量,即使这些线程处于空闲状态,也不会被销毁。当有新任务提交到线程池时,如果当前线程池中的线程数量小于 corePoolSize,会优先创建新的线程来处理任务。
    • 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数量。当任务队列已满,并且当前线程池中的线程数量小于 maximumPoolSize 时,会继续创建新的线程来处理任务。
    • 队列(workQueue):用于存放暂时无法处理的任务。当线程池中的线程数量达到 corePoolSize 后,新提交的任务会被放入队列中等待处理。常见的队列类型有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列,默认容量为 Integer.MAX_VALUE)、SynchronousQueue(不存储任务,直接提交给线程处理,如果没有空闲线程则任务会被拒绝)等。
    • 线程存活时间(keepAliveTime):当线程池中的线程数量超过 corePoolSize 时,多余的空闲线程在存活了 keepAliveTime 时间后会被销毁。
    • 时间单位(unit):keepAliveTime 的时间单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
  2. 线程池中的拒绝策略
    • AbortPolicy:这是默认的拒绝策略,当任务无法提交到线程池(队列已满且线程数达到 maximumPoolSize)时,直接抛出 RejectedExecutionException 异常。
    • DiscardPolicy:丢弃无法处理的任务,不抛出异常,也不会对任务进行任何处理。
    • DiscardOldestPolicy:丢弃队列中最老的任务(队头的任务),然后尝试将新任务提交到队列中。
    • CallerRunsPolicy:当任务无法提交到线程池时,由提交任务的线程(调用者线程)来执行该任务。这样可以降低新任务的提交速度,减轻线程池的压力。
  3. Spring 中 Bean 的作用域
    • singleton:单例模式,在一个 Spring 容器中,只会创建一个该 Bean 的实例。所有对该 Bean 的请求都会返回同一个实例。这是默认的作用域。例如,对于一些无状态的服务类,如 UserService,使用单例模式可以减少内存开销,因为不需要为每个请求创建新的实例。
    • prototype:原型模式,每次请求获取该 Bean 时,都会创建一个新的实例。适用于有状态的 Bean,比如 Struts2 的 Action 类,每个请求可能需要不同的状态,所以使用 prototype 作用域。
    • request:在一次 HTTP 请求中,只会创建一个该 Bean 的实例。当请求结束时,该实例会被销毁。适用于处理与 HTTP 请求相关的业务逻辑,如记录请求相关的日志等。
    • session:在一个 HTTP Session 中,只会创建一个该 Bean 的实例。当 Session 过期或被销毁时,该实例也会被销毁。适用于需要在整个 Session 生命周期内保持状态的场景,如用户登录信息的管理。
    • global - session:在一个全局的 HTTP Session 中(通常用于 Portlet 环境),只会创建一个该 Bean 的实例。
  4. Spring Boot 自动配置的原理
    • Spring Boot 基于 Spring 框架,它的自动配置是通过一系列的条件注解和配置类实现的。
    • 条件注解:例如 @ConditionalOnClass、@ConditionalOnProperty 等。@ConditionalOnClass 表示当类路径下存在某个类时,才会进行相关的配置。比如,如果类路径下存在 Tomcat 相关的类,Spring Boot 会自动配置 Tomcat 作为 Web 服务器。@ConditionalOnProperty 表示当配置文件中存在某个属性时,才会进行相关配置。
    • 配置类:Spring Boot 提供了大量的自动配置类,如 DataSourceAutoConfiguration 用于数据库连接池的自动配置,WebMvcAutoConfiguration 用于 Spring MVC 的自动配置等。这些配置类会根据条件注解来决定是否生效。
    • Spring Factories 机制:Spring Boot 使用 Spring Factories 机制来加载自动配置类。在 META - INF/spring.factories 文件中,定义了各种自动配置类的全限定名。Spring Boot 在启动时会扫描这些文件,加载并根据条件应用相应的自动配置类。
  5. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 来执行 SQL。这种方式可以有效防止 SQL 注入攻击,因为参数是作为占位符传递的,而不是直接拼接在 SQL 语句中。例如:
<select id="selectUserById" parameterType="int" resultType="User">
    SELECT * FROM user WHERE id = #{id}
</select>
  • **:是字符串替换方式,MyBatis在处理{}**:是字符串替换方式,MyBatis 在处理 {} 时,会直接将 ${} 中的内容替换到 SQL 语句中。如果参数值是用户输入的,并且没有进行严格的过滤,就可能导致 SQL 注入攻击。例如:
<select id="selectUserByUsername" parameterType="string" resultType="User">
    SELECT * FROM user WHERE username = '${username}'
</select>
  • 一般情况下,能用 #{} 的地方尽量使用 #{},只有在需要进行 SQL 片段拼接等特殊情况下才使用 ,并且使用{},并且使用 {} 时要对参数进行严格的过滤和验证。