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

43 阅读11分钟

第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 之后链表长度超过 8 会转红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:当 ArrayList 元素个数达到容量阈值时,会创建一个新的数组,新数组容量是原数组的 1.5 倍,然后把原数组元素复制到新数组。 面试官:很好。HashMap 在 put 操作时,如何确定元素要放到哪个桶里? 王铁牛:通过 key 的 hashCode 与数组长度减 1 做按位与运算,得到的结果就是桶的索引。

第二轮面试 面试官:接下来聊聊多线程和线程池。线程池有哪些核心参数? 王铁牛:有核心线程数、最大线程数、存活时间、时间单位,还有任务队列。 面试官:那线程池在什么情况下会拒绝任务? 王铁牛:嗯……当任务队列满了,而且线程数达到最大线程数的时候吧。 面试官:那 Executors 提供的几种线程池,比如 FixedThreadPool 和 CachedThreadPool 有什么区别? 王铁牛:FixedThreadPool 线程数固定,CachedThreadPool 线程数不固定,可动态增加减少,大概是这样。

第三轮面试 面试官:谈谈 Spring 和 Spring Boot。Spring 中 Bean 的生命周期是怎样的? 王铁牛:嗯……先实例化,然后属性注入,接着初始化,最后销毁。 面试官:那 Spring Boot 自动配置的原理是什么? 王铁牛:好像是通过一些配置类和条件注解,具体不太清楚。 面试官:MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,{} 是字符串替换,#{} 能防止 SQL 注入。 面试官:Dubbo 服务暴露和引用的过程是怎样的? 王铁牛:呃……就是通过注册中心,服务提供者暴露服务,消费者引用服务,具体细节不太记得了。 面试官:RabbitMQ 有哪些工作模式? 王铁牛:有简单模式、工作队列模式、发布订阅模式等。 面试官:xxl - job 任务调度的原理是什么? 王铁牛:不太清楚,好像是通过调度中心和执行器来实现的。 面试官:Redis 有哪些数据类型,ZSet 数据类型有什么特点? 王铁牛:有 String、Hash、List、Set、ZSet。ZSet 是有序集合,每个元素都关联一个分数,根据分数排序。

面试总结:从整体面试情况来看,你在一些基础知识的掌握上表现还不错,像 ArrayList、HashMap 的底层结构,多线程线程池的部分核心参数等回答得比较准确。但在一些进阶和深入的问题上,比如 Spring Boot 自动配置原理、Dubbo 服务暴露引用过程、xxl - job 任务调度原理等,回答得不够清晰和全面。我们后续会综合评估所有面试者的情况,你回家等通知吧,无论结果如何,我们都会在一周内给你回复。

问题答案

  1. ArrayList 和 HashMap 的底层数据结构
    • ArrayList:底层是数组结构,它允许以数组下标的方式快速访问元素。这种结构适合随机访问,但在插入和删除元素时,尤其是在数组中间位置操作,需要移动大量元素,时间复杂度较高。
    • HashMap:JDK1.8 之前底层是数组加链表,数组的每个位置是一个链表头节点。通过 key 的 hashCode 计算出在数组中的位置,如果该位置已有元素,则以链表形式存储新元素。JDK1.8 之后,当链表长度超过 8 且数组容量大于等于 64 时,链表会转换为红黑树,以提高查找效率。红黑树是一种自平衡二叉查找树,能保证在最坏情况下,查找、插入和删除操作的时间复杂度为 O(log n)。
  2. ArrayList 扩容操作
    • ArrayList 有一个容量(capacity)和实际元素个数(size)。当 size 达到 capacity 时,会触发扩容。
    • 扩容时,会创建一个新的数组,新数组容量是原数组容量的 1.5 倍(原容量右移一位再加原容量)。
    • 然后通过 System.arraycopy 方法将原数组中的元素复制到新数组中。这样就完成了扩容操作,使得 ArrayList 可以继续添加新元素。
  3. HashMap put 操作确定桶位置
    • 首先调用 key 的 hashCode() 方法获取哈希码。
    • 然后将哈希码与数组长度减 1 做按位与运算(hash & (length - 1))。这是因为在数组长度为 2 的幂次方时,这种按位与运算等价于取模运算(hash % length),但按位与运算效率更高。通过这种方式得到的结果就是元素要放入的桶的索引位置。
  4. 线程池核心参数
    • 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁,除非设置了 allowCoreThreadTimeOut 为 true。
    • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数。当任务队列已满,且活动线程数达到核心线程数时,新任务会创建新线程来处理,直到线程数达到最大线程数。
    • 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程存活的最长时间。超过这个时间,空闲线程会被销毁。
    • 时间单位(unit):存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
    • 任务队列(workQueue):用于存放等待执行的任务。常见的任务队列有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列)、SynchronousQueue(同步队列,不存储任务,直接交给线程处理)等。
  5. 线程池拒绝任务的情况
    • 当任务队列已满,且活动线程数达到最大线程数时,新提交的任务会被拒绝。线程池提供了几种拒绝策略:
      • AbortPolicy(默认策略):直接抛出 RejectedExecutionException 异常。
      • CallerRunsPolicy:将任务交给调用 execute 方法的线程来执行,也就是主线程。这样可以降低新任务的提交速度。
      • DiscardPolicy:直接丢弃任务,不做任何处理。
      • DiscardOldestPolicy:丢弃任务队列中最老的任务(队头任务),然后尝试提交新任务。
  6. FixedThreadPool 和 CachedThreadPool 的区别
    • FixedThreadPool
      • 线程池的核心线程数和最大线程数相等,即线程数固定。
      • 任务队列采用 LinkedBlockingQueue,是一个无界队列。这意味着如果任务提交速度过快,队列会不断增长,可能导致内存溢出。
      • 适用于需要控制并发线程数,且任务执行时间相对较长的场景,比如数据库连接池。
    • CachedThreadPool
      • 核心线程数为 0,最大线程数为 Integer.MAX_VALUE,线程数不固定。
      • 任务队列采用 SynchronousQueue,该队列不存储任务,新任务提交时直接寻找空闲线程执行,如果没有空闲线程则创建新线程。
      • 当线程空闲时间超过 60 秒(默认),线程会被销毁。适用于任务执行时间短,且提交任务数量不确定的场景,比如 Web 服务器处理短期请求。
  7. Spring 中 Bean 的生命周期
    • 实例化(Instantiation):通过构造函数创建 Bean 实例。
    • 属性注入(Populate):利用依赖注入(DI),将 Bean 依赖的其他对象注入到该 Bean 中。
    • 初始化(Initialization)
      • 如果 Bean 实现了 InitializingBean 接口,调用 afterPropertiesSet 方法。
      • 如果在配置文件中通过 init - method 属性指定了初始化方法,调用该方法。
    • 使用(Using):Bean 可以被应用程序使用。
    • 销毁(Destruction)
      • 如果 Bean 实现了 DisposableBean 接口,调用 destroy 方法。
      • 如果在配置文件中通过 destroy - method 属性指定了销毁方法,调用该方法。
  8. Spring Boot 自动配置原理
    • Spring Boot 基于 Spring 框架,通过大量的自动配置类(以 @Configuration 注解标注)来实现自动配置。
    • 这些自动配置类使用 @Conditional 系列注解(如 @ConditionalOnClass、@ConditionalOnProperty 等)。
    • @ConditionalOnClass 表示当类路径下存在某个类时,该自动配置类才生效。例如,当类路径下存在 Tomcat 相关类时,Spring Boot 会自动配置 Tomcat 作为 Web 服务器。
    • @ConditionalOnProperty 表示当配置文件中某个属性满足一定条件时,该自动配置类才生效。比如,当配置文件中 spring.datasource.url 属性存在时,才会配置数据源相关的 Bean。
    • Spring Boot 在启动时,会扫描 META - INF/spring.factories 文件,该文件中定义了各种自动配置类,Spring Boot 会根据条件注解决定是否加载这些自动配置类,从而实现自动配置。
  9. MyBatis 中 #{} 和 ${} 的区别
    • #{}
      • 是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行参数设置。
      • 这种方式可以有效防止 SQL 注入,因为参数是作为字符串传入,而不是直接拼接到 SQL 语句中。
    • ${}
      • 是字符串替换方式,MyBatis 在处理 时,会直接将{} 时,会直接将 {} 替换为变量的值。
      • 这种方式不能防止 SQL 注入,因为如果变量值被恶意修改,可能会改变 SQL 语句的逻辑。一般用于传入数据库对象,如表名、列名等,但使用时要特别小心。
  10. Dubbo 服务暴露和引用过程
  • 服务暴露
    • 服务提供者启动时,通过 Dubbo 配置文件或注解定义要暴露的服务接口和实现类。
    • Dubbo 框架根据配置信息,将服务接口和实现类封装成 Invoker 对象。
    • 然后通过 ProxyFactory 生成服务代理,将服务代理注册到注册中心(如 Zookeeper)。注册中心记录了服务提供者的地址和服务接口等信息。
  • 服务引用
    • 服务消费者启动时,根据配置信息向注册中心订阅所需的服务。
    • 注册中心将服务提供者的地址信息返回给服务消费者。
    • 服务消费者根据返回的地址信息,通过 ProxyFactory 生成服务代理,调用服务代理的方法时,实际是通过网络通信(如 Netty)向服务提供者发起远程调用,获取服务结果。
  1. RabbitMQ 工作模式
  • 简单模式(Simple):一个生产者,一个消费者,一条队列。生产者将消息发送到队列,消费者从队列中获取消息并处理。
  • 工作队列模式(Work Queue):一个生产者,多个消费者,一条队列。多个消费者竞争从队列中获取消息,每个消息只会被一个消费者处理。适用于任务量较大,需要多个消费者共同处理的场景。
  • 发布订阅模式(Publish/Subscribe):一个生产者,多个消费者,多个队列。生产者将消息发送到交换机(Exchange),交换机将消息广播到所有绑定的队列,每个队列的消费者都能收到消息副本。适用于需要将消息同时发送给多个不同业务模块处理的场景。
  • 路由模式(Routing):一个生产者,多个消费者,多个队列。生产者将消息发送到交换机,交换机根据消息的路由键(routing key)将消息发送到特定的队列。只有绑定了该路由键的队列的消费者才能收到消息。适用于需要根据不同条件将消息发送到不同队列的场景。
  • 主题模式(Topic):类似路由模式,但路由键支持通配符。交换机根据消息的路由键和队列绑定的通配符规则来决定将消息发送到哪些队列。适用于更灵活的消息路由场景。
  1. xxl - job 任务调度原理
  • 调度中心:是 xxl - job 的核心组件,负责任务的管理、调度触发等。
    • 调度中心维护了任务信息,包括任务的执行器地址、任务参数、调度规则(如 cron 表达式)等。
    • 调度中心根据任务的调度规则,定时触发任务调度。当到达调度时间时,调度中心会向任务对应的执行器发送调度请求。
  • 执行器:负责实际执行任务。
    • 执行器启动时,会向调度中心注册自己,并保持心跳连接。
    • 当接收到调度中心的调度请求时,执行器根据请求中的任务信息,调用具体的任务执行逻辑,执行任务,并将任务执行结果返回给调度中心。
  1. Redis 数据类型及 ZSet 特点
  • Redis 数据类型
    • String:最基本的数据类型,能存储字符串、数字、二进制数据等。可以进行简单的 SET、GET 操作,也支持一些原子性的自增、自减操作。
    • Hash:哈希表类型,用于存储键值对集合,适合存储对象。可以对哈希表中的单个字段进行操作,如 HSET、HGET 等。
    • List:链表类型,按照插入顺序存储元素。支持从链表两端插入和删除元素,如 LPUSH、RPOP 等操作,常用于实现消息队列。
    • Set:无序集合,元素唯一。支持集合的交、并、差等操作,如 SINTER、SUNION、SDIFF 等。
    • ZSet(Sorted Set):有序集合,每个元素都关联一个分数(score)。
  • ZSet 特点
    • 有序性:根据分数对元素进行排序,分数相同的元素按字典序排列。
    • 唯一性:集合中的元素不能重复。
    • 常用操作:可以通过分数范围或成员范围获取元素,如 ZRANGEBYSCORE 获取指定分数范围内的元素,ZRANK 获取元素的排名等。适用于排行榜、带权重的任务队列等场景。