互联网大厂 Java 面试:核心知识、框架与中间件大考验
王铁牛怀揣着忐忑与期待,坐在了这家互联网大厂的面试会议室里。对面,严肃的面试官正翻看着他的简历,一场决定他职场命运的面试即将拉开帷幕。
第一轮提问:Java 基础与常用类
面试官:首先,我问你几个基础问题。Java 中多态的实现方式有哪些?
王铁牛:嗯,有继承和接口实现两种方式。继承的话,子类可以重写父类的方法,调用时根据实际的对象类型调用对应的方法;接口实现就是一个类实现接口里的抽象方法。
面试官:回答得不错。那 HashMap 的底层数据结构是什么?
王铁牛:HashMap 底层是数组 + 链表 + 红黑树。数组是主体,链表是为了解决哈希冲突,当链表长度超过 8 且数组长度大于 64 时,链表会转化为红黑树,提升查找效率。
面试官:很好。那 ArrayList 是线程安全的吗?如果要实现线程安全的动态数组有什么办法?
王铁牛:ArrayList 不是线程安全的。可以用 Vector 类,它的方法都是同步的;也可以用 Collections.synchronizedList 方法把 ArrayList 包装成线程安全的。
面试官:回答得很清晰,基础很扎实。
第二轮提问:多线程与 JUC
面试官:接下来聊聊多线程。创建线程有哪几种方式?
王铁牛:有继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池这几种方式。
面试官:那线程池的核心参数有哪些,分别代表什么含义?
王铁牛:线程池的核心参数有 corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory 和 handler。corePoolSize 是核心线程数,maximumPoolSize 是最大线程数,keepAliveTime 是线程空闲时的存活时间,unit 是时间单位,workQueue 是任务队列,threadFactory 是线程工厂,handler 是任务拒绝策略。
面试官:那在 JUC 里,CountDownLatch 和 CyclicBarrier 有什么区别?
王铁牛:这个……嗯……好像都是和线程同步有关的,具体区别我有点记不清了,好像一个是倒计数,一个是循环的吧。(回答得含糊不清)
面试官:这两个是 JUC 里很重要的同步工具类,你下去还是要好好复习下。
第三轮提问:框架与中间件
面试官:现在问你一些框架相关的问题。Spring 的 IOC 和 AOP 是什么,有什么作用?
王铁牛:IOC 是控制反转,把对象的创建和依赖关系的管理交给 Spring 容器,降低了代码的耦合度;AOP 是面向切面编程,用于在不修改原有代码的情况下,对程序进行增强,比如实现日志记录、事务管理等。
面试官:Spring Boot 相对于 Spring 有什么优势?
王铁牛:Spring Boot 简化了 Spring 的开发,它提供了自动配置,减少了大量的配置文件,还内置了服务器,方便快速部署。
面试官:MyBatis 是如何实现 SQL 映射的?
王铁牛:MyBatis 通过 XML 映射文件或者注解来实现 SQL 映射。在 XML 里定义 SQL 语句,然后通过命名空间和 ID 来引用;注解的话就是直接在接口方法上写 SQL 语句。
面试官:那 Dubbo、RabbitMQ、xxl - job 和 Redis 这几个中间件,你分别说说它们的主要用途。
王铁牛:Dubbo 是分布式服务框架,用于服务的注册、发现和调用;RabbitMQ 是消息队列,用于异步通信和解耦;xxl - job 是分布式任务调度平台;Redis 是缓存数据库,能提高系统的读写性能。不过具体的一些实现细节我有点不太确定。(回答不够深入)
面试官推了推眼镜,说道:“今天的面试就到这里,你的基础部分掌握得还可以,但在一些高级知识和中间件的细节上还有所欠缺。你先回家等通知吧,后续如果有进一步的消息,我们会及时联系你。”
问题答案
- Java 中多态的实现方式有哪些?
- 继承:子类继承父类,并重写父类的方法。在调用方法时,根据实际创建的对象类型来决定调用哪个类的方法。例如:
class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Dog();
animal.sound();
}
}
- 接口实现:一个类实现一个或多个接口,并实现接口中的抽象方法。通过接口类型的引用指向实现类的对象,调用接口方法时会执行实现类的具体实现。例如:
interface Shape {
double area();
}
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public class Main {
public static void main(String[] args) {
Shape shape = new Circle(5);
System.out.println(shape.area());
}
}
- HashMap 的底层数据结构是什么?
- HashMap 底层是数组 + 链表 + 红黑树。数组是主体,每个数组元素称为一个桶(bucket)。当调用 put 方法插入键值对时,会根据键的哈希值计算出在数组中的索引位置。如果该位置没有元素,直接插入;如果有元素,说明发生了哈希冲突,会以链表的形式将新元素插入到该位置的链表尾部。当链表长度超过 8 且数组长度大于 64 时,链表会转化为红黑树,红黑树是一种自平衡的二叉搜索树,能将查找、插入和删除操作的时间复杂度从 O(n) 降低到 O(log n),提升了查找效率。当链表长度小于 6 时,红黑树会退化为链表。
- ArrayList 是线程安全的吗?如果要实现线程安全的动态数组有什么办法?
- ArrayList 不是线程安全的。在多线程环境下,如果多个线程同时对 ArrayList 进行读写操作,可能会出现数据不一致、数组越界等问题。
- 实现线程安全的动态数组的方法:
- 使用 Vector 类:Vector 是 Java 早期提供的动态数组类,它的方法都是同步的,即使用了 synchronized 关键字修饰,保证了线程安全。但由于同步操作会带来一定的性能开销,所以在单线程环境下不建议使用。例如:
import java.util.Vector;
public class Main {
public static void main(String[] args) {
Vector<Integer> vector = new Vector<>();
vector.add(1);
System.out.println(vector.get(0));
}
}
- 使用 Collections.synchronizedList 方法:可以将一个普通的 ArrayList 包装成线程安全的列表。例如:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
List<Integer> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add(1);
System.out.println(synchronizedList.get(0));
}
}
- 创建线程有哪几种方式?
- 继承 Thread 类:创建一个类继承 Thread 类,重写 run 方法,在 run 方法中定义线程要执行的任务。然后创建该类的对象并调用 start 方法启动线程。例如:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
- 实现 Runnable 接口:创建一个类实现 Runnable 接口,实现 run 方法。然后创建该类的对象,并将其作为参数传递给 Thread 类的构造函数,最后调用 Thread 对象的 start 方法启动线程。例如:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable is running");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
- 实现 Callable 接口:创建一个类实现 Callable 接口,实现 call 方法,该方法有返回值。使用 FutureTask 类来包装 Callable 对象,将 FutureTask 对象作为参数传递给 Thread 类的构造函数,调用 start 方法启动线程。通过 FutureTask 的 get 方法可以获取线程执行的结果。例如:
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 1 + 2;
}
}
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
- 使用线程池:线程池可以管理和复用线程,提高线程的使用效率。可以使用 Executors 类提供的工厂方法创建不同类型的线程池,例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Task is running");
}
}
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
MyRunnable runnable = new MyRunnable();
executorService.submit(runnable);
executorService.shutdown();
}
}
- 线程池的核心参数有哪些,分别代表什么含义?
- corePoolSize:核心线程数,线程池在初始化时会创建的线程数量。当有新任务提交时,如果当前线程数小于 corePoolSize,会创建新的线程来执行任务。
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数量。当任务队列已满,且当前线程数小于 maximumPoolSize 时,会创建新的线程来处理任务。
- keepAliveTime:线程空闲时的存活时间。当线程数大于 corePoolSize 时,空闲线程在超过 keepAliveTime 时间后会被销毁。
- unit:keepAliveTime 的时间单位,例如 TimeUnit.SECONDS 表示秒。
- workQueue:任务队列,用于存储等待执行的任务。常见的任务队列有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。
- threadFactory:线程工厂,用于创建线程。可以自定义线程工厂来设置线程的名称、优先级等属性。
- handler:任务拒绝策略,当任务队列已满且线程数达到 maximumPoolSize 时,新提交的任务会被拒绝。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程处理任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。
- 在 JUC 里,CountDownLatch 和 CyclicBarrier 有什么区别?
- CountDownLatch:它是一个计数器,初始化时需要指定一个计数初始值。当一个或多个线程调用 countDown 方法时,计数器的值会减 1。其他线程可以调用 await 方法来等待计数器的值变为 0。一旦计数器的值变为 0,等待的线程会被唤醒继续执行。CountDownLatch 是一次性的,计数器的值不能重置。例如,主线程需要等待多个子线程完成任务后再继续执行,可以使用 CountDownLatch:
import java.util.concurrent.CountDownLatch;
public class Main {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread finished");
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("All threads finished");
}
}
- CyclicBarrier:它也用于线程同步,初始化时需要指定一个线程数量和一个可选的屏障动作。当多个线程调用 await 方法时,会进入等待状态,直到指定数量的线程都调用了 await 方法,这些线程会同时被唤醒继续执行。CyclicBarrier 可以重复使用,即可以多次等待指定数量的线程到达屏障点。例如,多个线程需要同时开始执行某个任务,可以使用 CyclicBarrier:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Main {
public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier cyclicBarrier = new CyclicBarrier(threadCount, () -> {
System.out.println("All threads are ready, start!");
});
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
System.out.println("Thread is ready");
cyclicBarrier.await();
System.out.println("Thread is running");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
- Spring 的 IOC 和 AOP 是什么,有什么作用?
- IOC(控制反转):传统的软件开发中,对象的创建和依赖关系的管理由代码本身负责。而在 Spring 中,IOC 把对象的创建和依赖关系的管理交给 Spring 容器。通过配置文件或者注解的方式,告诉 Spring 容器需要创建哪些对象以及它们之间的依赖关系。这样可以降低代码的耦合度,提高代码的可维护性和可测试性。例如,在 Spring 中可以通过注解 @Component 来创建一个 Bean,然后通过 @Autowired 注解来实现依赖注入:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
class ServiceA {
public void doSomething() {
System.out.println("ServiceA is doing something");
}
}
@Component
class ServiceB {
@Autowired
private ServiceA serviceA;
public void callServiceA() {
serviceA.doSomething();
}
}
- AOP(面向切面编程):AOP 用于在不修改原有代码的情况下,对程序进行增强。它将那些与业务逻辑无关,但却被多个模块共同使用的功能(如日志记录、事务管理、权限验证等)提取出来,形成一个独立的切面。在程序运行时,通过动态代理的方式将这些切面织入到目标方法的前后、异常抛出时等特定位置。Spring AOP 支持基于代理的 AOP 实现,有 JDK 动态代理和 CGLIB 代理两种方式。例如,使用 Spring AOP 实现日志记录:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature().getName());
}
}
- Spring Boot 相对于 Spring 有什么优势?
- 简化配置:Spring Boot 提供了自动配置功能,根据项目中引入的依赖和配置,自动进行一些默认的配置,减少了大量的 XML 配置文件和 Java 配置代码。例如,引入 Spring Boot 的 Web 依赖后,它会自动配置嵌入式服务器(如 Tomcat、Jetty 等)和 Spring MVC。
- 快速开发:Spring Boot 内置了服务器,无需手动部署到外部服务器,只需要运行一个 Java 主类就可以启动应用程序,方便快速开发和测试。
- 依赖管理:Spring Boot 提供了依赖管理功能,通过父 POM 文件统一管理项目的依赖版本,避免了依赖冲突的问题。
- 生产就绪:Spring Boot 提供了一系列的生产就绪特性,如健康检查、指标监控、外部配置等,方便在生产环境中对应用程序进行管理和监控。
- MyBatis 是如何实现 SQL 映射的?
- XML 映射文件:在 XML 映射文件中定义 SQL 语句,通过命名空间和 ID 来唯一标识每个 SQL 语句。例如:
<mapper namespace="com.example.dao.UserDao">