《互联网大厂 Java 面试:核心知识、框架与中间件大考验》
王铁牛怀揣着紧张与期待,走进了互联网大厂的面试间。严肃的面试官早已坐在那里,一场对 Java 知识深度与广度的考验即将拉开帷幕。
第一轮面试开始 面试官:“我们先从 Java 核心知识开始。能说一下 Java 中多态的实现方式有哪些吗?” 王铁牛:“多态的实现方式主要有两种,一种是方法重载,在同一个类中,方法名相同但参数列表不同;另一种是方法重写,子类重写父类的方法。” 面试官:“回答得不错。那在 JVM 里,堆和栈的主要区别是什么?” 王铁牛:“堆主要用来存储对象实例,是线程共享的,而栈主要存储局部变量等,是线程私有的。” 面试官:“很好。再问一个,多线程中,synchronized 关键字和 Lock 接口的区别是什么?” 王铁牛:“synchronized 是 Java 的关键字,是隐式锁,由 JVM 来管理加锁和解锁;Lock 是一个接口,是显式锁,需要手动加锁和解锁,而且 Lock 提供了更多的功能,比如可中断锁、公平锁等。” 面试官:“非常棒,你的基础很扎实。”
第二轮面试开始 面试官:“接下来我们聊聊容器类。HashMap 的底层数据结构是什么样的?” 王铁牛:“HashMap 的底层是数组 + 链表 + 红黑树。当链表长度超过 8 且数组长度大于 64 时,链表会转化为红黑树,以提高查找效率。” 面试官:“不错。那 ArrayList 是如何实现动态扩容的?” 王铁牛:“当向 ArrayList 中添加元素时,如果当前容量不够,会创建一个新的数组,新数组的容量是原来的 1.5 倍,然后把原数组的元素复制到新数组中。” 面试官:“回答得很清晰。再问一下,线程池有哪些核心参数,它们分别有什么作用?” 王铁牛:“线程池的核心参数有 corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(线程空闲存活时间)、TimeUnit(时间单位)、workQueue(任务队列)和 ThreadFactory(线程工厂)。核心线程数是线程池一直保持的线程数量,最大线程数是线程池能容纳的最大线程数量,任务队列用来存储待执行的任务。” 面试官:“很专业,看来你对容器和线程池这块研究得比较深入。”
第三轮面试开始 面试官:“我们再深入到框架和中间件。Spring 框架中,依赖注入有哪些方式?” 王铁牛:“依赖注入的方式主要有构造器注入、setter 方法注入和接口注入。” 面试官:“那 Spring Boot 相对于 Spring 有哪些优势?” 王铁牛:“Spring Boot 可以快速搭建项目,它有自动配置功能,减少了大量的配置文件,还内置了 Tomcat 等服务器,方便部署。” 面试官:“不错。MyBatis 中,#{} 和 {} 的区别是什么?” **王铁牛**:“#{} 是预编译处理,会把参数替换为占位符,能防止 SQL 注入;{} 是直接替换,会存在 SQL 注入的风险。” 面试官:“回答得还可以。Dubbo 是一个分布式服务框架,它的服务发现机制是怎样的?” 王铁牛:“呃……这个……好像是通过注册中心,具体的我有点记不太清了。” 面试官:“那 RabbitMQ 中,消息确认机制是如何保证消息不丢失的?” 王铁牛:“这个……我只知道有消息确认,具体怎么保证不太清楚。” 面试官:“xxl - job 是一个分布式任务调度平台,它的调度原理是什么?” 面试官:“还有 Redis,它的持久化机制有哪些,分别有什么特点?” 王铁牛:“这些问题我有点模糊,回答不太上来。”
面试结束,面试官整理了一下手中的资料,说道:“今天的面试就到这里,你在一些基础和常见的问题上回答得还不错,但在框架和中间件的一些深入问题上表现得不太理想。你先回家等通知吧,我们会综合评估后给你反馈。”
答案详解
- Java 中多态的实现方式
- 方法重载:在同一个类中,方法名相同,但参数列表不同(参数的类型、个数或顺序不同)。编译器会根据调用方法时传入的实际参数来决定调用哪个重载方法。例如:
public class OverloadExample {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
- **方法重写**:子类继承父类后,重写父类的方法。重写的方法必须与父类被重写的方法具有相同的方法名、参数列表和返回类型(或返回类型是父类方法返回类型的子类)。例如:
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
- JVM 里堆和栈的主要区别
- 存储内容:堆主要用于存储对象实例和数组,所有线程共享堆内存;栈主要存储局部变量、方法调用的上下文等,每个线程都有自己独立的栈空间。
- 内存分配和回收:堆内存的分配和回收由垃圾回收器(GC)负责,程序员无法直接控制;栈内存的分配和回收是由系统自动完成的,当方法调用结束时,栈帧会自动弹出,释放栈内存。
- 生命周期:堆内存的生命周期与应用程序的生命周期相同,只要应用程序在运行,堆内存就会一直存在;栈内存的生命周期与线程的生命周期相同,线程结束时,栈内存会被释放。
- 多线程中,synchronized 关键字和 Lock 接口的区别
- 语法层面:synchronized 是 Java 的关键字,使用时不需要显式地获取和释放锁,由 JVM 自动完成;Lock 是一个接口,需要手动调用 lock() 方法获取锁,unlock() 方法释放锁,通常需要在 finally 块中释放锁,以确保锁一定会被释放。
- 锁的特性:synchronized 是不可中断锁,一旦线程获取了锁,其他线程只能等待;Lock 可以是可中断锁,通过 lockInterruptibly() 方法可以在等待锁的过程中响应中断。synchronized 是非公平锁,Lock 可以通过构造函数指定是否为公平锁。
- 锁的性能:在 JDK 1.6 之前,synchronized 的性能较差,因为它是重量级锁;JDK 1.6 之后,对 synchronized 进行了优化,引入了偏向锁、轻量级锁等,性能有了很大提升。在高并发场景下,Lock 的性能可能会更好,因为它可以根据具体需求进行更灵活的锁控制。
- HashMap 的底层数据结构
- 数组 + 链表 + 红黑树:HashMap 内部维护了一个数组,数组的每个元素称为一个桶(bucket)。当插入一个键值对时,会根据键的哈希值计算出在数组中的索引位置。如果该位置没有元素,直接插入;如果该位置已经有元素,会以链表的形式将新元素插入到链表的尾部。当链表长度超过 8 且数组长度大于 64 时,链表会转化为红黑树,以提高查找效率。当红黑树的节点数小于 6 时,红黑树会退化为链表。
- ArrayList 是如何实现动态扩容的
- 初始容量:ArrayList 有一个默认的初始容量,为 10。当创建一个空的 ArrayList 时,初始容量为 0,第一次添加元素时会将容量扩展为 10。
- 扩容机制:当向 ArrayList 中添加元素时,如果当前容量不够,会调用 grow() 方法进行扩容。扩容时,会创建一个新的数组,新数组的容量是原来的 1.5 倍(oldCapacity + (oldCapacity >> 1)),然后把原数组的元素复制到新数组中。
- 线程池的核心参数及作用
- corePoolSize(核心线程数):线程池一直保持的线程数量,当提交的任务数小于核心线程数时,线程池会创建新的线程来执行任务。
- maximumPoolSize(最大线程数):线程池能容纳的最大线程数量,当任务队列已满且线程数达到核心线程数时,线程池会继续创建新的线程,直到达到最大线程数。
- keepAliveTime(线程空闲存活时间):当线程池中的线程数量超过核心线程数时,多余的线程在空闲一段时间后会被销毁,这个空闲时间就是 keepAliveTime。
- TimeUnit(时间单位):keepAliveTime 的时间单位,如 TimeUnit.SECONDS、TimeUnit.MILLISECONDS 等。
- workQueue(任务队列):用来存储待执行的任务,常用的任务队列有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。
- ThreadFactory(线程工厂):用于创建线程,通过自定义线程工厂可以为线程设置名称、优先级等属性。
- Spring 框架中,依赖注入的方式
- 构造器注入:通过构造函数来注入依赖对象,确保对象在创建时就已经完成了依赖注入。例如:
public class UserService {
private UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
- **setter 方法注入**:通过 setter 方法来注入依赖对象,对象在创建后可以通过调用 setter 方法来设置依赖对象。例如:
public class UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
- **接口注入**:实现特定的接口,通过接口方法来注入依赖对象,这种方式在实际开发中使用较少。
8. Spring Boot 相对于 Spring 的优势 - 快速搭建项目:Spring Boot 提供了大量的 Starter 依赖,只需要在项目中添加相应的 Starter 依赖,就可以快速集成各种功能,减少了手动配置的工作量。 - 自动配置:Spring Boot 会根据项目中添加的依赖自动进行配置,开发者可以根据需要进行自定义配置来覆盖默认配置。 - 内置服务器:Spring Boot 内置了 Tomcat、Jetty 等服务器,不需要手动部署到外部服务器,直接运行项目即可启动服务器。 - 生产级特性:Spring Boot 提供了一些生产级特性,如健康检查、监控等,方便开发者对应用程序进行管理和维护。 9. MyBatis 中,#{} 和 ${} 的区别 - #{}:是预编译处理,MyBatis 会把 #{} 替换为占位符(?),然后使用 PreparedStatement 来执行 SQL 语句,能有效防止 SQL 注入。例如:
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
- **${}**:是直接替换,MyBatis 会把 ${} 直接替换为传入的参数值,不会进行预编译处理,存在 SQL 注入的风险。例如:
<select id="getUserByUsername" parameterType="String" resultType="User">
SELECT * FROM users WHERE username = '${username}'
</select>
- Dubbo 的服务发现机制
- 注册中心:Dubbo 依赖注册中心来实现服务发现。常见的注册中心有 Zookeeper、Nacos、Etcd 等。服务提供者在启动时会将自己的服务信息(如服务名称、地址、端口等)注册到注册中心;服务消费者在启动时会从注册中心订阅所需的服务信息,当服务提供者的信息发生变化时,注册中心会通知服务消费者。
- RabbitMQ 的消息确认机制
- 生产者确认机制:生产者将消息发送到 RabbitMQ 后,可以通过设置 confirm 模式或事务模式来确认消息是否发送成功。在 confirm 模式下,生产者发送消息后,RabbitMQ 会返回一个确认信息给生产者,生产者可以根据这个确认信息来判断消息是否发送成功。
- 消费者确认机制:消费者从 RabbitMQ 接收消息后,需要向 RabbitMQ 发送确认信息,告知 RabbitMQ 该消息已经被成功消费。RabbitMQ 收到确认信息后,会将该消息从队列中删除。消费者确认机制有自动确认和手动确认两种方式,手动确认可以更灵活地控制消息的消费。
- xxl - job 的调度原理
- 调度中心:xxl - job 有一个调度中心,负责任务的调度和管理。调度中心会根据任务的调度策略(如 cron 表达式)来触发任务的执行。
- 执行器:任务的实际执行者,每个执行器可以执行多个任务。执行器会与调度中心建立通信,接收调度中心的调度请求,并执行相应的任务。
- 任务注册:任务需要在调度中心进行注册,包括任务的基本信息(如任务名称、调度策略、执行器等)。调度中心会根据注册信息来管理和调度任务。
- Redis 的持久化机制及特点
- RDB(Redis Database):RDB 是 Redis 的一种快照持久化方式,它会在指定的时间间隔内将 Redis 中的数据快照保存到磁盘上。RDB 文件是一个二进制文件,恢复数据时速度较快,但可能会丢失最后一次快照之后的数据。
- AOF(Append Only File):AOF 是 Redis 的一种日志持久化方式,它会将 Redis 的写操作以日志的形式追加到文件末尾。AOF 文件是一个文本文件,数据安全性较高,因为它记录了每一次的写操作,但文件体积可能会比较大,恢复数据时速度相对较慢。
通过以上的答案详解,希望能帮助小白更好地理解这些 Java 技术点。在实际学习和开发中,还需要不断实践和深入研究,才能真正掌握这些知识。