互联网大厂 Java 面试:核心知识、框架与中间件大考验
在一间明亮但略显严肃的面试室内,一位穿着整洁西装的面试官正襟危坐,对面坐着略显紧张的求职者王铁牛。面试开始了,一场对 Java 核心知识的考察正式拉开帷幕。
第一轮提问 面试官:首先,我问几个基础的 Java 核心知识问题。Java 中的基本数据类型有哪些? 王铁牛:嗯,Java 的基本数据类型有 byte、short、int、long、float、double、char 和 boolean。 面试官:回答得不错。那说说 Java 中多态的实现方式有哪些? 王铁牛:多态的实现方式主要有继承和接口。通过子类重写父类的方法,或者实现接口的类重写接口中的方法,就可以实现多态。 面试官:很好。那在 Java 里,什么是自动装箱和拆箱? 王铁牛:自动装箱就是把基本数据类型自动转换为对应的包装类对象,比如把 int 转换为 Integer;拆箱就是把包装类对象自动转换为基本数据类型,像把 Integer 转换为 int。 面试官:非常棒,基础很扎实。
第二轮提问 面试官:接下来,我们聊聊 JUC、JVM 和多线程相关的内容。JUC 包下常用的并发工具类有哪些? 王铁牛:嗯……有 CountDownLatch、CyclicBarrier 和 Semaphore。 面试官:不错。那 JVM 的内存模型是怎样的? 王铁牛:呃……JVM 内存主要有堆、栈、方法区这些。堆是存放对象实例的,栈是存放局部变量和方法调用信息的,方法区是存放类的信息、常量、静态变量啥的。 面试官:回答得还行。那多线程编程中,如何避免死锁? 王铁牛:这个嘛……好像要避免循环等待,还有就是加锁顺序要一致。 面试官:大致思路是对的。那线程池的工作原理能说一下吗? 王铁牛:线程池就是先创建一些线程,有任务来了就分配给线程去执行。要是线程都忙,就把任务放到队列里,等有线程空闲了再从队列里取任务执行。 面试官:整体回答得还可以。
第三轮提问 面试官:现在问一些关于框架和中间件的问题。HashMap 和 ArrayList 的区别是什么? 王铁牛:HashMap 是键值对存储的,通过键来查找值,ArrayList 是按顺序存储元素的,通过索引来查找元素。 面试官:不错。那 Spring 框架的核心特性有哪些? 王铁牛:Spring 的核心特性有 IoC(控制反转)和 AOP(面向切面编程)。IoC 就是把对象的创建和管理交给 Spring 容器,AOP 就是在不修改原有代码的基础上增加额外的功能。 面试官:回答得挺好。那 Spring Boot 和 Spring 有什么关系? 王铁牛:Spring Boot 是基于 Spring 的,它简化了 Spring 的配置,让开发更方便快捷。 面试官:很好。那 MyBatis 是如何实现数据库操作的? 王铁牛:MyBatis 是通过映射文件或者注解把 SQL 语句和 Java 方法关联起来,然后执行 SQL 语句和处理结果。 面试官:不错。那 Dubbo、RabbitMq、xxl - job 和 Redis 你都了解哪些应用场景? 王铁牛:Dubbo 主要用于分布式服务调用,RabbitMq 用于消息队列,xxl - job 用于分布式任务调度,Redis 用于缓存、分布式锁这些。不过具体细节我有点不太清楚了。 面试官:嗯,有一定的了解。那今天的面试就到这里,你先回家等通知吧。
问题答案详解
- Java 中的基本数据类型有哪些?
- Java 中有 8 种基本数据类型,可分为 4 类:
- 整数类型:byte(1 字节,范围 -128 到 127)、short(2 字节,范围 -32768 到 32767)、int(4 字节,范围 -2147483648 到 2147483647)、long(8 字节,范围 -2^63 到 2^63 - 1)。
- 浮点类型:float(4 字节,单精度浮点数)、double(8 字节,双精度浮点数)。
- 字符类型:char(2 字节,用于存储单个字符,采用 Unicode 编码)。
- 布尔类型:boolean(只有两个值,true 和 false)。
- Java 中有 8 种基本数据类型,可分为 4 类:
- 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(); // 输出 "Dog barks"
}
}
- **接口**:类实现接口,并实现接口中的抽象方法。通过接口引用指向实现类的对象,调用接口方法时会执行实现类的具体实现。例如:
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());
}
}
- 在 Java 里,什么是自动装箱和拆箱?
- 自动装箱:是指 Java 编译器在基本数据类型和对应的包装类之间进行的自动转换。例如:
Integer num = 10; // 自动装箱,相当于 Integer num = Integer.valueOf(10);
- **自动拆箱**:是指将包装类对象自动转换为基本数据类型。例如:
Integer num = 10;
int value = num; // 自动拆箱,相当于 int value = num.intValue();
- JUC 包下常用的并发工具类有哪些?
- 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 their work");
}
}
- **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();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
- **Semaphore**:一个计数信号量,用于控制同时访问某个资源的线程数量。例如,限制同时访问某个文件的线程数量:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int permits = 2;
Semaphore semaphore = new Semaphore(permits);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " has acquired the permit");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + " has released the permit");
}
}).start();
}
}
}
- JVM 的内存模型是怎样的?
- 堆(Heap):是 JVM 中最大的一块内存区域,用于存储对象实例和数组。所有通过 new 创建的对象都存放在堆中。堆是线程共享的,会出现垃圾回收机制来回收不再使用的对象。
- 栈(Stack):每个线程都有自己的栈,栈中存储局部变量、方法调用信息和操作数栈等。每个方法在执行时会创建一个栈帧,栈帧包含局部变量表、操作数栈、动态链接和方法出口等信息。方法执行结束后,栈帧会被弹出。
- 方法区(Method Area):用于存储类的信息、常量、静态变量等。方法区也是线程共享的。在 Java 8 及以后,方法区被元空间(Metaspace)取代,元空间使用本地内存,而不是 JVM 内存。
- 程序计数器(Program Counter Register):每个线程都有一个独立的程序计数器,用于记录当前线程执行的字节码指令地址。
- 本地方法栈(Native Method Stack):与栈类似,不过它是为本地方法(使用 native 关键字修饰的方法)服务的。
- 多线程编程中,如何避免死锁?
- 避免循环等待:线程获取锁的顺序要一致,避免不同线程以不同的顺序获取多个锁,从而形成循环等待的情况。
- 限时获取锁:使用
tryLock方法,在一定时间内尝试获取锁,如果获取不到则放弃,避免线程一直等待。例如:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockAvoidance {
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
if (lock1.tryLock()) {
try {
Thread.sleep(100);
if (lock2.tryLock()) {
try {
System.out.println("Thread 1 has both locks");
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
if (lock1.tryLock()) {
try {
if (lock2.tryLock()) {
try {
System.out.println("Thread 2 has both locks");
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
} catch (Exception e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
}
}
- **减少锁的持有时间**:尽量减少线程持有锁的时间,避免长时间占用锁导致其他线程等待。
7. 线程池的工作原理能说一下吗? - 线程池的工作原理主要包括以下几个步骤: - 创建线程池:初始化线程池时,会创建一定数量的核心线程。 - 任务提交:当有任务提交到线程池时,线程池会首先检查核心线程是否有空闲的。如果有,就将任务分配给空闲的核心线程执行。 - 核心线程满了:如果核心线程都在工作,任务会被放入任务队列中。 - 任务队列满了:如果任务队列也满了,线程池会创建新的线程(非核心线程)来执行任务,直到达到最大线程数。 - 最大线程数也满了:如果最大线程数也达到了,新提交的任务会根据线程池的拒绝策略进行处理,常见的拒绝策略有抛出异常、直接丢弃、丢弃最老的任务等。 - 线程回收:当线程空闲时间超过一定阈值时,非核心线程会被回收。 8. HashMap 和 ArrayList 的区别是什么? - 存储方式: - HashMap:以键值对的形式存储数据,通过键来查找值,键是唯一的。 - ArrayList:按顺序存储元素,通过索引来查找元素,索引从 0 开始。 - 数据结构: - HashMap:基于哈希表实现,通过哈希函数将键映射到数组的某个位置,可能会存在哈希冲突,需要解决冲突。 - ArrayList:基于数组实现,会自动扩容。 - 使用场景: - HashMap:适用于需要通过键快速查找值的场景,比如缓存、配置信息存储等。 - ArrayList:适用于需要按顺序存储和访问元素的场景,比如列表展示等。 9. Spring 框架的核心特性有哪些? - IoC(控制反转):也称为依赖注入(DI),是指将对象的创建和管理交给 Spring 容器,而不是由对象本身来创建和管理依赖对象。通过 IoC,降低了对象之间的耦合度。例如,通过 XML 配置或注解的方式将依赖对象注入到目标对象中:
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;
}
public void doSomething() {
serviceB.doAnotherThing();
}
}
@Component
class ServiceB {
public void doAnotherThing() {
System.out.println("Doing another thing");
}
}
- **AOP(面向切面编程)**:是一种编程范式,用于在不修改原有代码的基础上,增加额外的功能,比如日志记录、事务管理等。AOP 通过将这些横切关注点(如日志、事务)从业务逻辑中分离出来,提高了代码的可维护性和可复用性。例如,使用 Spring AOP 实现日志记录:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("Method " + joinPoint.getSignature().getName() + " has finished");
}
}
- Spring Boot 和 Spring 有什么关系?
- Spring Boot 是基于 Spring 构建的,它简化了 Spring 应用的开发和配置。Spring 是一个功能强大的框架,但配置相对复杂,需要编写大量的 XML 配置文件或使用 Java 配置类。而 Spring Boot 通过自动配置和约定大于配置的原则,减少了开发人员的配置工作,让开发人员可以更快速地搭建和开发 Spring 应用。例如,使用 Spring Boot 可以通过一个简单的
@SpringBootApplication注解来启动一个 Spring 应用:
- Spring Boot 是基于 Spring 构建的,它简化了 Spring 应用的开发和配置。Spring 是一个功能强大的框架,但配置相对复杂,需要编写大量的 XML 配置文件或使用 Java 配置类。而 Spring Boot 通过自动配置和约定大于配置的原则,减少了开发人员的配置工作,让开发人员可以更快速地搭建和开发 Spring 应用。例如,使用 Spring Boot 可以通过一个简单的
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
- MyBatis 是如何实现数据库操作的?
- 配置文件:MyBatis 需要配置数据源、映射文件等信息。可以使用 XML 配置文件或 Java 代码进行配置。
- 映射文件或注解:通过映射文件(XML 文件)或注解将 SQL 语句和 Java 方法关联起来。例如,使用 XML 映射文件:
<mapper namespace="com.example.dao.UserDao">