互联网大厂 Java 面试:核心知识、框架与中间件大考验
在互联网大厂的一间严肃的面试室内,面试官正襟危坐,对面坐着紧张又期待的求职者王铁牛。一场对 Java 核心知识的严格考验即将拉开帷幕。
第一轮提问 面试官:首先问几个 Java 基础问题。Java 中多态的实现方式有哪些? 王铁牛:多态的实现方式主要有两种,方法重载和方法重写。方法重载是在一个类中,有多个方法名相同但参数列表不同的方法;方法重写是子类重写父类的方法,要求方法名、参数列表和返回值类型都相同。 面试官:回答得不错。那 JVM 的内存结构能说一下吗? 王铁牛:JVM 内存结构主要包括堆、栈、方法区等。堆是用来存储对象实例的,是垃圾回收的主要区域;栈分为虚拟机栈和本地方法栈,虚拟机栈存储局部变量等信息,本地方法栈为本地方法服务;方法区存储类的信息、常量、静态变量等。 面试官:很好,理解得挺清晰。那线程和进程的区别是什么? 王铁牛:进程是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位;线程是进程中的一个执行单元,是 CPU 调度和分派的基本单位。一个进程可以包含多个线程。
第二轮提问 面试官:接下来聊聊 JUC 相关的。CountDownLatch 和 CyclicBarrier 有什么区别? 王铁牛:嗯……这个嘛,好像都是和线程同步有关的,具体区别我有点不太确定。 面试官:那说一下线程池的核心参数有哪些,分别有什么作用? 王铁牛:线程池核心参数……有最大线程数,还有……还有个什么最小的,具体名字我忘了,作用嘛就是控制线程的数量。 面试官:再问一个,HashMap 在 JDK1.7 和 JDK1.8 中有哪些主要区别? 王铁牛:这个我知道一点,好像 1.8 有个什么红黑树,其他的就不太清楚了。
第三轮提问 面试官:现在来谈谈框架方面的。Spring 的 AOP 原理是什么? 王铁牛:AOP 就是切面编程,原理嘛……就是可以在方法前后加一些代码,具体怎么实现的我不太明白。 面试官:Spring Boot 的自动配置原理能说一下吗? 王铁牛:自动配置就是能自动配置一些东西,具体怎么自动配置的我讲不清楚。 面试官:MyBatis 的一级缓存和二级缓存是怎么回事? 王铁牛:缓存就是能存数据,一级缓存好像是和会话有关,二级缓存不太清楚。
面试结束,面试官看着王铁牛,严肃地说:“今天的面试就到这里,你先回家等通知吧。我们会综合评估你的表现,之后会有专人联系你。”
答案详解
- Java 中多态的实现方式
- 方法重载:在同一个类中,方法名相同但参数列表不同(参数个数、参数类型或参数顺序不同)。例如:
class Calculator {
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 sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
- JVM 的内存结构
- 堆(Heap):是 JVM 中最大的一块内存区域,所有对象实例和数组都在这里分配内存。它是垃圾回收的主要区域,又可以分为新生代和老年代。新生代包括 Eden 区和两个 Survivor 区。
- 栈(Stack)
- 虚拟机栈:每个线程在执行时都会创建一个虚拟机栈,它的生命周期与线程相同。每个方法在执行时会创建一个栈帧,栈帧中存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 本地方法栈:与虚拟机栈类似,不过它是为本地方法服务的。
- 方法区(Method Area):用于存储类的信息、常量、静态变量、即时编译器编译后的代码等数据。在 JDK1.8 之后,方法区被元空间(Metaspace)取代,元空间使用的是本地内存。
- 线程和进程的区别
- 进程:是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间和系统资源,进程之间的通信比较复杂。
- 线程:是进程中的一个执行单元,是 CPU 调度和分派的基本单位。一个进程可以包含多个线程,线程共享进程的内存空间和系统资源,线程之间的通信相对简单。
- CountDownLatch 和 CyclicBarrier 的区别
- CountDownLatch:是一个计数器,初始化时设置一个初始值,线程可以调用
countDown()方法将计数器减 1,当计数器减为 0 时,等待在await()方法上的线程会被唤醒继续执行。它是一次性的,不能重复使用。例如,主线程等待多个子线程完成任务:
- CountDownLatch:是一个计数器,初始化时设置一个初始值,线程可以调用
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 {
System.out.println(Thread.currentThread().getName() + " is working");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}).start();
}
latch.await();
System.out.println("All threads have finished");
}
}
- **CyclicBarrier**:可以让一组线程在到达一个屏障点时相互等待,直到所有线程都到达该屏障点后,所有线程才会继续执行。它可以重复使用。例如,多个线程并行计算,最后一起汇总结果:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
System.out.println("All threads have reached the barrier");
});
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " is working");
Thread.sleep(1000);
barrier.await();
System.out.println(Thread.currentThread().getName() + " continues after the barrier");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
- 线程池的核心参数及作用
- corePoolSize:核心线程数,线程池在初始化时会创建这么多个核心线程,即使这些线程处于空闲状态,也不会被销毁。
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数量。
- keepAliveTime:线程空闲时间,当线程池中的线程数量超过核心线程数时,空闲线程在经过 keepAliveTime 时间后会被销毁。
- unit:keepAliveTime 的时间单位,如 TimeUnit.SECONDS 等。
- workQueue:工作队列,用于存储等待执行的任务。常见的工作队列有 ArrayBlockingQueue、LinkedBlockingQueue 等。
- threadFactory:线程工厂,用于创建线程。
- handler:拒绝策略,当工作队列已满且线程池中的线程数量达到最大线程数时,新提交的任务会触发拒绝策略。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(让调用者线程执行任务)等。
- HashMap 在 JDK1.7 和 JDK1.8 中的主要区别
- 数据结构:JDK1.7 采用数组 + 链表的结构,JDK1.8 采用数组 + 链表 + 红黑树的结构。当链表长度达到 8 且数组长度达到 64 时,链表会转换为红黑树,以提高查找效率。
- 插入方式:JDK1.7 采用头插法,即新节点插入到链表头部;JDK1.8 采用尾插法,即新节点插入到链表尾部。
- 扩容机制:JDK1.7 在扩容时会重新计算 hash 值并插入到新数组中,可能会导致链表形成环形结构;JDK1.8 在扩容时通过判断节点的 hash 值与原数组长度的与运算结果,将节点分配到新数组的相应位置,避免了环形链表的问题。
- Spring 的 AOP 原理
- Spring AOP 主要基于动态代理实现,有两种方式:
- JDK 动态代理:基于接口实现,通过
java.lang.reflect.Proxy类和InvocationHandler接口来创建代理对象。当目标对象实现了接口时,Spring 会使用 JDK 动态代理。 - CGLIB 动态代理:基于继承实现,通过生成目标对象的子类来创建代理对象。当目标对象没有实现接口时,Spring 会使用 CGLIB 动态代理。
- JDK 动态代理:基于接口实现,通过
- AOP 的实现步骤包括定义切面(Aspect)、切点(Pointcut)和通知(Advice),Spring 会在运行时根据配置将通知织入到目标方法的相应位置。
- Spring AOP 主要基于动态代理实现,有两种方式:
- Spring Boot 的自动配置原理
- Spring Boot 的自动配置是基于
@EnableAutoConfiguration注解实现的。当启动类上添加了@SpringBootApplication注解(该注解包含了@EnableAutoConfiguration)时,Spring Boot 会自动扫描META - INF/spring.factories文件,该文件中定义了一系列的自动配置类。 - 这些自动配置类会根据类路径下的依赖和配置文件中的属性进行条件判断,如果满足条件则会自动配置相应的 Bean。例如,如果类路径下存在
DataSource相关的依赖,Spring Boot 会自动配置数据源。
- Spring Boot 的自动配置是基于
- MyBatis 的一级缓存和二级缓存
- 一级缓存:是会话级别的缓存,每个
SqlSession对象都有一个独立的一级缓存。在同一个SqlSession中,执行相同的查询语句时,第一次查询会将结果存入缓存,后续相同的查询会直接从缓存中获取结果,而不会再次查询数据库。当SqlSession关闭或调用clearCache()方法时,一级缓存会被清空。 - 二级缓存:是全局级别的缓存,多个
SqlSession可以共享二级缓存。需要在mapper文件中添加<cache/>标签或在配置文件中配置开启二级缓存。二级缓存默认是开启的,当执行更新、插入、删除操作时,会清空二级缓存。
- 一级缓存:是会话级别的缓存,每个