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

45 阅读9分钟

第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK8 之后引入了红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时是怎样的机制? 王铁牛:当 ArrayList 元素个数达到容量阈值时,会进行扩容,新容量是原容量的 1.5 倍,然后把原数组内容复制到新数组。 面试官:回答得很好。HashMap 在 JDK8 中引入红黑树,为什么要引入呢? 王铁牛:好像是为了优化查找效率,链表太长查找就慢,红黑树查找效率高。

第二轮面试 面试官:接下来聊聊多线程和线程池。创建线程有几种方式? 王铁牛:有三种,继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。 面试官:很好。那线程池的核心参数有哪些,分别代表什么含义? 王铁牛:有核心线程数、最大线程数、存活时间、阻塞队列。核心线程数就是一直存活的线程数,最大线程数是线程池能容纳的最大线程数,存活时间是线程在没有任务时存活的时间,阻塞队列是存放等待执行任务的队列。 面试官:还行。那线程池的拒绝策略有哪些? 王铁牛:有 AbortPolicy,直接抛出异常;CallerRunsPolicy,让调用者线程执行任务;DiscardPolicy,丢弃任务不抛异常;DiscardOldestPolicy,丢弃队列最老的任务。

第三轮面试 面试官:再谈谈框架相关。Spring 中 Bean 的作用域有哪些? 王铁牛:有 singleton 单例、prototype 原型、request、session 这些。 面试官:好。Spring Boot 自动配置的原理是什么? 王铁牛:嗯……好像是通过一些配置类和条件注解,自动帮我们配置一些组件。 面试官:不太清晰。那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,{} 是字符串替换,#{} 能防止 SQL 注入。 面试官:最后问下分布式相关,Dubbo 的服务暴露过程是怎样的? 王铁牛:呃……就是通过一些协议把服务暴露出去,具体不太清楚。 面试官:好的,今天的面试就到这里。你回去等通知吧,我们会综合评估所有候选人后,再做决定。感谢你今天来参加面试。

答案:

  1. ArrayList 和 HashMap 底层数据结构
    • ArrayList:底层是数组结构,它允许我们以数组下标的方式快速访问元素,适合随机访问场景。例如,在需要频繁根据索引获取元素的业务场景,如分页查询结果集的获取某一页数据,使用 ArrayList 就很合适。
    • HashMap:JDK7 及之前底层是数组加链表,JDK8 引入红黑树优化。数组部分通过哈希算法确定元素存放位置,链表解决哈希冲突。当链表长度超过阈值(8)且数组容量大于 64 时,链表会转化为红黑树,以提高查找效率。在需要快速根据 key 获取 value 的场景,如用户信息缓存,根据用户 ID 快速获取用户信息,HashMap 就很适用。
  2. ArrayList 扩容机制
    • ArrayList 有一个容量(capacity)和实际元素个数(size)。当 size 达到 capacity 时,就会触发扩容。新容量是原容量的 1.5 倍(原容量左移一位再加 1)。扩容时,会创建一个新的更大的数组,然后将原数组的元素复制到新数组。这是为了在动态添加元素时,既能保证一定的空间利用率,又不会频繁扩容影响性能。比如在批量插入数据时,如果提前知道数据量,可以通过构造函数指定初始容量,减少扩容次数。
  3. HashMap 引入红黑树原因
    • 在 JDK7 及之前,HashMap 处理哈希冲突是链表方式。当哈希冲突严重,链表会很长,导致查找效率降低,时间复杂度从 O(1) 退化为 O(n)。引入红黑树后,当链表长度过长(超过 8 且数组容量大于 64),链表转化为红黑树,红黑树的查找时间复杂度为 O(logn),大大提高了查找效率,尤其是在数据量较大且哈希冲突较多的场景下。
  4. 创建线程方式
    • 继承 Thread 类:通过继承 Thread 类,重写 run 方法,然后创建该类实例并调用 start 方法启动线程。例如:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行");
    }
}
MyThread thread = new MyThread();
thread.start();
  • 实现 Runnable 接口:实现 Runnable 接口的 run 方法,将该实现类实例作为参数传递给 Thread 构造函数创建线程。例如:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程执行");
    }
}
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
  • 实现 Callable 接口:实现 Callable 接口的 call 方法,与 Runnable 不同的是,call 方法有返回值。通过 FutureTask 类来包装 Callable 实例,再将 FutureTask 作为参数传递给 Thread 构造函数创建线程。例如:
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 callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
    Integer result = futureTask.get();
    System.out.println("线程返回结果:" + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}
  1. 线程池核心参数
    • 核心线程数(corePoolSize):线程池在正常情况下保持的线程数,即使这些线程处于空闲状态,也不会被销毁(除非设置了 allowCoreThreadTimeOut 为 true)。例如,在一个 Web 服务器中,核心线程数可以设置为能处理日常平均请求量的线程数,让这些线程一直存活,随时处理请求。
    • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数。当任务队列已满且核心线程都在忙碌时,线程池会创建新线程,直到达到最大线程数。比如在电商大促期间,请求量剧增,线程池可能会创建更多线程直到最大线程数来处理请求。
    • 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程存活的最长时间。超过这个时间,这些空闲线程会被销毁。例如,在一个批处理任务系统中,任务执行完后,多余的线程在存活时间后会被销毁,避免资源浪费。
    • 阻塞队列(workQueue):用于存放等待执行的任务。常见的阻塞队列有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列,默认容量为 Integer.MAX_VALUE)、SynchronousQueue(不存储任务,直接提交给线程处理)等。不同的阻塞队列适用于不同的场景,如 ArrayBlockingQueue 适用于需要控制任务队列大小的场景,防止任务堆积过多导致内存溢出。
  2. 线程池拒绝策略
    • AbortPolicy:这是默认的拒绝策略,当任务无法提交到线程池(队列已满且线程数达到最大线程数)时,直接抛出 RejectedExecutionException 异常。适用于需要立即知道任务是否被成功处理的场景,如一些关键业务操作,不允许任务丢失。
    • CallerRunsPolicy:当任务被拒绝时,会将任务交给调用者线程来执行。这样可以降低新任务的提交速度,减轻线程池的压力。例如在一个简单的单线程调用线程池的场景下,当线程池繁忙时,调用者线程自己执行任务,避免任务丢失。
    • DiscardPolicy:直接丢弃被拒绝的任务,不抛出任何异常。适用于对任务处理结果不敏感,允许部分任务丢失的场景,如一些日志记录任务,偶尔丢失一两条日志不影响整体业务。
    • DiscardOldestPolicy:丢弃队列中最老的任务(即将被执行的任务),然后尝试提交新任务。适用于希望优先处理新任务的场景,比如在实时数据处理系统中,新数据更重要,丢弃旧数据以处理新数据。
  3. Spring Bean 作用域
    • singleton:单例模式,在整个 Spring 容器中,只会创建一个该 Bean 的实例。例如,数据库连接池的配置 Bean,只需要一个实例供整个应用使用,就可以设置为 singleton 作用域。
    • prototype:原型模式,每次从容器中获取 Bean 时,都会创建一个新的实例。比如在多线程环境下,每个线程需要独立的对象实例,就可以将相关 Bean 设置为 prototype 作用域。
    • request:在一次 HTTP 请求中,只会创建一个该 Bean 的实例。适用于 Web 应用中,与请求相关的 Bean,如处理请求参数的 Bean,每个请求需要独立的实例。
    • session:在一个 HTTP Session 范围内,只会创建一个该 Bean 的实例。适用于与用户会话相关的 Bean,如记录用户登录信息的 Bean,在用户会话期间保持同一个实例。
  4. Spring Boot 自动配置原理
    • Spring Boot 自动配置是基于条件配置(@Conditional 注解及其衍生注解)和 Spring Factories 机制。在 Spring Boot 的启动过程中,会扫描 META - INF/spring.factories 文件,该文件中定义了各种自动配置类。这些自动配置类会根据条件注解(如 @ConditionalOnClass、@ConditionalOnProperty 等)来判断是否需要进行配置。例如,如果 classpath 下存在某个特定的类(@ConditionalOnClass),或者某个配置属性满足特定条件(@ConditionalOnProperty),就会自动配置相关的 Bean。比如 Spring Boot 对数据库连接的自动配置,当 classpath 下存在 MySQL 驱动相关类时,并且配置文件中有相关数据库连接配置属性,就会自动配置 MySQL 连接的 Bean。
  5. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行参数设置。这样可以有效防止 SQL 注入攻击,因为参数是作为字符串传入,而不是直接嵌入 SQL 语句。例如:
<select id="selectUser" parameterType="int" resultType="User">
    SELECT * FROM user WHERE id = #{id}
</select>
  • **:是字符串替换,MyBatis在处理{}**:是字符串替换,MyBatis 在处理 {} 时,会直接将 ${} 替换为变量的值,这种方式存在 SQL 注入风险。例如:
<select id="selectUserByUsername" parameterType="string" resultType="User">
    SELECT * FROM user WHERE username = '${username}'
</select>

如果传入的 username 为 “'; DROP TABLE user; --”,就会导致数据库表被删除。所以在使用 ${} 时,一定要对传入的值进行严格的校验和过滤。 10. Dubbo 服务暴露过程

  • 配置解析:Dubbo 首先会解析服务提供者的配置文件,获取服务接口、实现类、协议、端口等信息。例如,在 Spring 配置文件中配置的 dubbo:service 标签相关信息。
  • Proxy 生成:通过 Javassist 或 JDK 动态代理等技术,生成服务接口的代理类。这个代理类封装了服务调用的逻辑,包括网络通信等。
  • 协议绑定:根据配置的协议(如 Dubbo 协议、HTTP 协议等),将服务绑定到指定的端口。以 Dubbo 协议为例,会创建 Netty 服务器(默认),监听指定端口。
  • 注册中心注册:将服务的元数据(接口名、版本号、服务地址等)注册到注册中心(如 Zookeeper、Nacos 等)。注册中心用于服务的发现和治理,消费者可以从注册中心获取服务提供者的地址等信息。
  • 服务暴露完成:完成上述步骤后,服务就成功暴露,可以接受消费者的调用。