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

41 阅读12分钟

第一轮面试 面试官:先问些基础的,Java 中 ArrayList 和 HashMap 的底层数据结构分别是什么? 王铁牛:ArrayList 底层是数组,HashMap 底层是数组加链表,JDK1.8 后引入了红黑树。 面试官:不错,回答得很准确。那 ArrayList 在扩容时具体是怎么操作的? 王铁牛:当元素个数达到容量阈值时,会创建一个新的更大的数组,然后把旧数组的元素复制到新数组。 面试官:回答得很好。HashMap 在 JDK1.8 中引入红黑树,为什么要引入呢? 王铁牛:好像是为了优化查询性能,链表太长查询效率低,红黑树能提高效率。

第二轮面试 面试官:接下来聊聊多线程和线程池。创建线程有几种方式? 王铁牛:有继承 Thread 类,实现 Runnable 接口,还有实现 Callable 接口。 面试官:很好。那线程池的核心参数有哪些,分别代表什么含义? 王铁牛:有核心线程数、最大线程数,还有……还有队列容量,核心线程数就是一直保留的线程数,最大线程数是能创建的最大线程数,队列容量就是存放任务的队列大小。 面试官:还行。那线程池在什么情况下会拒绝任务? 王铁牛:嗯……好像是队列满了,而且线程数达到最大线程数的时候。

第三轮面试 面试官:谈谈 Spring 和 Spring Boot 吧。Spring 的 IOC 是什么,有什么作用? 王铁牛:IOC 就是控制反转,把对象的创建和管理交给 Spring 容器,这样代码耦合度更低。 面试官:不错。Spring Boot 相对于 Spring 有什么优势? 王铁牛:Spring Boot 能快速搭建项目,自动配置很多东西,开发更方便。 面试官:那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译,能防止 SQL 注入,{} 是直接替换,可能有 SQL 注入风险。 面试官:最后问下,Dubbo、RabbitMq、xxl - job、Redis 这些技术在项目中一般都用于什么场景? 王铁牛:Dubbo 好像是做服务治理的,RabbitMq 用来做消息队列,xxl - job 是任务调度,Redis 可以做缓存。

面试总结:从这三轮面试来看,你对于一些基础的知识点掌握得还可以,像 ArrayList、HashMap 的底层结构,创建线程的方式等回答得都不错。但对于一些稍微深入点的问题,比如线程池拒绝任务的具体场景等,回答得不是特别清晰和全面。在 Spring、Spring Boot 以及其他框架的应用场景理解上,虽然能说出大概方向,但细节方面还有所欠缺。整体表现有一定的基础,但在技术的深度和广度上还有提升空间。回去等通知吧,我们会综合评估所有候选人后,再做决定。

问题答案

  1. ArrayList 和 HashMap 的底层数据结构
    • ArrayList:底层是数组结构,它可以动态扩容。数组的特点是可以根据索引快速访问元素,但插入和删除元素(非尾部)时效率较低,因为需要移动元素。
    • HashMap:JDK1.7 及之前,底层是数组加链表结构。数组的每个位置是一个链表头节点,通过哈希算法计算 key 的哈希值,再对数组长度取模确定元素在数组中的位置。如果哈希冲突(不同 key 计算出相同的数组位置),则以链表形式存储在该位置。JDK1.8 引入红黑树,当链表长度达到 8 且数组容量大于等于 64 时,链表会转换为红黑树,以提高查询性能。红黑树是一种自平衡二叉查找树,能保证在最坏情况下,查询、插入和删除操作的时间复杂度为 O(log n)。
  2. ArrayList 扩容操作
    • ArrayList 有一个默认初始容量,一般为 10。当向 ArrayList 中添加元素时,会检查当前元素个数是否达到容量阈值(一般是当前容量)。如果达到阈值,会创建一个新的数组,新数组的容量是原数组容量的 1.5 倍(通过位运算实现,newCapacity = oldCapacity + (oldCapacity >> 1))。然后通过 System.arraycopy 方法将旧数组的元素复制到新数组中,最后将新数组赋值给 ArrayList 的底层数组引用。
  3. HashMap 引入红黑树的原因
    • 在 JDK1.7 及之前,HashMap 处理哈希冲突是链表形式。当哈希冲突严重时,链表会变得很长,此时查询一个元素的时间复杂度会退化为 O(n),n 为链表长度。引入红黑树后,当链表长度达到一定阈值(8)且数组容量大于等于 64 时,链表转换为红黑树。红黑树能保证在最坏情况下,查询、插入和删除操作的时间复杂度为 O(log n),大大提高了哈希冲突严重时的查询性能。
  4. 创建线程的方式
    • 继承 Thread 类:创建一个类继承 Thread 类,重写 run 方法,在 run 方法中编写线程执行的逻辑。然后创建该类的实例,调用 start 方法启动线程。例如:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行逻辑");
    }
}
MyThread myThread = new MyThread();
myThread.start();
  • 实现 Runnable 接口:创建一个类实现 Runnable 接口,实现 run 方法。然后创建该类的实例,将其作为参数传递给 Thread 类的构造函数,再调用 start 方法启动线程。例如:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程执行逻辑");
    }
}
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
  • 实现 Callable 接口:创建一个类实现 Callable 接口,实现 call 方法,call 方法可以有返回值和抛出异常。通过 FutureTask 类来包装 Callable 实现类的实例,再将 FutureTask 作为参数传递给 Thread 类的构造函数,调用 start 方法启动线程。可以通过 FutureTask 的 get 方法获取线程执行的返回值。例如:
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 100;
    }
}
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
try {
    Integer result = futureTask.get();
    System.out.println("线程返回值: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}
  1. 线程池的核心参数
    • 核心线程数(corePoolSize):线程池中一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁(除非设置了 allowCoreThreadTimeOut 为 true)。当有新任务提交时,如果线程池中的线程数小于核心线程数,会创建新的线程来处理任务。
    • 最大线程数(maximumPoolSize):线程池中允许创建的最大线程数。当任务队列已满,且线程池中的线程数小于最大线程数时,会创建新的线程来处理任务。
    • 队列容量(workQueue):用于存放等待执行的任务的队列。当线程池中的线程数达到核心线程数后,新提交的任务会被放入队列中等待执行。常见的队列类型有 ArrayBlockingQueue(有界数组队列)、LinkedBlockingQueue(无界链表队列,默认容量为 Integer.MAX_VALUE)、SynchronousQueue(同步队列,不存储元素,每个插入操作必须等待另一个线程的移除操作)等。
    • 线程存活时间(keepAliveTime):当线程池中的线程数大于核心线程数时,多余的空闲线程在等待新任务到来的时间超过该存活时间后,会被销毁。
    • 时间单位(unit):用于指定线程存活时间的时间单位,如 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
  2. 线程池拒绝任务的情况
    • AbortPolicy(默认策略):当队列满了且线程数达到最大线程数时,新任务提交会抛出 RejectedExecutionException 异常。
    • CallerRunsPolicy:当队列满了且线程数达到最大线程数时,新任务不会被丢弃,而是由提交任务的线程(调用 execute 方法的线程)来执行该任务。
    • DiscardPolicy:当队列满了且线程数达到最大线程数时,新任务直接被丢弃,不做任何处理。
    • DiscardOldestPolicy:当队列满了且线程数达到最大线程数时,会丢弃队列中最老的任务(队头任务),然后尝试将新任务加入队列。
  3. Spring 的 IOC(控制反转)
    • 概念:IOC 是一种设计思想,全称 Inversion of Control,即控制反转。它将对象的创建和管理从应用程序代码中转移到 Spring 容器中。在传统的编程中,对象的创建和依赖关系的管理由应用程序自己负责,这导致代码耦合度较高。而在 Spring 中,通过 IOC 容器,对象的创建、配置和组装都由容器来完成,应用程序只需要使用这些对象即可。
    • 作用
      • 降低耦合度:使得组件之间的依赖关系更加清晰,组件不需要关心其依赖对象的创建和销毁,只需要关注自身的业务逻辑。例如,一个 Service 类依赖一个 Dao 类,在传统方式下,Service 类需要自己创建 Dao 实例,而在 Spring IOC 中,Spring 容器会创建并注入 Dao 实例给 Service 类。
      • 提高可维护性和可测试性:由于对象的创建和配置都在 Spring 容器中,当需要修改对象的依赖关系或替换实现类时,只需要在 Spring 配置文件或注解中进行修改,而不需要修改大量的业务代码。同时,在单元测试时,可以方便地为被测试对象注入模拟的依赖对象。
  4. Spring Boot 相对于 Spring 的优势
    • 快速搭建项目:Spring Boot 提供了大量的 Starter 依赖,通过引入这些依赖,可以快速集成各种常用的技术,如数据库连接、Web 开发等,大大减少了项目搭建的时间和工作量。例如,引入 spring - boot - starter - web 依赖,就可以快速搭建一个基于 Spring MVC 的 Web 项目。
    • 自动配置:Spring Boot 能够根据项目中引入的依赖自动进行配置。它会根据类路径下的依赖情况,自动配置相应的组件,如数据源、日志等。开发者只需要进行少量的自定义配置即可,避免了在 Spring 中大量繁琐的 XML 配置或 Java 配置。
    • 生产级特性:Spring Boot 内置了一些生产级特性,如健康检查、指标监控等。通过这些特性,可以方便地对应用程序进行监控和管理,提高应用程序的可靠性和稳定性。
  5. MyBatis 中 #{} 和 ${} 的区别
    • #{}:是预编译方式,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?,然后使用 PreparedStatement 进行设置参数值。这种方式可以有效防止 SQL 注入,因为参数值不会直接拼接到 SQL 语句中。例如:SELECT * FROM user WHERE username = #{username},实际执行的 SQL 是 SELECT * FROM user WHERE username =?,然后通过 PreparedStatement 设置 username 参数值。
    • **:是字符串替换方式,MyBatis在处理{}**:是字符串替换方式,MyBatis 在处理 {} 时,会直接将 中的内容替换到SQL语句中。如果参数值来自用户输入,可能会导致SQL注入风险。例如:SELECTFROMuserWHEREusername={} 中的内容替换到 SQL 语句中。如果参数值来自用户输入,可能会导致 SQL 注入风险。例如:`SELECT * FROM user WHERE username = '{username}',如果用户输入 ' OR '1' = '1',则实际执行的 SQL 变为 SELECT * FROM user WHERE username = '' OR '1' = '1'`,这会导致查询出所有用户数据。所以在使用 ${} 时,要确保参数值是安全的,一般用于传入表名、字段名等。
  6. Dubbo、RabbitMq、xxl - job、Redis 在项目中的应用场景
  • Dubbo:是一款高性能的 Java RPC 框架,主要用于服务治理。在大型分布式系统中,服务之间的调用关系复杂,Dubbo 可以实现服务的注册与发现,让服务提供者将自己的服务注册到注册中心(如 Zookeeper),服务消费者从注册中心获取服务提供者的地址,从而实现服务之间的远程调用。它还提供了负载均衡、容错等功能,提高了系统的可用性和性能。例如,电商系统中,商品服务、订单服务等不同的微服务之间可以通过 Dubbo 进行通信。
  • RabbitMq:是一个消息队列中间件,主要用于异步处理、解耦系统组件和流量削峰。
    • 异步处理:例如在电商系统中,用户下单后,可能需要发送短信通知、生成订单报表等操作,这些操作可以通过消息队列异步处理,提高系统响应速度,用户下单后不需要等待这些操作完成,系统可以立即返回下单成功的结果。
    • 解耦系统组件:假设一个系统有多个模块,如订单模块、库存模块、物流模块等,订单模块下单后,通过消息队列发送消息,库存模块和物流模块监听消息进行相应处理,这样各个模块之间不需要直接调用,降低了耦合度。
    • 流量削峰:在高并发场景下,如电商大促活动,大量的请求同时到达系统,通过消息队列可以将请求先放入队列中,系统按照一定的速度从队列中取出请求进行处理,避免系统因瞬间高并发而崩溃。
  • xxl - job:是一个轻量级分布式任务调度框架,主要用于定时任务的调度和管理。在项目中,可能有一些定时任务,如每天凌晨备份数据库、定时清理过期数据等。xxl - job 可以方便地管理这些任务,包括任务的创建、调度、执行、监控等。它支持集群部署,提高了任务调度的可靠性和性能。例如,在一个电商系统中,可以使用 xxl - job 定时统计每天的销售数据。
  • Redis:是一个高性能的 key - value 存储数据库,常用于缓存、分布式锁、消息队列等场景。
    • 缓存:可以将经常访问的数据(如热点商品信息、用户信息等)存储在 Redis 中,当用户请求数据时,先从 Redis 中获取,如果 Redis 中没有再从数据库中查询,然后将查询结果存入 Redis,这样可以大大提高系统的响应速度,减轻数据库的压力。
    • 分布式锁:在分布式系统中,多个节点可能同时访问共享资源,为了保证数据的一致性,需要使用分布式锁。Redis 可以通过 SETNX(SET if Not eXists)命令实现分布式锁,只有获取到锁的节点才能访问共享资源。
    • 消息队列:Redis 提供了发布订阅模式和 List 数据结构来实现简单的消息队列功能。发布订阅模式下,生产者发布消息,多个消费者可以订阅并接收消息;通过 List 数据结构,可以将消息放入 List 中,消费者从 List 中取出消息进行处理。