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

21 阅读14分钟

第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 后引入了红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时是如何操作的? 王铁牛:当 ArrayList 元素个数达到容量阈值时,会进行扩容,新容量是原容量的 1.5 倍,然后把原数组内容复制到新数组。 面试官:很好。HashMap 在 JDK1.8 中引入红黑树的条件是什么? 王铁牛:当链表长度大于 8 且数组容量大于等于 64 时,链表会转化为红黑树。

第二轮面试 面试官:接下来聊聊多线程和线程池。线程池有哪些核心参数? 王铁牛:有核心线程数、最大线程数、存活时间、时间单位,还有任务队列。 面试官:嗯,回答得还行。那线程池的拒绝策略有哪些? 王铁牛:有 AbortPolicy,直接抛出异常;还有 DiscardPolicy,丢弃任务不抛异常。 面试官:还有呢? 王铁牛:嗯……还有 DiscardOldestPolicy,好像是丢弃队列最前面的任务,还有个 CallerRunsPolicy,交给调用者线程处理。 面试官:好,那在高并发场景下,如何合理设置线程池参数? 王铁牛:呃……就是根据任务类型和系统资源来设置吧,具体怎么设,我还得再想想。

第三轮面试 面试官:谈谈 Spring 和 Spring Boot。Spring 中 Bean 的作用域有哪些? 王铁牛:有 singleton 单例、prototype 原型、request、session 这些。 面试官:Spring Boot 自动配置的原理是什么? 王铁牛:就是通过一些注解和配置类,Spring Boot 能自动帮我们配置好很多东西,具体原理我不太能说清楚。 面试官:那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,{} 是字符串替换,#{} 能防止 SQL 注入。 面试官:Dubbo 是做什么的,它的核心功能有哪些? 王铁牛:Dubbo 是个分布式服务框架,能做服务治理,核心功能有服务注册与发现、负载均衡这些。 面试官:RabbitMQ 有哪些常见的应用场景? 王铁牛:可以用于异步处理,还有解耦系统,像订单系统和库存系统之间可以用它。 面试官:xxl - job 是什么,有什么特点? 王铁牛:它是个分布式任务调度平台,特点嘛,好像是简单易用,支持集群部署。 面试官:Redis 有哪些数据类型,分别适用于什么场景? 王铁牛:有 String、Hash、List、Set、Sorted Set。String 存简单数据,Hash 存对象,List 做队列,Set 去重,Sorted Set 做排行榜。

面试官总结:今天的面试就到这里,整体来看,你对一些基础的知识点掌握得还不错,但对于一些稍微复杂和深入的问题,回答得不是很清晰。我们后续会综合评估所有候选人,你回家等通知吧,无论结果如何,我们都会在一周内给你回复。

答案:

  1. ArrayList 和 HashMap 底层数据结构
    • ArrayList:底层是数组结构,它允许以数组下标的方式快速访问元素。例如 list.get(0) 可以直接获取到第一个元素,这是因为数组在内存中是连续存储的,通过计算偏移量能快速定位元素。
    • HashMap:JDK1.8 之前底层是数组加链表结构,数组的每个位置是一个链表头节点。当发生哈希冲突时(不同的 key 计算出相同的哈希值),会将新的 key - value 对以链表的形式挂在该数组位置的链表上。JDK1.8 引入红黑树后,当链表长度大于 8 且数组容量大于等于 64 时,链表会转化为红黑树,以提高查询效率。红黑树是一种自平衡的二叉查找树,相比于链表,在查找、插入和删除操作上平均时间复杂度为 O(log n),而链表在最坏情况下时间复杂度为 O(n)。
  2. ArrayList 扩容操作
    • ArrayList 有一个容量(capacity)的概念,当向 ArrayList 中添加元素时,如果当前元素个数(size)达到了容量阈值(一般是当前容量),就会触发扩容。扩容时,新容量是原容量的 1.5 倍(newCapacity = oldCapacity + (oldCapacity >> 1),右移一位相当于除以 2)。然后通过 Arrays.copyOf() 方法将原数组内容复制到新数组。例如,原数组容量为 10,当添加第 11 个元素时,新容量变为 15,然后把原数组的 10 个元素复制到新的容量为 15 的数组中。
  3. HashMap 引入红黑树条件
    • 当链表长度大于 8 且数组容量大于等于 64 时,链表会转化为红黑树。这是因为在哈希分布相对均匀的情况下,链表长度一般不会太长。但如果链表过长,查询效率会降低到 O(n)。而红黑树的平均查询效率为 O(log n),能有效提升查询性能。当数组容量小于 64 时,优先进行扩容而不是转化为红黑树,因为扩容相对简单且能在一定程度上减少哈希冲突,提高整体性能。
  4. 线程池核心参数
    • 核心线程数(corePoolSize):线程池中会一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁(除非设置了 allowCoreThreadTimeOut 为 true)。例如,一个任务队列中有很多任务,核心线程数为 5,那么线程池会优先创建 5 个线程来处理任务。
    • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数。当任务队列已满,且核心线程数都在忙碌时,线程池会继续创建线程,直到达到最大线程数。但如果此时任务队列还是满的,就会触发拒绝策略。
    • 存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程存活的最长时间。例如,核心线程数为 5,最大线程数为 10,当有 8 个线程处理完任务后,多余的 3 个线程如果在存活时间内没有新任务,就会被销毁。
    • 时间单位(unit):存活时间的单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
    • 任务队列(workQueue):用于存放等待执行的任务。常见的任务队列有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列,默认容量为 Integer.MAX_VALUE)、SynchronousQueue(不存储任务,直接交给线程处理)等。不同的任务队列特性会影响线程池的性能和行为。
  5. 线程池拒绝策略
    • AbortPolicy:这是默认的拒绝策略,当任务无法提交到线程池(任务队列已满且线程数达到最大线程数)时,直接抛出 RejectedExecutionException 异常。例如,在一个高并发的订单处理系统中,如果线程池处理不过来新订单任务,采用此策略会让调用者知道任务处理失败,以便进行相应的处理(如重试等)。
    • DiscardPolicy:丢弃任务,不抛出任何异常。这种策略适用于对任务处理结果不是特别关注,且希望系统能继续运行而不中断的场景。比如一些日志记录任务,如果线程池满了,丢弃一些日志记录任务可能不会对系统造成严重影响。
    • DiscardOldestPolicy:丢弃队列中最老的任务(即将队列头部的任务丢弃),然后尝试提交新任务。假设任务队列是一个先进先出的队列,当新任务无法提交时,会把最早进入队列但还未处理的任务丢弃,然后尝试把新任务放入队列。这种策略适用于新任务比老任务更重要的场景,例如在实时数据处理系统中,新的数据可能更具时效性,老数据相对不那么重要。
    • CallerRunsPolicy:把任务交给调用者线程来处理。当线程池无法处理新任务时,调用 execute() 方法的线程会自己执行该任务。这种策略能在一定程度上减轻线程池的压力,同时保证任务不会被丢弃。例如,在一个 web 应用中,当线程池满了,处理请求的 tomcat 线程会自己执行任务,这样虽然会阻塞当前请求处理线程,但能保证任务被处理。
  6. 高并发场景下合理设置线程池参数
    • 任务类型
      • CPU 密集型任务:这类任务主要消耗 CPU 资源,如复杂的数学计算等。由于 CPU 资源有限,线程数不宜过多,一般设置核心线程数为 CPU 核心数 + 1。例如,服务器是 8 核 CPU,核心线程数可设为 9。这样既能充分利用 CPU 资源,又能避免过多线程上下文切换带来的开销。
      • I/O 密集型任务:这类任务主要等待 I/O 操作完成,如数据库查询、文件读写等。由于 I/O 操作相对较慢,线程在等待 I/O 时 CPU 处于空闲状态,所以可以设置较多的线程数。一般核心线程数可设为 2 * CPU 核心数。例如,8 核 CPU 的服务器,核心线程数可设为 16,以便在等待 I/O 时能有更多线程去处理其他任务。
    • 系统资源:要考虑服务器的内存、网络带宽等资源。如果内存有限,过多的线程可能导致内存溢出。例如,每个线程需要一定的栈空间,如果线程数过多,栈空间占用的内存过大,就可能引发内存问题。同时,网络带宽也会影响任务的执行效率,如果网络带宽有限,过多的网络 I/O 任务线程可能会导致网络拥塞。
  7. Spring 中 Bean 的作用域
    • singleton:单例模式,在整个 Spring 容器中,一个 Bean 定义只会创建一个实例。例如,数据库连接池的配置 Bean,通常设置为 singleton,这样整个应用中都使用同一个数据库连接池实例,避免了资源浪费。
    • prototype:原型模式,每次请求获取 Bean 时,都会创建一个新的实例。比如在处理多用户请求时,如果每个用户的业务处理逻辑需要独立的对象实例,就可以将相关 Bean 设置为 prototype 作用域。
    • request:在一次 HTTP 请求中,一个 Bean 定义只会创建一个实例。适用于 Web 应用中,与当前请求相关的 Bean,例如记录当前请求的一些属性的 Bean,在整个请求处理过程中是同一个实例。
    • session:在一个 HTTP Session 中,一个 Bean 定义只会创建一个实例。常用于存储与用户会话相关的数据,比如用户登录信息等,在用户的整个会话期间,该 Bean 实例是唯一的。
  8. Spring Boot 自动配置原理
    • Spring Boot 基于条件配置(@Conditional)和自动配置类实现自动配置。在 spring - boot - autoconfigure 模块中,有大量的自动配置类,如 DataSourceAutoConfiguration(数据库连接自动配置)、WebMvcAutoConfiguration(Spring MVC 自动配置)等。这些自动配置类上使用了 @Conditional 系列注解,例如 @ConditionalOnClass 表示当类路径下存在某个类时才进行配置,@ConditionalOnProperty 表示当配置文件中某个属性满足条件时才进行配置。当 Spring Boot 应用启动时,会扫描这些自动配置类,根据当前项目的依赖和配置条件,自动配置相应的 Bean。例如,如果项目中引入了 spring - jdbc 依赖,DataSourceAutoConfiguration 会检测到相关数据库连接类(如 DriverManagerDataSource)存在于类路径下,并且配置文件中有相关数据库连接属性配置,就会自动配置数据源 Bean。
  9. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 ?,然后使用 PreparedStatement 进行参数设置。例如,SQL 语句 SELECT * FROM user WHERE username = #{username},实际执行时会变为 SELECT * FROM user WHERE username =?,然后通过 PreparedStatement.setString(1, usernameValue) 设置参数值。这种方式能有效防止 SQL 注入,因为参数值是作为字符串传入,不会被解析为 SQL 语句的一部分。
    • **:是字符串替换,MyBatis会直接将{}**:是字符串替换,MyBatis 会直接将 `{}中的内容替换为实际的值。例如,SQL 语句SELECT * FROM user WHERE username = username,如果username的值为adminOR1=1,那么实际执行的SQL就变为SELECTFROMuserWHEREusername=adminOR1=1,这就导致了SQL注入漏洞。所以{username}`,如果 `username` 的值为 `'admin' OR '1' = '1'`,那么实际执行的 SQL 就变为 `SELECT * FROM user WHERE username = 'admin' OR '1' = '1'`,这就导致了 SQL 注入漏洞。所以 `{}` 一般用于传入表名、字段名等,且要确保传入的值是安全的。
  10. Dubbo 核心功能
  • 服务注册与发现:Dubbo 采用注册中心(如 Zookeeper、Nacos 等)来实现服务的注册与发现。服务提供者启动时,会将自己提供的服务注册到注册中心,服务消费者启动时,会从注册中心订阅自己需要的服务。例如,一个电商系统中,商品服务作为服务提供者将商品查询、添加等服务注册到 Zookeeper 注册中心,订单服务作为服务消费者从 Zookeeper 订阅商品服务,这样订单服务就能调用商品服务的接口。
  • 负载均衡:当有多个服务提供者提供相同的服务时,Dubbo 提供了多种负载均衡策略,如随机(RandomLoadBalance)、轮询(RoundRobinLoadBalance)、最少活跃调用数(LeastActiveLoadBalance)等。以随机策略为例,服务消费者在调用服务时,会从多个服务提供者中随机选择一个进行调用,这样能在一定程度上均匀分配请求,避免某个服务提供者压力过大。
  1. RabbitMQ 常见应用场景
  • 异步处理:例如在一个电商下单系统中,下单成功后需要发送短信通知用户、更新库存、记录订单日志等操作。如果这些操作都同步执行,会导致下单响应时间过长。可以将这些操作封装成消息发送到 RabbitMQ 队列中,下单操作完成后立即返回给用户,而短信发送、库存更新等操作由消费者从队列中取出消息异步处理,这样能大大提高系统的响应速度。
  • 解耦系统:假设一个大型电商系统中有订单系统、库存系统、物流系统等多个子系统。订单系统下单后,需要通知库存系统扣减库存,通知物流系统准备发货。如果订单系统直接调用库存系统和物流系统的接口,会导致系统之间耦合度很高。通过 RabbitMQ,订单系统只需要将订单消息发送到队列,库存系统和物流系统从队列中消费消息进行相应处理,这样各个系统之间的耦合度降低,提高了系统的可维护性和扩展性。
  1. xxl - job 特点
  • 简单易用:xxl - job 提供了简洁的 Web 控制台,通过控制台可以方便地管理任务,包括任务的新增、修改、删除、启动、停止等操作。例如,开发人员只需要在控制台填写任务的执行地址、执行参数等信息,就可以快速创建一个定时任务,无需复杂的配置和开发。
  • 支持集群部署:在高并发场景下,单个节点可能无法满足任务调度的需求。xxl - job 支持集群部署,多个调度器节点可以共同处理任务,提高任务调度的性能和可靠性。同时,它还具备自动故障转移功能,当某个调度器节点出现故障时,其他节点可以接管其任务,保证任务的正常执行。
  1. Redis 数据类型及适用场景
  • String:最基本的数据类型,适用于存储简单的键值对,如缓存用户信息、配置参数等。例如,将用户的登录名和密码以 key - value 形式存储,key 为用户 ID,value 为用户登录名和密码的 JSON 字符串。
  • Hash:适合存储对象,将对象的每个字段作为一个子键,字段值作为子值。比如存储用户详细信息,key 为用户 ID,field 可以是 nameageaddress 等,value 为对应的值。这种方式相比于 String 存储对象,在获取和修改对象部分字段时更高效,因为不需要获取整个对象字符串再进行解析和修改。
  • List:可以看作是一个双向链表,适用于实现队列和栈的功能。例如,实现一个消息队列,生产者将消息通过 rpush 命令添加到列表尾部,消费者通过 lpop 命令从列表头部取出消息进行处理,从而实现消息的异步处理。
  • Set:无序且不重复的集合,适用于去重和交集、并集、差集等集合运算。比如统计网站的独立访客,每次用户访问时将用户 ID 加入到 Set 中,由于 Set 的去重特性,最终 Set 的元素个数就是独立访客数。
  • Sorted Set:有序且不重复的集合,每个元素都关联一个分数(score