第一轮面试:Java 基础与并发编程考验
面试官:你好,先问几个基础问题。Java 中多态的实现方式有哪些? 王铁牛:多态的实现方式主要有方法重载和方法重写。方法重载是在一个类中,方法名相同但参数列表不同;方法重写是子类重写父类的方法。 面试官:回答得不错。那说说 JUC 包下 CountDownLatch 的作用是什么? 王铁牛:CountDownLatch 可以让一个或多个线程等待其他线程完成操作。就像比赛时,运动员都准备好(线程完成操作),裁判才开始(主线程继续执行)。 面试官:很好。那线程池有哪些创建方式? 王铁牛:可以通过 Executors 工具类创建,比如 newFixedThreadPool 创建固定大小的线程池,newCachedThreadPool 创建可缓存的线程池,还有通过 ThreadPoolExecutor 自定义创建。 面试官:回答得很全面,看来基础掌握得不错。
第二轮面试:数据结构与框架深入探究
面试官:接着聊聊数据结构,HashMap 在 JDK 1.8 有哪些优化? 王铁牛:呃……好像是增加了红黑树吧,当链表长度超过 8 就会转成红黑树。 面试官:对,那 ArrayList 是如何实现动态扩容的? 王铁牛:这个……大概就是容量不够的时候就扩大容量,具体咋扩我有点不太确定。 面试官:好吧。那说说 Spring 的核心特性有哪些? 王铁牛:Spring 核心特性有 IOC(控制反转)和 AOP(面向切面编程)。IOC 就是把对象的创建和依赖关系的管理交给 Spring 容器;AOP 可以在不修改源代码的情况下增强功能。 面试官:基本正确,不过关于 ArrayList 扩容这块还得再巩固下。
第三轮面试:中间件与分布式知识挑战
面试官:我们来谈谈中间件,MyBatis 是如何实现 SQL 语句和 Java 代码的映射的? 王铁牛:好像是通过 XML 配置文件或者注解,具体咋弄我有点说不清楚。 面试官:那 Dubbo 的服务发现机制是怎样的? 王铁牛:Dubbo 服务发现……我记得和注册中心有关,具体啥原理我不太知道。 面试官:RabbitMQ 如何保证消息的可靠性传输? 王铁牛:这个……好像有确认机制,具体咋实现我不太了解。 面试官:你对中间件这块的知识掌握得不够扎实,后续还需要加强学习。
面试结束,面试官严肃地说:“今天的面试就到这里,你整体基础还可以,但在一些深入的技术点上还有所欠缺,尤其是中间件和分布式相关知识。你先回家等通知吧,我们会综合评估后给你答复。”
答案详解
- Java 中多态的实现方式有哪些:
- 方法重载(Overloading):在同一个类中,方法名相同但参数列表不同(参数的类型、个数、顺序不同),与返回值类型无关。编译器会根据调用方法时传入的实际参数来决定调用哪个方法。例如:
class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
- 方法重写(Overriding):子类继承父类后,重写父类中的方法。要求方法名、参数列表和返回值类型都相同(返回值类型可以是父类方法返回值类型的子类,这称为协变返回类型)。重写的方法不能比父类中被重写的方法有更严格的访问权限。例如:
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");
}
}
- JUC 包下 CountDownLatch 的作用是什么: CountDownLatch 是一个同步工具类,它允许一个或多个线程等待其他线程完成操作。它使用一个计数器来实现,初始化时设置计数器的值,每当一个线程完成任务时,计数器的值减 1,当计数器的值为 0 时,等待的线程可以继续执行。例如,在一个多线程的任务中,主线程需要等待所有子线程完成任务后再继续执行:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
// 模拟线程执行任务
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " completed");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 线程完成任务,计数器减 1
latch.countDown();
}
}).start();
}
// 主线程等待所有子线程完成任务
latch.await();
System.out.println("All threads completed, main thread continues");
}
}
- 线程池有哪些创建方式:
- 通过 Executors 工具类创建:
newFixedThreadPool(int nThreads):创建一个固定大小的线程池,线程池中的线程数量始终保持不变。当有新任务提交时,如果线程池中有空闲线程,则立即执行;如果没有空闲线程,则将任务放入队列中等待。
- 通过 Executors 工具类创建:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 5; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is running");
});
}
executor.shutdown();
}
}
- `newCachedThreadPool()`:创建一个可缓存的线程池,线程池中的线程数量可以动态变化。如果有新任务提交,且线程池中有空闲线程,则立即执行;如果没有空闲线程,则创建新线程执行任务。当线程空闲时间超过 60 秒时,会被回收。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is running");
});
}
executor.shutdown();
}
}
- **通过 ThreadPoolExecutor 自定义创建**:
ThreadPoolExecutor 是线程池的核心实现类,通过它可以自定义线程池的各种参数,如核心线程数、最大线程数、线程空闲时间、任务队列等。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolExample {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60, // 线程空闲时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10) // 任务队列
);
for (int i = 0; i < 5; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is running");
});
}
executor.shutdown();
}
}
- HashMap 在 JDK 1.8 有哪些优化:
- 数据结构:JDK 1.8 之前,HashMap 采用数组 + 链表的结构;JDK 1.8 引入了红黑树,当链表长度超过 8 且数组长度大于 64 时,链表会转换为红黑树,以提高查找效率。当红黑树节点数小于 6 时,会转换回链表。
- 插入方式:JDK 1.8 之前采用头插法,多线程环境下可能会导致链表成环的问题;JDK 1.8 采用尾插法,避免了这个问题。
- 哈希算法:JDK 1.8 对哈希算法进行了优化,减少了哈希冲突的概率。
- ArrayList 是如何实现动态扩容的:
ArrayList 底层使用数组存储元素,当添加元素时,如果数组容量不足,会进行扩容操作。具体步骤如下:
- 当调用
add方法添加元素时,会先检查当前数组的容量是否足够,如果不够,则调用grow方法进行扩容。 grow方法会计算新的容量,新容量为旧容量的 1.5 倍(oldCapacity + (oldCapacity >> 1))。- 然后使用
Arrays.copyOf方法将旧数组中的元素复制到新数组中。
- 当调用
import java.util.ArrayList;
public class ArrayListResizeExample {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 20; i++) {
list.add(i);
}
}
}
- Spring 的核心特性有哪些:
- IOC(控制反转):控制反转是指将对象的创建和依赖关系的管理从代码中转移到 Spring 容器中。Spring 容器负责创建对象、管理对象的生命周期和依赖关系。通过 IOC,降低了代码的耦合度,提高了可维护性和可测试性。例如,通过 XML 配置文件或注解来定义 Bean 和它们之间的依赖关系:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
class ServiceA {
private ServiceB serviceB;
@Autowired
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Component
class ServiceB {
// ...
}
- **AOP(面向切面编程)**:面向切面编程是指在不修改源代码的情况下,对程序的功能进行增强。AOP 可以将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,提高代码的复用性和可维护性。Spring AOP 基于代理模式实现,有两种代理方式:JDK 动态代理和 CGLIB 代理。例如,使用注解定义切面和通知:
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
class LoggingAspect {
@After("execution(* com.example.service.*.*(..))")
public void logAfterMethod() {
System.out.println("Method executed");
}
}
- MyBatis 是如何实现 SQL 语句和 Java 代码的映射的:
- XML 配置方式:通过编写 XML 配置文件,在文件中定义 SQL 语句和 Java 方法的映射关系。例如:
<mapper namespace="com.example.dao.UserDao">
<select id="getUserById" parameterType="int" resultType="com.example.entity.User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
在 Java 代码中,通过 Mapper 接口调用相应的方法:
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserDao {
User getUserById(int id);
}
- **注解方式**:在 Mapper 接口的方法上使用注解来定义 SQL 语句。例如:
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserDao {
@Select("SELECT * FROM users WHERE id = #{id}")
User getUserById(int id);
}
- Dubbo 的服务发现机制是怎样的:
Dubbo 的服务发现机制依赖于注册中心,常见的注册中心有 ZooKeeper、Nacos 等。服务发现的过程如下:
- 服务提供者:在启动时,将自己提供的服务信息(服务接口、服务地址等)注册到注册中心。
- 服务消费者:在启动时,从注册中心订阅所需的服务信息,并缓存到本地。当需要调用服务时,直接从本地缓存中获取服务提供者的地址。
- 注册中心:负责管理服务提供者的注册信息和服务消费者的订阅信息,当服务提供者的信息发生变化时,会及时通知服务消费者。 例如,使用 ZooKeeper 作为注册中心,在 Dubbo 配置文件中配置注册中心地址:
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
- RabbitMQ 如何保证消息的可靠性传输:
- 生产者确认机制:生产者将消息发送到 RabbitMQ 后,RabbitMQ 会返回一个确认信号给生产者,告知消息是否成功接收。有两种确认模式:
- 同步确认:生产者发送消息后,等待 RabbitMQ 的确认信号,只有收到确认信号后才继续发送下一条消息。
- 异步确认:生产者发送消息后,继续发送其他消息,RabbitMQ 异步返回确认信号,生产者通过回调函数处理确认结果。
- 消息持久化:将队列和消息都设置为持久化,这样即使 RabbitMQ 服务器重启,消息也不会丢失。在创建队列和发送消息时,设置相应的参数:
- 生产者确认机制:生产者将消息发送到 RabbitMQ 后,RabbitMQ 会返回一个确认信号给生产者,告知消息是否成功接收。有两种确认模式:
// 创建持久化队列
channel.queueDeclare("queue_name", true, false, false, null);
// 发送持久化消息
channel.basicPublish("", "queue_name", MessageProperties.PERSISTENT_TEXT_PLAIN, "message".getBytes());
- **消费者确认机制**:消费者从队列中获取消息后,需要向 RabbitMQ 发送确认信号,告知消息已经成功处理。有两种确认模式:
- **自动确认**:消费者获取消息后,RabbitMQ 自动认为消息已经处理成功。
- **手动确认**:消费者处理完消息后,手动向 RabbitMQ 发送确认信号。如果消费者在处理消息过程中出现异常,可以拒绝消息,让 RabbitMQ 重新发送。
// 手动确认模式
channel.basicConsume("queue_name", false, (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
try {
// 处理消息
System.out.println("Received message: " + message);
// 手动确认消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
} catch (Exception e) {
// 拒绝消息,让 RabbitMQ 重新发送
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true);
}
}, consumerTag -> {});