互联网大厂 Java 面试:核心知识、框架与中间件大考验
在互联网大厂的一间明亮的面试室内,严肃的面试官坐在桌前,对面坐着略显紧张的求职者王铁牛。一场对 Java 核心知识、JUC、JVM 等多方面技术的面试拉开了帷幕。
第一轮面试 面试官:首先,我们来聊聊 Java 核心知识。Java 中基本数据类型有哪些? 王铁牛:Java 基本数据类型有 byte、short、int、long、float、double、char、boolean。 面试官:回答得不错。那你说说 String 类为什么是不可变的? 王铁牛:因为 String 类是用 final 修饰的,它的底层是一个用 final 修饰的字符数组,一旦创建就不能改变。 面试官:很好。那在 Java 里,什么是多态?请举例说明。 王铁牛:多态就是同一个行为具有多个不同表现形式或形态的能力。比如父类有一个方法,子类重写了这个方法,当通过父类引用指向子类对象时,调用这个方法会执行子类重写后的方法。就像动物类有叫的方法,猫类和狗类继承动物类并重写叫的方法,用动物类引用指向猫对象或者狗对象,调用叫的方法会有不同的表现。 面试官:非常棒,你的基础很扎实。
第二轮面试 面试官:接下来我们谈谈 JUC、多线程和线程池。JUC 里的 CountDownLatch 是做什么用的? 王铁牛:CountDownLatch 可以让一个或多个线程等待其他线程完成操作。比如在一个任务中,主线程需要等待其他几个子线程都完成工作后再继续执行,就可以用 CountDownLatch。 面试官:回答正确。那多线程编程中,如何避免死锁? 王铁牛:可以通过按照相同顺序获取锁、设置锁的超时时间等方法来避免死锁。 面试官:不错。那线程池有哪些核心参数? 王铁牛:线程池的核心参数有 corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(线程空闲存活时间)、unit(时间单位)、workQueue(工作队列)、threadFactory(线程工厂)和 handler(拒绝策略)。 面试官:很好,看来你对多线程和线程池这块掌握得很清晰。
第三轮面试 面试官:现在我们说说一些框架和中间件。Spring 中的 IoC 和 AOP 是什么? 王铁牛:IoC 是控制反转,就是把对象的创建和依赖关系的管理交给 Spring 容器。AOP 是面向切面编程,它可以在不修改原有代码的情况下,对程序进行增强,比如实现日志记录、事务管理等。 面试官:解释得挺准确。那 Spring Boot 相比 Spring 有什么优势? 王铁牛:Spring Boot 可以快速搭建项目,它有自动配置的功能,减少了很多配置文件,开发起来更方便。 面试官:那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,它会把参数部分用占位符 ? 代替,能防止 SQL 注入。{} 是字符串替换,会直接把参数值拼接到 SQL 语句中,有 SQL 注入的风险。 面试官:整体表现不错。不过在面试过程中,对于一些问题虽然能说出大概,但在细节阐述上还可以更深入。我们会综合评估你的表现,你先回家等通知吧。
问题答案详解
1. Java 基本数据类型
Java 中有 8 种基本数据类型,可分为 4 类:
- 整数类型:
- byte:占 1 个字节,取值范围 -128 到 127。
- short:占 2 个字节,取值范围 -32768 到 32767。
- int:占 4 个字节,取值范围 -2147483648 到 2147483647。
- long:占 8 个字节,取值范围 -2^63 到 2^63 - 1,定义 long 类型常量时需要在数字后面加 L 或 l。
- 浮点类型:
- float:占 4 个字节,单精度浮点数,定义 float 类型常量时需要在数字后面加 F 或 f。
- double:占 8 个字节,双精度浮点数。
- 字符类型:
- char:占 2 个字节,用于表示单个字符,用单引号括起来,如 'A'。
- 布尔类型:
- boolean:只有两个值,true 和 false,用于逻辑判断。
2. String 类为什么是不可变的
String 类被 final 修饰,意味着它不能被继承。其底层是一个用 final 修饰的字符数组 private final char value[]。当一个 String 对象被创建后,它所包含的字符序列就被固定下来,不能再改变。如果对 String 对象进行拼接等操作,实际上是创建了一个新的 String 对象,原对象并不会改变。这样设计的好处有:
- 安全性:在多线程环境下,不可变对象是线程安全的,因为它的状态不能被改变。
- 缓存哈希码:String 类重写了
hashCode()方法,由于 String 不可变,所以它的哈希码可以被缓存,提高了使用 String 作为键在哈希表中的性能。 - 字符串常量池:可以让多个引用指向同一个字符串常量,节省内存。
3. 多态
多态是面向对象编程的一个重要特性,它允许不同的对象对同一消息做出不同的响应。实现多态有三个必要条件:
- 继承:子类继承父类,形成类的层次结构。
- 重写:子类重写父类的方法,提供自己的实现。
- 父类引用指向子类对象:通过父类的引用调用重写后的方法,根据实际指向的子类对象来决定执行哪个子类的方法。
示例代码如下:
class Animal {
public void call() {
System.out.println("动物叫");
}
}
class Cat extends Animal {
@Override
public void call() {
System.out.println("喵喵喵");
}
}
class Dog extends Animal {
@Override
public void call() {
System.out.println("汪汪汪");
}
}
public class PolymorphismExample {
public static void main(String[] args) {
Animal cat = new Cat();
Animal dog = new Dog();
cat.call();
dog.call();
}
}
4. CountDownLatch 的作用
CountDownLatch 是 JUC 包中的一个同步工具类,它允许一个或多个线程等待其他线程完成操作。它内部维护了一个计数器,在创建 CountDownLatch 对象时需要指定计数器的初始值。当一个线程完成任务后,调用 countDown() 方法将计数器减 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 {
System.out.println(Thread.currentThread().getName() + " 开始工作");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 工作完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}).start();
}
latch.await();
System.out.println("所有线程工作完成,主线程继续执行");
}
}
5. 避免死锁的方法
死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。避免死锁的方法有:
- 按顺序获取锁:所有线程按照相同的顺序获取锁,避免出现循环等待的情况。
- 设置锁的超时时间:如果一个线程在一定时间内无法获取到锁,就放弃并释放已经持有的锁,避免一直等待。
- 使用可重入锁:可重入锁可以避免同一个线程多次获取同一把锁时出现死锁。
- 检测死锁:通过工具或代码检测死锁的发生,并采取相应的措施进行处理。
6. 线程池的核心参数
- corePoolSize:核心线程数,线程池在创建后会创建 corePoolSize 个核心线程,即使这些线程处于空闲状态,也不会被销毁(除非设置了
allowCoreThreadTimeOut为 true)。 - maximumPoolSize:最大线程数,线程池允许创建的最大线程数量。当工作队列满了,并且线程数小于 maximumPoolSize 时,会创建新的线程来处理任务。
- keepAliveTime:线程空闲存活时间,当线程池中的线程数量超过 corePoolSize 时,多余的线程在空闲时间超过 keepAliveTime 后会被销毁。
- unit:时间单位,用于指定 keepAliveTime 的时间单位,如 TimeUnit.SECONDS、TimeUnit.MILLISECONDS 等。
- workQueue:工作队列,用于存储等待执行的任务。常见的工作队列有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。
- threadFactory:线程工厂,用于创建线程。可以通过自定义线程工厂来设置线程的名称、优先级等属性。
- handler:拒绝策略,当工作队列满了,并且线程数达到 maximumPoolSize 时,新提交的任务会触发拒绝策略。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程处理任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。
7. Spring 中的 IoC 和 AOP
- IoC(控制反转):是一种设计思想,将对象的创建和依赖关系的管理从代码中转移到 Spring 容器中。在传统的编程中,对象的创建和依赖关系的维护是由程序员手动完成的,而在 Spring 中,这些工作由 Spring 容器负责。通过 IoC,代码的耦合度降低,提高了代码的可维护性和可测试性。IoC 的实现方式主要有依赖注入(DI),包括构造函数注入、属性注入和接口注入。
- AOP(面向切面编程):是对面向对象编程的一种补充,它允许在不修改原有代码的情况下,对程序进行增强。AOP 的主要概念有切面(Aspect)、连接点(Join Point)、切点(Pointcut)、通知(Advice)和引入(Introduction)。常见的应用场景有日志记录、事务管理、权限验证等。
8. Spring Boot 相比 Spring 的优势
- 快速搭建项目:Spring Boot 提供了很多 Starter 依赖,只需要添加相应的依赖,就可以快速搭建一个项目,减少了很多配置文件和代码。
- 自动配置:Spring Boot 会根据项目中添加的依赖自动进行配置,大大减少了开发人员的配置工作量。例如,添加了 Spring Data JPA 的依赖,Spring Boot 会自动配置数据源、JPA 等。
- 内嵌服务器:Spring Boot 内嵌了 Tomcat、Jetty 等服务器,不需要额外部署服务器,直接运行项目就可以启动服务器。
- 生产就绪特性:Spring Boot 提供了很多生产就绪的特性,如健康检查、指标监控、外部化配置等,方便在生产环境中对应用进行管理和监控。
9. MyBatis 中 #{} 和 ${} 的区别
- #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换成 ? 占位符,然后使用 PreparedStatement 进行预编译,防止 SQL 注入。例如:
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
在执行时,MyBatis 会将 SQL 预编译为 SELECT * FROM users WHERE id = ?,然后将参数值设置到占位符中。
- **{} 时,会直接将 ${} 中的内容替换为参数值。例如:
<select id="getUserByTableName" parameterType="String" resultType="User">
SELECT * FROM ${tableName}
</select>
如果传入的参数值不安全,可能会导致 SQL 注入攻击。因此,在使用时应尽量使用 #{},只有在需要动态指定表名、列名等情况下才使用 ${}。