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

49 阅读7分钟

第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK8 之后还有红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时是怎么操作的? 王铁牛:当元素个数达到容量的一定比例,好像是 0.75 吧,就会扩容,新容量是原来的 1.5 倍,然后把旧数组的数据复制到新数组。 面试官:很好。HashMap 在 JDK8 中,链表转红黑树的条件是什么? 王铁牛:当链表长度达到 8,并且数组容量大于等于 64 时,链表就会转成红黑树。

第二轮面试 面试官:接下来聊聊多线程和线程池。创建线程有几种方式? 王铁牛:有三种,继承 Thread 类,实现 Runnable 接口,还有实现 Callable 接口。 面试官:嗯,那线程池有哪些核心参数? 王铁牛:有核心线程数、最大线程数、存活时间、时间单位,还有任务队列。 面试官:那线程池的拒绝策略有哪些? 王铁牛:呃……有 AbortPolicy,直接抛出异常;还有 DiscardPolicy,丢弃任务不抛异常,其他的……我不太记得了。

第三轮面试 面试官:再谈谈框架相关的。Spring 中 Bean 的作用域有哪些? 王铁牛:有 singleton,单例模式,整个应用就一个实例;还有 prototype,每次请求都会创建新的实例。 面试官:Spring Boot 自动配置的原理是什么? 王铁牛:就是通过一些注解和配置类,Spring Boot 能自动帮我们配置一些常用的组件,具体怎么实现的我不太清楚。 面试官:MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,能防止 SQL 注入,{} 是直接拼接 SQL,不安全。

面试官:今天的面试就到这里,你回去等通知吧。我们会综合评估你的表现,无论结果如何,都会尽快给你答复。感谢你今天来参加面试。

问题答案

  1. ArrayList 和 HashMap 的底层数据结构
    • ArrayList:底层是数组结构,它可以动态扩容,方便按索引快速访问元素。例如,在需要顺序存储大量数据且经常根据索引访问元素的场景下,如学生成绩按顺序记录,使用 ArrayList 很合适。
    • HashMap:JDK7 及之前底层是数组加链表结构,JDK8 之后,当链表长度达到 8 且数组容量大于等于 64 时,链表会转成红黑树。数组的作用是通过哈希值快速定位元素位置,链表用于解决哈希冲突。在需要快速根据键获取值的场景,如用户信息通过用户 ID 快速查找,HashMap 效率很高。
  2. ArrayList 扩容操作
    • ArrayList 有一个默认初始容量,一般为 10。当添加元素时,如果当前元素个数达到了容量的 0.75 倍(加载因子默认是 0.75),就会触发扩容。扩容时,新容量是原来容量的 1.5 倍(通过位运算实现,newCapacity = oldCapacity + (oldCapacity >> 1))。然后会创建一个新的数组,将旧数组中的元素复制到新数组中。这是为了在保证一定空间利用率的同时,避免频繁扩容带来的性能开销。
  3. HashMap 链表转红黑树条件
    • 当链表长度达到 8 且数组容量大于等于 64 时,链表会转成红黑树。这是因为链表在长度较短时,查询性能还可以接受,但长度过长时,查询时间复杂度会退化为 O(n)。而红黑树的查询时间复杂度稳定为 O(logn),能提高查询效率。当数组容量小于 64 时,优先进行扩容而不是转红黑树,因为扩容相对转红黑树的成本更低。
  4. 创建线程的方式
    • 继承 Thread 类:通过继承 Thread 类,重写 run 方法来定义线程执行的逻辑。例如:
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。
    • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数。当任务队列已满且活动线程数小于最大线程数时,线程池会创建新的线程来处理任务。
    • 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程的存活时间。即这些空闲线程在多长时间内没有任务执行就会被销毁。
    • 时间单位(unit):存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
    • 任务队列(workQueue):用于存放等待执行的任务。常用的任务队列有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)等。
  2. 线程池拒绝策略
    • AbortPolicy:这是默认的拒绝策略,当任务无法提交到线程池(队列已满且线程数达到最大线程数)时,直接抛出 RejectedExecutionException 异常。
    • DiscardPolicy:丢弃无法处理的任务,不抛出任何异常。
    • DiscardOldestPolicy:丢弃队列中最老的任务(即将队列头部的任务丢弃),然后尝试提交新任务。
    • CallerRunsPolicy:当任务无法提交到线程池时,由提交任务的线程(调用者)来执行该任务。这样可以降低新任务的提交速度,缓解线程池的压力。
  3. Spring 中 Bean 的作用域
    • singleton:单例模式,在整个 Spring 应用中,只会创建一个该 Bean 的实例。例如,数据库连接池的配置 Bean,只需要一个实例来管理数据库连接,使用 singleton 作用域可以避免重复创建连接池带来的资源浪费。
    • prototype:原型模式,每次请求获取该 Bean 时,都会创建一个新的实例。比如在处理多用户请求时,每个用户的会话相关的 Bean 可以设置为 prototype 作用域,保证每个用户的数据相互隔离。
    • request:在一次 HTTP 请求中,只会创建一个该 Bean 的实例。适用于处理与 HTTP 请求相关的业务逻辑,如记录请求日志的 Bean,每个请求创建一个实例记录该请求的日志。
    • session:在一个 HTTP Session 中,只会创建一个该 Bean 的实例。常用于处理与用户会话相关的业务,如用户登录信息的 Bean,在用户整个会话期间保持同一个实例。
    • globalSession:在一个全局的 HTTP Session 中,只会创建一个该 Bean 的实例。主要用于 Portlet 应用中,在 Portlet 的全局会话中共享 Bean 实例。
  4. Spring Boot 自动配置原理
    • Spring Boot 利用了 Spring 4.0 引入的条件化配置(@Conditional)功能。在 Spring Boot 的启动过程中,会扫描 META - INF/spring.factories 文件,该文件中定义了大量的自动配置类。这些自动配置类会根据项目的依赖和配置情况,通过 @Conditional 注解的条件判断来决定是否生效。例如,如果项目中引入了 spring - data - jpa 依赖,那么 Spring Boot 会自动配置 JPA 相关的 Bean,如 EntityManagerFactory、JpaTransactionManager 等。自动配置类会根据条件判断,如是否存在某个类、是否有特定的配置属性等,来决定是否创建相应的 Bean 并注入到 Spring 容器中,大大简化了 Spring 应用的配置过程。
  5. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 来执行 SQL。这种方式可以有效防止 SQL 注入攻击,因为参数是作为占位符传递的,而不是直接拼接在 SQL 语句中。例如:SELECT * FROM user WHERE username = #{username},在执行时会将 #{username} 替换为?,然后设置参数值。
    • **:是字符串替换方式,MyBatis在处理{}**:是字符串替换方式,MyBatis 在处理 {} 时,会直接将 中的内容替换到SQL语句中。这种方式存在SQL注入风险,因为参数直接拼接在SQL中。例如:SELECTFROMuserWHEREusername={} 中的内容替换到 SQL 语句中。这种方式存在 SQL 注入风险,因为参数直接拼接在 SQL 中。例如:`SELECT * FROM user WHERE username = '{username}',如果 username 参数被恶意注入,如 '; DROP TABLE user; --`,就会导致数据库表被删除。所以在使用 ${} 时,一定要对参数进行严格的校验和过滤。