第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 之后链表长度超过 8 会转成红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:当 ArrayList 元素个数达到容量大小时,会进行扩容,新容量是原来容量的 1.5 倍,然后把旧数组的元素复制到新数组。 面试官:回答得很好。HashMap 在 put 操作时,如何确定元素在数组中的位置? 王铁牛:通过 key 的 hashCode 与数组长度减 1 进行按位与运算,得到的结果就是元素在数组中的位置。
第二轮面试 面试官:接下来聊聊多线程和线程池。创建线程有几种方式? 王铁牛:有三种,继承 Thread 类,实现 Runnable 接口,还有实现 Callable 接口结合 FutureTask。 面试官:嗯,还行。那线程池的核心参数有哪些,分别代表什么含义? 王铁牛:有核心线程数、最大线程数、存活时间、时间单位、任务队列。核心线程数就是一直存活的线程数,最大线程数是线程池能容纳的最大线程数,存活时间是线程在没有任务时最多存活的时间,时间单位就是存活时间的单位,任务队列就是存放任务的队列。 面试官:那线程池处理任务的流程是怎样的? 王铁牛:嗯……就是先看核心线程有没有满,没满就创建核心线程处理任务,满了就放队列里,队列满了就看最大线程数,没达到就创建非核心线程处理,达到了就执行拒绝策略。
第三轮面试 面试官:谈谈 Spring 和 Spring Boot。Spring 中 Bean 的生命周期是怎样的? 王铁牛:嗯……就是先实例化,然后进行属性注入,接着如果实现了一些 Aware 接口,会进行相应的回调,再然后初始化,最后使用,销毁的时候执行销毁方法。 面试官:Spring Boot 自动配置的原理是什么? 王铁牛:就是通过一些配置类,根据类路径下是否存在某些类,来自动配置一些 Bean。 面试官:那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,{} 是字符串替换,#{} 能防止 SQL 注入,${} 不能。
面试官:今天的面试就到这里,你回去等通知吧。我们会综合评估所有候选人,之后给你回复。感谢你今天来参加面试,希望你之后也能继续提升自己的技术能力。
问题答案:
- ArrayList 和 HashMap 的底层数据结构:
- ArrayList:底层是数组结构,它可以动态扩容。这种结构使得它在随机访问元素时效率很高,因为可以通过数组下标直接定位元素。例如
list.get(5)可以快速获取到索引为 5 的元素。 - HashMap:JDK1.7 及之前,底层是数组加链表。数组的每个位置是一个链表的头节点。当发生哈希冲突(不同的 key 计算出相同的哈希值)时,就会将新的键值对以链表的形式挂在该位置。JDK1.8 之后,当链表长度超过 8 且数组容量大于等于 64 时,链表会转成红黑树,以提高查找效率。因为红黑树的查找时间复杂度为 O(logn),相比链表的 O(n) 更高效。
- ArrayList:底层是数组结构,它可以动态扩容。这种结构使得它在随机访问元素时效率很高,因为可以通过数组下标直接定位元素。例如
- ArrayList 扩容操作:
- 当 ArrayList 中元素个数达到其容量大小时,会触发扩容机制。新容量是原来容量的 1.5 倍(通过
oldCapacity + (oldCapacity >> 1)计算得到,oldCapacity >> 1相当于oldCapacity / 2)。然后会创建一个新的更大的数组,并将旧数组中的元素复制到新数组中。这个复制过程是通过System.arraycopy()方法实现的。例如,原数组容量为 10,当添加第 11 个元素时,就会扩容到 15。
- 当 ArrayList 中元素个数达到其容量大小时,会触发扩容机制。新容量是原来容量的 1.5 倍(通过
- HashMap put 操作确定元素位置:
- 首先计算 key 的 hashCode 值,然后通过
(n - 1) & hash(n 为数组长度)来确定元素在数组中的位置。这里使用按位与运算而不是取模运算,是因为按位与运算在计算机底层执行效率更高。例如,数组长度 n = 16,其n - 1 = 15,二进制表示为1111。假设 key 的 hashCode 为 10,二进制表示为1010,那么1010 & 1111 = 1010,十进制为 10,即元素会被存放在数组索引为 10 的位置。
- 首先计算 key 的 hashCode 值,然后通过
- 创建线程的方式:
- 继承 Thread 类:创建一个类继承 Thread 类,重写 run 方法,在 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 接口结合 FutureTask:实现 Callable 接口,重写 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 10;
}
}
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):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁。当有新任务提交时,如果当前线程数小于核心线程数,会优先创建核心线程来处理任务。
- 最大线程数(maximumPoolSize):线程池能容纳的最大线程数。当任务队列已满,且当前线程数小于最大线程数时,会创建非核心线程来处理任务。
- 存活时间(keepAliveTime):非核心线程在没有任务执行时,最多存活的时间。超过这个时间,非核心线程会被销毁。
- 时间单位(unit):存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
- 任务队列(workQueue):用于存放等待处理的任务。常见的任务队列有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)等。
- 线程池处理任务流程:
- 当有新任务提交到线程池时,首先判断当前线程数是否小于核心线程数。如果小于,会创建核心线程来处理任务。
- 如果当前线程数已经达到核心线程数,任务会被放入任务队列中等待处理。
- 如果任务队列已满,再判断当前线程数是否小于最大线程数。如果小于,会创建非核心线程来处理任务。
- 如果当前线程数已经达到最大线程数,且任务队列也已满,此时线程池会执行拒绝策略。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(将任务交给调用者线程处理)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试重新提交当前任务)。
- Spring 中 Bean 的生命周期:
- 实例化(Instantiation):通过构造函数创建 Bean 的实例对象。
- 属性注入(Populate):使用依赖注入(DI)的方式,将 Bean 所依赖的其他对象注入到该 Bean 中。例如,通过
@Autowired注解实现自动装配。 - Aware 接口回调:如果 Bean 实现了
BeanNameAware、BeanFactoryAware、ApplicationContextAware等 Aware 接口,Spring 容器会调用相应的回调方法,让 Bean 获取到容器相关的信息。例如,实现BeanNameAware接口的setBeanName方法,可以获取到当前 Bean 在容器中的名称。 - 初始化(Initialization):如果 Bean 实现了
InitializingBean接口,会调用其afterPropertiesSet方法;或者配置了init - method属性,会调用指定的初始化方法。这个阶段用于完成一些初始化操作,如数据库连接的建立等。 - 使用(Usage):Bean 可以被应用程序使用,提供相应的服务。
- 销毁(Destruction):当 Bean 不再需要时,Spring 容器会销毁它。如果 Bean 实现了
DisposableBean接口,会调用其destroy方法;或者配置了destroy - method属性,会调用指定的销毁方法。例如,关闭数据库连接等操作可以放在这里。
- Spring Boot 自动配置原理:
- Spring Boot 基于约定大于配置的理念,通过自动配置机制简化了 Spring 应用的开发。它的核心是
@EnableAutoConfiguration注解,该注解导入了AutoConfigurationImportSelector类。 AutoConfigurationImportSelector会从META - INF/spring.factories文件中读取所有的自动配置类。这些自动配置类通常以AutoConfiguration结尾,如DataSourceAutoConfiguration。- 每个自动配置类会根据类路径下是否存在某些特定的类、配置属性等条件来决定是否生效。例如,
DataSourceAutoConfiguration会检查类路径下是否存在数据库连接相关的类(如HikariDataSource),如果存在且相关配置属性满足条件,就会自动配置数据源。 - 自动配置类会使用
@Conditional系列注解(如@ConditionalOnClass、@ConditionalOnProperty等)来控制配置的生效条件。@ConditionalOnClass表示当类路径下存在某个类时才生效,@ConditionalOnProperty表示当配置文件中某个属性满足条件时才生效。
- Spring Boot 基于约定大于配置的理念,通过自动配置机制简化了 Spring 应用的开发。它的核心是
- MyBatis 中 #{} 和 ${} 的区别:
- #{}:是预编译处理方式。MyBatis 在处理
#{}时,会将 SQL 中的#{}替换为?,然后使用 PreparedStatement 来执行 SQL。这种方式可以有效防止 SQL 注入攻击,因为参数是作为占位符传递的,而不是直接嵌入 SQL 语句中。例如:SELECT * FROM user WHERE username = #{username},实际执行时会将#{username}替换为?,然后设置参数值。 - **{}
时,会直接将{username}',如果username的值为' OR '1' = '1',就会导致 SQL 语句被篡改,查询出所有用户数据。所以在使用${}` 时,要确保传入的值是安全的,通常用于传入表名、列名等不会引起 SQL 注入的场景。
- #{}:是预编译处理方式。MyBatis 在处理