《互联网大厂 Java 面试:从基础到进阶的核心技术大考察》

28 阅读9分钟

第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 后引入了红黑树。 面试官:不错,回答得很准确。那 HashMap 在什么情况下会触发扩容? 王铁牛:当 HashMap 中的元素个数达到负载因子(默认 0.75)乘以当前容量的时候,就会触发扩容。 面试官:回答得很好。那 ArrayList 在添加元素时,如果容量不够了会怎样? 王铁牛:它会进行扩容,新容量大概是原来容量的 1.5 倍,然后把旧数组的数据复制到新数组。

第二轮面试 面试官:接下来聊聊多线程和线程池。线程池有哪些核心参数? 王铁牛:嗯……有核心线程数、最大线程数,还有……还有队列容量。 面试官:还算可以。那线程池的拒绝策略有哪些? 王铁牛:这个……好像有 AbortPolicy,还有……其他的不太记得了。 面试官:行吧。那在高并发场景下,使用线程池有什么好处? 王铁牛:能复用线程,减少线程创建和销毁的开销,提高性能。

第三轮面试 面试官:再谈谈 Spring 和 Spring Boot。Spring 中 Bean 的作用域有哪些? 王铁牛:有 singleton、prototype,还有……好像还有 request 和 session。 面试官:勉强算对。那 Spring Boot 自动配置的原理是什么? 王铁牛:就是……它会根据类路径下的依赖,自动配置一些 Bean 吧,具体不太清楚。 面试官:最后一个问题,MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,能防止 SQL 注入,{} 是直接替换,可能有 SQL 注入风险。

面试官:今天的面试就到这里,你回去等通知吧。整体来看,你对一些基础的知识点掌握得还可以,但对于一些稍微深入的问题,理解得还不够透彻。在技术领域,不仅要知其然,更要知其所以然。希望你回去之后,能对今天问到的知识点进行更深入的学习和研究。如果后续有进一步的消息,我们会及时联系你。

问题答案

  1. ArrayList 和 HashMap 的底层数据结构
    • ArrayList:底层是数组结构。数组可以快速地通过索引访问元素,但是在插入和删除元素时,尤其是在数组中间位置操作时,需要移动大量元素,时间复杂度较高。例如,在 ArrayList 中间插入一个元素,后续元素都要向后移动一位。
    • HashMap:JDK1.7 及之前,底层是数组加链表。数组的每个位置存放一个链表头节点,通过 key 的哈希值计算出在数组中的位置,若该位置已有元素(哈希冲突),则以链表形式存储。JDK1.8 引入红黑树,当链表长度大于 8 且数组容量大于 64 时,链表会转换为红黑树,以提高查找效率。因为红黑树的查找、插入、删除平均时间复杂度为 O(logn),而链表在最坏情况下查找时间复杂度为 O(n)。
  2. HashMap 触发扩容的情况:HashMap 有一个负载因子(load factor),默认值是 0.75。当 HashMap 中的元素个数(size)达到负载因子乘以当前容量(capacity)时,就会触发扩容。例如,当前容量为 16,负载因子为 0.75,当元素个数达到 16 * 0.75 = 12 时,就会进行扩容。扩容后新容量是原来的 2 倍,这样可以降低哈希冲突的概率,提高 HashMap 的性能。
  3. ArrayList 添加元素容量不够时的处理:当 ArrayList 容量不够时,会进行扩容。它会创建一个新的数组,新数组的容量大概是原来容量的 1.5 倍(int newCapacity = oldCapacity + (oldCapacity >> 1); 这里右移一位相当于除以 2)。然后将旧数组中的元素复制到新数组中,使用 System.arraycopy 方法进行复制。这样就完成了扩容操作,使得 ArrayList 可以继续添加新元素。
  4. 线程池的核心参数
    • 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁。例如,一个 Web 服务器的线程池,核心线程数可以设置为能处理日常请求量的线程数量。
    • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数。当任务队列已满,且核心线程都在忙碌时,线程池会创建新线程,直到达到最大线程数。但如果达到最大线程数后任务队列还是满的,就会触发拒绝策略。
    • 队列容量(workQueue):用于存放等待执行的任务。常见的队列类型有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列)等。例如,使用 ArrayBlockingQueue 时,需要指定队列的容量大小。
    • 线程存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程存活的最长时间。超过这个时间,多余的线程会被销毁。
    • 时间单位(unit):keepAliveTime 的时间单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
  5. 线程池的拒绝策略
    • AbortPolicy:这是默认的拒绝策略。当任务无法提交到线程池(队列已满且线程数达到最大线程数)时,会抛出 RejectedExecutionException 异常。例如,在一个高并发的订单处理系统中,如果线程池处理不过来新订单,采用此策略会让调用者知道任务被拒绝,以便进行相应处理。
    • CallerRunsPolicy:当任务被拒绝时,会在调用 execute 方法的线程中直接执行该任务。这样可以减轻线程池的压力,但可能会影响调用者所在线程的正常工作。比如在一个简单的单线程调用线程池的场景下,使用此策略可以保证任务不会丢失。
    • DiscardPolicy:直接丢弃被拒绝的任务,不做任何处理。适用于对任务可靠性要求不高的场景,例如一些日志记录任务,如果线程池忙不过来,丢弃部分日志任务可能影响不大。
    • DiscardOldestPolicy:丢弃队列中最老的任务(即将队列头部的任务丢弃),然后尝试将新任务加入队列。这种策略假设新任务比老任务更重要,例如在实时数据处理场景中,新的数据可能更有价值。
  6. 高并发场景下使用线程池的好处
    • 复用线程:避免了频繁创建和销毁线程的开销。创建和销毁线程需要与操作系统进行交互,涉及到内核态和用户态的切换,开销较大。线程池可以复用已创建的线程,提高了线程的使用效率。
    • 控制并发度:通过设置核心线程数和最大线程数,可以控制同时执行的任务数量,避免因线程过多导致系统资源耗尽。例如,在一个数据库连接池场景中,如果并发线程过多,可能会导致数据库连接数耗尽,使用线程池可以合理控制并发访问数据库的线程数量。
    • 提高响应速度:当有新任务到来时,如果线程池中有空闲线程,能立即处理任务,而不需要等待线程创建,从而提高了系统的响应速度。
  7. Spring 中 Bean 的作用域
    • singleton:单例模式,在整个 Spring 容器中,只会创建一个实例。例如,配置一个数据库连接池的 Bean,使用 singleton 作用域,整个应用程序都共享这一个数据库连接池实例,避免了资源浪费。
    • prototype:原型模式,每次请求获取 Bean 时,都会创建一个新的实例。适用于有状态的 Bean,例如一个处理用户请求的 Bean,每个用户请求都需要一个独立的实例,以避免状态相互影响。
    • request:在一次 HTTP 请求中,只会创建一个实例。当请求结束,该实例会被销毁。常用于 Web 应用中,处理与请求相关的业务逻辑,例如记录请求的日志信息,每个请求有独立的日志记录实例。
    • session:在一个 HTTP Session 范围内,只会创建一个实例。当 Session 过期或被销毁时,该实例也会被销毁。适用于需要在用户会话期间共享数据的场景,比如记录用户在会话期间的购物车信息。
    • global - session:在一个全局的 HTTP Session 范围内有效,主要用于 Portlet 环境(一种基于 Java 的 Web 应用技术),在普通的 Web 应用中较少使用。
  8. Spring Boot 自动配置的原理:Spring Boot 自动配置是基于条件配置(@Conditional)和 Spring Factories 机制实现的。
    • Spring Factories 机制:在 META - INF/spring.factories 文件中,定义了各种自动配置类。例如,spring - boot - starter - web 依赖的 META - INF/spring.factories 文件中,定义了 WebMvcAutoConfiguration 等自动配置类。Spring Boot 在启动时,会扫描所有依赖的 META - INF/spring.factories 文件,加载其中定义的自动配置类。
    • 条件配置(@Conditional):自动配置类上使用了 @Conditional 及其相关注解(如 @ConditionalOnClass、@ConditionalOnProperty 等)。例如,@ConditionalOnClass 表示当类路径下存在某个类时,才会进行该自动配置。以 DataSourceAutoConfiguration 为例,如果类路径下存在 HikariCP 相关的类,并且配置文件中配置了数据库连接相关属性,就会自动配置数据源。这样 Spring Boot 可以根据项目的依赖和配置,自动配置相应的 Bean,减少了开发者手动配置的工作量。
  9. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译方式。MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行参数设置。这样可以有效防止 SQL 注入攻击,因为参数是作为字符串传入的,不会被解析为 SQL 语句的一部分。例如,SQL 语句为 “SELECT * FROM user WHERE username = #{username}”,实际执行时会变为 “SELECT * FROM user WHERE username =?”,然后通过 PreparedStatement.setString(1, “user1”) 设置参数值。
    • **:是字符串替换方式。MyBatis在处理{}**:是字符串替换方式。MyBatis 在处理 {} 时,会直接将 中的内容替换到SQL中。如果参数值是通过用户输入获取的,可能会导致SQL注入风险。例如,用户输入“user1OR1=1”,SQL语句“SELECTFROMuserWHEREusername={} 中的内容替换到 SQL 中。如果参数值是通过用户输入获取的,可能会导致 SQL 注入风险。例如,用户输入 “user1 OR 1 = 1”,SQL 语句 “SELECT * FROM user WHERE username = {username}” 会变为 “SELECT * FROM user WHERE username = user1 OR 1 = 1”,这样就会返回所有用户数据,造成安全问题。所以在使用 ${} 时,要确保参数值是可信的,通常用于传入表名、字段名等静态值。