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

48 阅读3分钟

第一轮面试 面试官:先问些基础的,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 注入,${} 不能。

面试官:今天的面试就到这里,你回去等通知吧。我们会综合评估所有候选人,之后给你回复。感谢你今天来参加面试,希望你之后也能继续提升自己的技术能力。

问题答案

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