第一轮面试 面试官:先问些基础的,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,不安全。
面试官:今天的面试就到这里,你回去等通知吧。我们会综合评估你的表现,无论结果如何,都会尽快给你答复。感谢你今天来参加面试。
问题答案:
- ArrayList 和 HashMap 的底层数据结构:
- ArrayList:底层是数组结构,它可以动态扩容,方便按索引快速访问元素。例如,在需要顺序存储大量数据且经常根据索引访问元素的场景下,如学生成绩按顺序记录,使用 ArrayList 很合适。
- HashMap:JDK7 及之前底层是数组加链表结构,JDK8 之后,当链表长度达到 8 且数组容量大于等于 64 时,链表会转成红黑树。数组的作用是通过哈希值快速定位元素位置,链表用于解决哈希冲突。在需要快速根据键获取值的场景,如用户信息通过用户 ID 快速查找,HashMap 效率很高。
- ArrayList 扩容操作:
- ArrayList 有一个默认初始容量,一般为 10。当添加元素时,如果当前元素个数达到了容量的 0.75 倍(加载因子默认是 0.75),就会触发扩容。扩容时,新容量是原来容量的 1.5 倍(通过位运算实现,
newCapacity = oldCapacity + (oldCapacity >> 1))。然后会创建一个新的数组,将旧数组中的元素复制到新数组中。这是为了在保证一定空间利用率的同时,避免频繁扩容带来的性能开销。
- ArrayList 有一个默认初始容量,一般为 10。当添加元素时,如果当前元素个数达到了容量的 0.75 倍(加载因子默认是 0.75),就会触发扩容。扩容时,新容量是原来容量的 1.5 倍(通过位运算实现,
- HashMap 链表转红黑树条件:
- 当链表长度达到 8 且数组容量大于等于 64 时,链表会转成红黑树。这是因为链表在长度较短时,查询性能还可以接受,但长度过长时,查询时间复杂度会退化为 O(n)。而红黑树的查询时间复杂度稳定为 O(logn),能提高查询效率。当数组容量小于 64 时,优先进行扩容而不是转红黑树,因为扩容相对转红黑树的成本更低。
- 创建线程的方式:
- 继承 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();
}
- 线程池核心参数:
- 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁,除非设置了 allowCoreThreadTimeOut 为 true。
- 最大线程数(maximumPoolSize):线程池允许创建的最大线程数。当任务队列已满且活动线程数小于最大线程数时,线程池会创建新的线程来处理任务。
- 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程的存活时间。即这些空闲线程在多长时间内没有任务执行就会被销毁。
- 时间单位(unit):存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
- 任务队列(workQueue):用于存放等待执行的任务。常用的任务队列有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)等。
- 线程池拒绝策略:
- AbortPolicy:这是默认的拒绝策略,当任务无法提交到线程池(队列已满且线程数达到最大线程数)时,直接抛出 RejectedExecutionException 异常。
- DiscardPolicy:丢弃无法处理的任务,不抛出任何异常。
- DiscardOldestPolicy:丢弃队列中最老的任务(即将队列头部的任务丢弃),然后尝试提交新任务。
- CallerRunsPolicy:当任务无法提交到线程池时,由提交任务的线程(调用者)来执行该任务。这样可以降低新任务的提交速度,缓解线程池的压力。
- 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 实例。
- 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 应用的配置过程。
- MyBatis 中 #{} 和 ${} 的区别:
- #{}:是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 来执行 SQL。这种方式可以有效防止 SQL 注入攻击,因为参数是作为占位符传递的,而不是直接拼接在 SQL 语句中。例如:
SELECT * FROM user WHERE username = #{username},在执行时会将 #{username} 替换为?,然后设置参数值。 - **{} 时,会直接将 {username}'
,如果 username 参数被恶意注入,如'; DROP TABLE user; --`,就会导致数据库表被删除。所以在使用 ${} 时,一定要对参数进行严格的校验和过滤。
- #{}:是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 来执行 SQL。这种方式可以有效防止 SQL 注入攻击,因为参数是作为占位符传递的,而不是直接拼接在 SQL 语句中。例如: