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

44 阅读3分钟

第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 之后引入了红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:当 ArrayList 元素个数达到容量阈值时,会创建一个新的数组,新数组容量是原数组的 1.5 倍,然后把原数组的元素复制到新数组。 面试官:很好。HashMap 在 JDK1.8 中,链表转红黑树的条件是什么? 王铁牛:当链表长度大于等于 8 且数组容量大于等于 64 时,链表会转成红黑树。

第二轮面试 面试官:接下来聊聊多线程和线程池。创建线程有哪些方式? 王铁牛:可以继承 Thread 类,或者实现 Runnable 接口,还能通过 Callable 和 Future 创建有返回值的线程。 面试官:嗯,回答得可以。那线程池的核心参数有哪些,分别代表什么含义? 王铁牛:有核心线程数、最大线程数、存活时间、时间单位、任务队列。核心线程数就是线程池初始化的线程数量,最大线程数是线程池能容纳的最大线程数,存活时间是线程在没有任务时最多存活的时间,时间单位就是存活时间的单位,任务队列用来存放提交但未执行的任务。 面试官:那线程池中的拒绝策略有哪些? 王铁牛:呃……有那个……AbortPolicy,这个是直接抛出异常,还有……DiscardPolicy,好像是直接丢弃任务,其他的我一下子想不起来了。

第三轮面试 面试官:再谈谈框架相关的。Spring 中 Bean 的作用域有哪些? 王铁牛:有 singleton,单例模式,整个应用中只有一个实例;prototype,每次请求都会创建一个新的实例;还有 request、session 等,分别用于 Web 应用的不同场景。 面试官:好。Spring Boot 自动配置的原理是什么? 王铁牛:嗯……就是它会根据类路径下的依赖和配置文件,自动配置一些 Bean 吧,具体原理我也不是特别清楚。 面试官:那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,{} 是字符串替换,#{} 能防止 SQL 注入,${} 可能会有 SQL 注入风险。 面试官:最后问下分布式相关的,Dubbo 的服务暴露过程是怎样的? 王铁牛:呃,好像是通过注册中心,然后……具体细节我不太记得了。

面试总结:从这三轮面试来看,你在一些基础知识的掌握上还是不错的,像 ArrayList、HashMap 的底层结构,创建线程的方式等回答得都比较准确。但在一些稍微深入的问题上,比如线程池拒绝策略的完整回答,Spring Boot 自动配置原理,Dubbo 服务暴露过程等,回答得不是很清晰,还有提升的空间。我们后续会综合评估所有面试者的情况,你回家等通知吧,无论结果如何,我们都会在一周内给你回复。

问题答案

  1. ArrayList 和 HashMap 的底层数据结构
    • ArrayList:底层是数组结构,它允许以数组下标的方式快速访问元素。数组的大小会根据实际存储元素的数量动态调整。
    • HashMap:JDK1.8 之前底层是数组加链表结构,数组的每个位置是一个链表头节点。JDK1.8 之后,当链表长度大于等于 8 且数组容量大于等于 64 时,链表会转成红黑树,以提高查找效率。这样在哈希冲突较严重时,通过红黑树的特性(如自平衡等),能让查找操作的时间复杂度从链表的 O(n) 优化到红黑树的 O(logn)。
  2. ArrayList 扩容操作
    • 当 ArrayList 中元素个数达到容量阈值(一般是当前容量的 0.75 倍)时,会触发扩容。
    • 扩容时,会创建一个新的数组,新数组容量是原数组的 1.5 倍(原容量右移一位再加原容量)。
    • 然后通过 System.arraycopy 方法将原数组的元素复制到新数组中。这样就完成了 ArrayList 的扩容,使得它可以继续存储更多元素。
  3. HashMap 链表转红黑树条件
    • 链表长度大于等于 8:当链表长度过长时,查找元素的时间复杂度会趋近于 O(n),为了优化查找效率,考虑转成红黑树。
    • 数组容量大于等于 64:如果数组容量过小,转成红黑树可能会增加空间复杂度且收益不大,所以需要数组容量达到一定程度才进行转换。当满足这两个条件时,HashMap 会将链表转成红黑树,以提高查找性能。
  4. 创建线程的方式
    • 继承 Thread 类:创建一个类继承 Thread 类,重写 run 方法,在 run 方法中编写线程执行的逻辑。然后创建该类的实例,调用 start 方法启动线程。例如:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行逻辑");
    }
}
MyThread thread = new MyThread();
thread.start();
  • 实现 Runnable 接口:创建一个类实现 Runnable 接口,实现 run 方法。然后创建该类的实例,将其作为参数传递给 Thread 类的构造函数,再调用 start 方法启动线程。例如:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程执行逻辑");
    }
}
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
  • 通过 Callable 和 Future 创建有返回值的线程:创建一个类实现 Callable 接口,实现 call 方法,该方法有返回值。通过 FutureTask 类来包装 Callable 实例,再将 FutureTask 作为参数传递给 Thread 类的构造函数启动线程。可以通过 FutureTask 的 get 方法获取线程执行的返回结果。例如:
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。
    • 最大线程数(maximumPoolSize):线程池能容纳的最大线程数。当任务队列已满且活动线程数小于最大线程数时,线程池会创建新的线程来处理任务。
    • 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。
    • 时间单位(unit):存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
    • 任务队列(workQueue):用于存放提交但未执行的任务。常见的任务队列有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)、SynchronousQueue(同步队列)等。不同的任务队列特性会影响线程池的行为,比如有界队列在队列满时可能会触发创建新线程或者拒绝任务等操作。
  2. 线程池拒绝策略
    • AbortPolicy:这是默认的拒绝策略,当任务无法执行时,直接抛出 RejectedExecutionException 异常。
    • DiscardPolicy:直接丢弃任务,不做任何处理,也不会抛出异常。
    • DiscardOldestPolicy:丢弃队列中最老的任务(即将队列头部的任务出队),然后尝试重新提交当前任务。
    • CallerRunsPolicy:由提交任务的线程来执行该任务,而不是线程池中的线程执行。这样可以降低新任务的提交速度,减轻线程池的压力。
  3. Spring 中 Bean 的作用域
    • singleton(单例):在整个 Spring 应用中,只会创建一个该 Bean 的实例。所有对该 Bean 的请求都会返回同一个实例,适用于无状态的 Bean,如一些工具类 Bean。
    • prototype(原型):每次请求获取该 Bean 时,都会创建一个新的实例。适用于有状态的 Bean,比如处理不同用户请求的业务对象,每个请求都需要一个独立的实例。
    • request:在一次 HTTP 请求中,只会创建一个该 Bean 的实例。当请求结束时,该 Bean 实例会被销毁。适用于 Web 应用中与请求相关的 Bean,比如处理请求参数的 Bean。
    • session:在一个 HTTP Session 范围内,只会创建一个该 Bean 的实例。当 Session 过期或被销毁时,该 Bean 实例也会被销毁。适用于与用户会话相关的 Bean,比如保存用户登录信息的 Bean。
    • application:在 ServletContext 范围内,只会创建一个该 Bean 的实例。整个 Web 应用共享该实例,类似于 singleton 作用域,但它是基于 ServletContext 的。
  4. Spring Boot 自动配置原理
    • 条件注解:Spring Boot 使用了大量的条件注解,如 @ConditionalOnClass、@ConditionalOnProperty 等。这些注解会根据类路径下是否存在某个类、配置文件中是否存在某个属性等条件来决定是否自动配置某个 Bean。
    • SpringFactoriesLoader:Spring Boot 通过 META - INF/spring.factories 文件来加载自动配置类。在这个文件中,定义了各种自动配置类的全限定名。Spring Boot 在启动时,会通过 SpringFactoriesLoader 加载这些自动配置类。
    • 自动配置类:这些自动配置类会根据条件注解的判断,为应用程序自动配置各种 Bean,比如数据源、数据库连接池、Web 服务器等。例如,当类路径下存在 HikariCP 的相关类时,Spring Boot 会自动配置 HikariCP 作为数据库连接池。
  5. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行设置参数值,这样可以有效防止 SQL 注入。例如:SELECT * FROM user WHERE username = #{username},实际执行的 SQL 是 SELECT * FROM user WHERE username =?,然后通过 PreparedStatement 的 setString 等方法设置参数值。
    • **:是字符串替换,MyBatis在处理{}**:是字符串替换,MyBatis 在处理 {} 时,会直接将 中的内容替换到SQL中。如果参数值是字符串,不会添加单引号等,所以可能会导致SQL注入风险。例如:SELECTFROMuserWHEREusername={} 中的内容替换到 SQL 中。如果参数值是字符串,不会添加单引号等,所以可能会导致 SQL 注入风险。例如:`SELECT * FROM user WHERE username = '{username}',如果传入的 username 是 '; DROP TABLE user; --`,则会执行恶意的 SQL 语句。一般在使用 ${} 时,要确保参数值是安全的,比如用于表名、列名等替换。
  6. Dubbo 服务暴露过程
  • 配置解析:Dubbo 首先会解析服务提供者的配置文件,获取服务接口、实现类、注册中心地址等信息。
  • 代理对象创建:通过动态代理技术(如 Javassist 或 JDK 动态代理)创建服务接口的代理对象,这个代理对象封装了服务调用的逻辑。
  • 协议适配:根据配置选择合适的协议(如 Dubbo 协议、HTTP 协议等),将服务接口和实现类封装成协议相关的消息格式。
  • 注册中心注册:将服务的元数据(如服务接口、版本、分组等信息)注册到注册中心(如 Zookeeper、Nacos 等)。注册中心会维护服务提供者的地址列表等信息。
  • 监听端口:服务提供者启动一个网络服务,监听指定的端口,等待消费者的请求。当消费者发起请求时,注册中心会将服务提供者的地址返回给消费者,消费者通过网络通信调用服务提供者的接口。