第一轮面试 面试官:首先问几个基础问题。Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,在 JDK1.8 之后,链表长度大于 8 会转成红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:当 ArrayList 元素个数达到容量大小时,会进行扩容,新容量是原容量的 1.5 倍,然后把原数组内容复制到新数组。 面试官:很好。那 HashMap 在 put 一个元素时,具体的流程是怎样的? 王铁牛:先计算 key 的 hash 值,根据 hash 值找到对应的数组位置,如果该位置为空,就直接插入;如果不为空,就判断 key 是否相同,相同就覆盖,不同就看是链表还是红黑树,链表就插入链表尾部,红黑树就按红黑树规则插入。
第二轮面试 面试官:接下来问些多线程相关的。讲讲线程池的核心参数有哪些,分别代表什么含义? 王铁牛:有核心线程数、最大线程数、存活时间、时间单位,还有任务队列。核心线程数就是线程池一开始创建的线程数量,最大线程数是线程池能容纳的最大线程数,存活时间是线程在没有任务时存活的时间,时间单位就是存活时间对应的单位,任务队列就是存放任务的地方。 面试官:还行。那多线程环境下,如何保证线程安全,说几种方式? 王铁牛:可以用 synchronized 关键字,还有 Lock 接口。 面试官:那 synchronized 关键字在静态方法和实例方法上使用有什么区别? 王铁牛:嗯……在静态方法上,锁的是类对象,在实例方法上,锁的是实例对象。
第三轮面试 面试官:现在问些框架相关的。Spring 中 Bean 的生命周期是怎样的? 王铁牛:嗯……先实例化,然后进行属性注入,接着如果实现了一些 Aware 接口,会回调相应方法,再进行初始化,最后使用,销毁的时候执行销毁方法。 面试官:那 Spring Boot 相对于 Spring 有什么优势? 王铁牛:Spring Boot 更方便,它有自动配置,能快速搭建项目,减少配置文件。 面试官:MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,能防止 SQL 注入,{} 是直接替换,可能有 SQL 注入风险。
面试官:今天的面试就到这里,你回去等通知吧。我们会综合评估你的表现,无论结果如何,都会尽快给你回复。感谢你今天来参加面试。
答案:
- ArrayList 和 HashMap 底层数据结构:
- ArrayList:底层是数组结构,它可以动态扩容。数组的特点是内存连续,随机访问效率高,根据索引获取元素时间复杂度为 O(1)。
- HashMap:JDK1.8 之前底层是数组加链表,数组的每个位置是一个链表头。JDK1.8 之后,当链表长度大于 8 且数组容量大于 64 时,链表会转成红黑树。红黑树是一种自平衡的二叉查找树,查找、插入、删除的时间复杂度在 O(log n),相比链表在数据量较大时性能更好。
- ArrayList 扩容操作:
- 当 ArrayList 中元素个数达到其容量(capacity)时,会触发扩容。新容量为原容量的 1.5 倍(原容量右移一位再加原容量)。然后通过 System.arraycopy 方法将原数组内容复制到新的更大的数组中。例如,原数组容量为 10,当添加第 11 个元素时,新容量变为 15,然后把原数组 10 个元素复制到新的容量为 15 的数组中。
- HashMap put 流程:
- 首先计算 key 的 hash 值,通过 hash 值与数组长度减 1 进行按位与运算(h & (length - 1))得到数组的索引位置。
- 如果该位置为空,直接将键值对插入该位置。
- 如果该位置不为空,说明发生了哈希冲突。此时判断 key 是否相同(通过 equals 方法),如果相同则覆盖旧值。
- 如果 key 不同,若该位置是链表结构,则将新的键值对插入链表尾部(JDK1.8 之前是头部插入);若该位置是红黑树结构,则按红黑树的插入规则插入新节点。
- 线程池核心参数:
- 核心线程数(corePoolSize):线程池初始化时创建的线程数量,这些线程会一直存活,即使没有任务也不会被销毁(除非设置了 allowCoreThreadTimeOut 为 true)。
- 最大线程数(maximumPoolSize):线程池能容纳的最大线程数量。当任务队列已满且活动线程数小于最大线程数时,线程池会创建新的线程来处理任务。
- 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程存活的最长时间。超过这个时间,多余的线程会被销毁。
- 时间单位(unit):存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
- 任务队列(workQueue):用于存放等待执行的任务。常见的任务队列有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)等。
- 保证线程安全的方式:
- synchronized:是 Java 的关键字,它可以修饰方法和代码块。修饰实例方法时,锁的是当前实例对象;修饰静态方法时,锁的是类对象。它是一种内置锁,通过 JVM 实现,在进入同步代码块或方法时自动获取锁,退出时自动释放锁。
- Lock:是 Java 5.0 引入的接口,如 ReentrantLock 实现了该接口。与 synchronized 相比,Lock 提供了更灵活的锁操作,如可中断的获取锁、尝试获取锁等。使用 Lock 时需要手动获取锁(lock() 方法)和释放锁(unlock() 方法),通常放在 finally 块中以确保锁一定会被释放。
- synchronized 在静态方法和实例方法上的区别:
- 静态方法:synchronized 修饰静态方法时,锁的是类对象(Class 对象)。因为静态方法属于类,不属于任何实例,所以无论创建多少个类的实例,对静态方法加锁都是对同一个类对象加锁。例如,多个线程调用同一个类的静态 synchronized 方法时,只有一个线程能执行该方法,其他线程需要等待锁的释放。
- 实例方法:synchronized 修饰实例方法时,锁的是当前实例对象。不同的实例对象有不同的锁,所以不同实例调用实例 synchronized 方法时,不会相互影响。但同一个实例对象在同一时间只能有一个线程执行其 synchronized 实例方法。
- Spring Bean 的生命周期:
- 实例化(Instantiation):通过构造函数创建 Bean 实例。
- 属性注入(Populate):利用依赖注入(DI)将 Bean 所依赖的其他对象注入到 Bean 中。
- Aware 接口回调:如果 Bean 实现了一些 Aware 接口,如 BeanNameAware、ApplicationContextAware 等,Spring 容器会回调相应的方法,让 Bean 获取容器相关信息。
- 初始化(Initialization):如果 Bean 实现了 InitializingBean 接口,会调用其 afterPropertiesSet 方法;或者配置了 init - method 属性,会调用指定的初始化方法。
- 使用(Usage):Bean 可以被应用程序使用。
- 销毁(Destruction):当容器关闭时,如果 Bean 实现了 DisposableBean 接口,会调用其 destroy 方法;或者配置了 destroy - method 属性,会调用指定的销毁方法。
- Spring Boot 相对于 Spring 的优势:
- 自动配置:Spring Boot 基于约定大于配置的原则,能根据项目依赖自动配置 Spring 应用,大大减少了手动配置的工作量。例如,引入 Spring Data JPA 和 MySQL 依赖,Spring Boot 就能自动配置好数据源、JPA 相关的配置。
- 快速搭建项目:Spring Boot 提供了各种 Starter 依赖,通过简单引入相关 Starter 就能快速搭建一个完整的项目,如 Web 项目引入 spring - boot - starter - web 即可。
- 减少配置文件:相比 Spring 大量的 XML 配置或 Java 配置类,Spring Boot 只需要在 application.properties 或 application.yml 文件中进行少量配置即可。
- MyBatis 中 #{} 和 ${} 的区别:
- #{}:是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行参数设置。这种方式能有效防止 SQL 注入,因为参数是作为字符串传入,不会被解析为 SQL 语句的一部分。例如,SQL 语句为 SELECT * FROM user WHERE username = #{username},实际执行时会变为 SELECT * FROM user WHERE username =?,然后设置参数值。
- **{} 中的内容替换到 SQL 中。如果传入的值是恶意的 SQL 语句,就可能导致 SQL 注入。例如,SQL 语句为 SELECT * FROM user WHERE username = '${username}',如果传入的值为 '1 OR 1 = 1',则实际执行的 SQL 变为 SELECT * FROM user WHERE username = '1 OR 1 = 1',会返回所有用户数据,造成安全问题。