互联网大厂 Java 面试:核心知识、框架与中间件大考验
王铁牛,一位怀揣着进入互联网大厂梦想的 Java 程序员,走进了这场决定他职业走向的面试。严肃的面试官坐在桌前,一场技术的较量即将开始。
第一轮面试 面试官:首先问几个 Java 核心知识的问题。Java 中基本数据类型有哪些? 王铁牛:这个我知道,Java 基本数据类型有 byte、short、int、long、float、double、char、boolean。 面试官:不错,回答得很准确。那 Java 中多态的实现方式有哪些? 王铁牛:多态主要通过继承、接口实现和方法重写来实现。子类继承父类,重写父类的方法,就可以实现多态。实现接口并重写接口方法也能实现多态。 面试官:回答得很好。那在 Java 中,String 类为什么是不可变的? 王铁牛:因为 String 类被 final 修饰,它的成员变量也是 final 的,所以它是不可变的。 面试官:非常棒,你的基础很扎实。
第二轮面试 面试官:接下来聊聊 JUC 和多线程的问题。什么是线程池,使用线程池有什么好处? 王铁牛:线程池就是管理线程的一个池子,使用线程池可以减少线程创建和销毁的开销,提高性能,还能控制线程的数量,避免资源过度使用。 面试官:很好。那在 JUC 中,CountDownLatch 和 CyclicBarrier 有什么区别? 王铁牛:嗯……这个嘛,好像……它们都是用来协调线程的,具体区别我有点记不太清了。 面试官:没关系,接着问,在多线程环境下,如何保证数据的一致性? 王铁牛:可以使用 synchronized 关键字或者 Lock 接口来加锁,保证同一时间只有一个线程能访问共享数据。 面试官:前面那个问题回答得不太准确,但后面这个回答得还行。
第三轮面试 面试官:现在谈谈一些框架和中间件。Spring 的 IOC 和 AOP 是什么? 王铁牛:IOC 是控制反转,把对象的创建和依赖关系的管理交给 Spring 容器。AOP 是面向切面编程,用于在不修改原有代码的情况下,对程序进行增强。 面试官:回答得不错。那 Spring Boot 相对于 Spring 有什么优势? 王铁牛:Spring Boot 简化了 Spring 的配置,它有自动配置功能,能快速搭建项目,减少了很多样板代码。 面试官:很好。那 MyBatis 中 #{} 和 ${} 的区别是什么? 王铁牛:这个……好像一个是预编译,一个不是,具体哪个对应哪个我有点乱了。 面试官:整体表现有好有坏吧。你先回家等通知吧,后续如果有消息会及时联系你。
问题答案
- Java 中基本数据类型有哪些? Java 的基本数据类型分为四类八种:
- 整数类型:byte(1 字节)、short(2 字节)、int(4 字节)、long(8 字节)。
- 浮点类型:float(4 字节)、double(8 字节)。
- 字符类型:char(2 字节)。
- 布尔类型:boolean(理论上 1 位,但实际实现中通常为 1 字节)。
- Java 中多态的实现方式有哪些?
- 继承:子类继承父类,重写父类的方法。在调用方法时,根据实际对象的类型来决定调用哪个类的方法。例如:
class Animal {
public void sound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Woof");
}
}
// 使用
Animal animal = new Dog();
animal.sound(); // 输出 Woof
- 接口实现:类实现接口并重写接口中的方法。不同的类实现同一个接口,可以有不同的实现逻辑。例如:
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;
}
}
class Rectangle implements Shape {
private double length;
private double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
public double area() {
return length * width;
}
}
- 方法重写:子类对父类中具有相同方法名、参数列表和返回类型的方法进行重新实现。
- 在 Java 中,String 类为什么是不可变的?
- String 类被 final 修饰,这意味着它不能被继承。
- String 类内部的字符数组 value 也是被 final 修饰的,并且没有提供修改字符数组内容的方法。一旦创建了 String 对象,其内部的字符序列就不能被改变。例如:
String str = "hello";
str = str + " world"; // 这里实际上是创建了一个新的 String 对象
不可变的好处包括线程安全、可以作为 HashMap 的键、提高性能(字符串常量池的使用)等。
- 什么是线程池,使用线程池有什么好处? 线程池是一种管理线程的机制,它预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务,任务执行完后线程不会销毁,而是返回线程池等待下一个任务。 使用线程池的好处:
- 减少线程创建和销毁的开销:线程的创建和销毁需要消耗系统资源,使用线程池可以避免频繁创建和销毁线程,提高性能。
- 提高响应速度:任务提交后可以立即从线程池中获取线程执行,不需要等待线程创建。
- 控制线程数量:可以避免创建过多的线程导致系统资源耗尽。
- 方便线程管理:可以对线程进行统一的监控和管理。
- 在 JUC 中,CountDownLatch 和 CyclicBarrier 有什么区别?
- CountDownLatch:是一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。它有一个计数器,当计数器的值减为 0 时,等待的线程会被唤醒。计数器一旦减为 0 就不能再重置。例如:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " finished");
latch.countDown();
}).start();
}
latch.await();
System.out.println("All threads finished");
}
}
- CyclicBarrier:也是一个同步辅助类,它允许一组线程相互等待,直到所有线程都到达一个屏障点,然后所有线程继续执行。它的计数器可以重置,因此可以重复使用。例如:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All threads reached the barrier"));
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " is waiting at the barrier");
barrier.await();
System.out.println(Thread.currentThread().getName() + " passed the barrier");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
- 在多线程环境下,如何保证数据的一致性?
- 使用 synchronized 关键字:可以修饰方法或代码块,保证同一时间只有一个线程能访问被修饰的方法或代码块。例如:
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
}
- 使用 Lock 接口:例如 ReentrantLock。相比于 synchronized,Lock 提供了更灵活的锁机制,如可中断锁、公平锁等。例如:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
- 使用原子类:如 AtomicInteger、AtomicLong 等,它们基于 CAS(Compare-And-Swap)操作,保证对变量的原子性操作。例如:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
}
- Spring 的 IOC 和 AOP 是什么?
- IOC(Inversion of Control,控制反转):是一种设计原则,把对象的创建和依赖关系的管理从代码中移除,交给 Spring 容器来完成。通过依赖注入(Dependency Injection)的方式,将对象的依赖关系注入到对象中。例如:
// 接口
interface UserService {
void addUser();
}
// 实现类
class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("Add user");
}
}
// 使用 Spring 配置文件或注解注入
- AOP(Aspect-Oriented Programming,面向切面编程):是一种编程范式,用于在不修改原有代码的情况下,对程序进行增强。它将一些通用的功能(如日志记录、事务管理等)从业务逻辑中分离出来,形成一个个切面,在特定的连接点(如方法调用前后)插入这些切面代码。例如:
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")
public void beforeServiceMethod() {
System.out.println("Before service method");
}
@After("serviceMethods()")
public void afterServiceMethod() {
System.out.println("After service method");
}
}
- Spring Boot 相对于 Spring 有什么优势?
- 简化配置:Spring Boot 提供了自动配置功能,根据项目中引入的依赖自动配置 Spring 应用,减少了大量的 XML 配置文件和 Java 配置代码。
- 快速搭建项目:通过 Spring Initializr 可以快速生成一个基本的 Spring Boot 项目骨架,包含所需的依赖和基本配置。
- 嵌入式服务器:Spring Boot 内置了 Tomcat、Jetty 等嵌入式服务器,无需额外部署服务器,直接运行项目即可。
- 生产级特性:提供了如监控、健康检查、配置管理等生产级特性,方便应用的运维和管理。
- 依赖管理:Spring Boot 的 Starter 依赖可以自动管理项目的依赖,避免了依赖冲突。
- MyBatis 中 #{} 和 ${} 的区别是什么?
- #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为占位符 ?,然后使用 PreparedStatement 进行预编译,能有效防止 SQL 注入攻击。例如:
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
- {} 时,会将 ${} 直接替换为传入的参数值。这种方式可能会导致 SQL 注入攻击,一般用于动态表名、列名等情况。例如:
<select id="getUserByColumn" parameterType="map" resultType="User">
SELECT * FROM users WHERE ${column} = #{value}
</select>