《互联网大厂 Java 面试:核心知识、JUC、JVM 等全方位大考验》
在互联网大厂宽敞明亮的面试室内,严肃的面试官正襟危坐,对面坐着略显紧张的求职者王铁牛。一场对 Java 核心知识的严峻考验即将拉开帷幕。
第一轮提问
面试官:首先问你几个 Java 基础问题。Java 中多态的实现方式有哪些? 王铁牛:嗯……多态的实现方式有方法重载和方法重写。方法重载是在一个类中,有多个方法名相同但参数列表不同;方法重写是子类重写父类的方法。 面试官:回答得不错。那 String、StringBuilder 和 StringBuffer 有什么区别? 王铁牛:String 是不可变的,每次对 String 进行操作都会创建新的对象。而 StringBuilder 和 StringBuffer 是可变的,StringBuffer 是线程安全的,StringBuilder 是非线程安全的,所以在单线程环境下用 StringBuilder 性能会更好。 面试官:很好,理解得很清晰。那说说 Java 中的访问修饰符有哪些,它们的作用范围是怎样的? 王铁牛:Java 有四种访问修饰符,分别是 private、default(默认,不写修饰符时)、protected 和 public。private 只能在本类中访问;default 可以在本包内访问;protected 除了本包内,不同包的子类也能访问;public 是所有类都能访问。 面试官:非常棒,基础很扎实。
第二轮提问
面试官:接下来考考你 JUC 和多线程的知识。什么是线程池,使用线程池有什么好处? 王铁牛:线程池就是管理线程的一个容器。使用线程池可以减少创建和销毁线程的开销,提高系统的性能,还能控制线程的数量,避免过多线程导致系统资源耗尽。 面试官:回答正确。那线程池有哪些常见的创建方式? 王铁牛:可以通过 Executors 工具类创建,像 newFixedThreadPool 可以创建固定大小的线程池,newCachedThreadPool 可以创建可缓存的线程池,还有 newSingleThreadExecutor 创建单线程的线程池。 面试官:不错。那在多线程环境下,如何保证线程安全,有哪些方法? 王铁牛:可以使用 synchronized 关键字,它可以修饰方法或者代码块,保证同一时间只有一个线程能访问。还可以使用 Lock 接口的实现类,像 ReentrantLock。 面试官:很好,对多线程安全有一定的了解。
第三轮提问
面试官:现在来谈谈框架相关的问题。Spring 的核心特性有哪些? 王铁牛:Spring 的核心特性有依赖注入和面向切面编程。依赖注入就是把对象的创建和依赖关系的管理交给 Spring 容器,面向切面编程可以在不修改原有代码的基础上,增强功能。 面试官:回答得还行。那 Spring Boot 相比 Spring 有什么优势? 王铁牛:Spring Boot 可以快速搭建项目,它有自动配置的功能,减少了很多配置文件的编写,还能方便地集成各种第三方库。 面试官:有一定的理解。那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:这个……好像 #{} 是预编译的,能防止 SQL 注入,{} 就是直接替换,可能会有 SQL 注入风险。不过具体的我有点不太清楚了。 面试官:回答得不太完整。你先回去等通知吧,后续如果有结果会及时联系你。
问题答案详细解析
1. Java 中多态的实现方式有哪些?
多态是 Java 面向对象编程的重要特性之一,它允许不同类的对象对同一消息做出不同的响应。
- 方法重载(Overloading):在同一个类中,有多个方法名相同,但参数列表不同(参数的类型、个数、顺序不同)。方法重载与返回值类型无关。例如:
public class OverloadExample {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
- 方法重写(Overriding):子类重写父类的方法,要求方法名、参数列表和返回值类型都相同。重写的方法不能比被重写的方法有更严格的访问权限。例如:
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
2. String、StringBuilder 和 StringBuffer 有什么区别?
- String:String 类是不可变的,一旦创建,其值不能被修改。每次对 String 进行操作(如拼接、替换等),都会创建一个新的 String 对象,这会导致频繁的内存分配和垃圾回收,影响性能。例如:
String str = "Hello";
str = str + " World"; // 这里会创建新的 String 对象
- StringBuilder:StringBuilder 是可变的,它在内部维护一个可变的字符数组。在进行字符串拼接等操作时,不会创建新的对象,而是直接在原数组上进行修改,因此性能较高。但它是非线程安全的,适用于单线程环境。例如:
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // 直接在原对象上修改
- StringBuffer:StringBuffer 也是可变的,和 StringBuilder 类似,不同的是它是线程安全的。它的方法大多使用了 synchronized 关键字进行同步,保证在多线程环境下操作的安全性,但这也导致了一定的性能开销。例如:
StringBuffer sbf = new StringBuffer("Hello");
sbf.append(" World"); // 线程安全的操作
3. Java 中的访问修饰符有哪些,它们的作用范围是怎样的?
- private:被 private 修饰的成员(字段、方法等)只能在本类中访问。它用于封装类的内部实现细节,防止外部类直接访问和修改。例如:
class PrivateExample {
private int privateField = 10;
private void privateMethod() {
System.out.println("This is a private method");
}
}
- default(默认):如果不写任何访问修饰符,就是默认的访问权限。默认访问权限的成员可以在本包内的其他类中访问,但不同包的类无法访问。例如:
// 假设在 com.example 包下
package com.example;
class DefaultExample {
int defaultField = 20;
void defaultMethod() {
System.out.println("This is a default method");
}
}
- protected:protected 修饰的成员可以在本包内的其他类中访问,同时,不同包的子类也可以访问。它主要用于在继承体系中,让子类可以访问父类的一些特定成员。例如:
// 父类在 com.example 包下
package com.example;
class ProtectedExample {
protected int protectedField = 30;
protected void protectedMethod() {
System.out.println("This is a protected method");
}
}
// 子类在不同包下
package com.another;
import com.example.ProtectedExample;
class SubClass extends ProtectedExample {
public void accessProtected() {
System.out.println(protectedField);
protectedMethod();
}
}
- public:被 public 修饰的成员可以被所有类访问,没有任何访问限制。例如:
public class PublicExample {
public int publicField = 40;
public void publicMethod() {
System.out.println("This is a public method");
}
}
4. 什么是线程池,使用线程池有什么好处?
- 线程池的定义:线程池是一种管理线程的机制,它预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务,任务执行完毕后,线程不会销毁,而是返回到线程池中等待下一个任务。
- 使用线程池的好处:
- 减少线程创建和销毁的开销:创建和销毁线程是比较消耗系统资源的操作,使用线程池可以避免频繁地创建和销毁线程,提高系统的性能。
- 提高响应速度:当有任务提交时,线程池中可能已经有空闲的线程,任务可以立即得到执行,减少了任务的等待时间。
- 控制线程数量:可以通过线程池控制同时运行的线程数量,避免过多线程导致系统资源耗尽,从而提高系统的稳定性。
5. 线程池有哪些常见的创建方式?
在 Java 中,可以通过 Executors 工具类创建不同类型的线程池:
- newFixedThreadPool:创建一个固定大小的线程池,线程池中的线程数量是固定的。当有新任务提交时,如果线程池中有空闲线程,则立即执行任务;如果没有空闲线程,则任务会被放入任务队列中等待。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executor.execute(new Task());
}
executor.shutdown();
}
}
class Task implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}
- newCachedThreadPool:创建一个可缓存的线程池,线程池中的线程数量是可变的。如果有新任务提交,且线程池中有空闲线程,则使用空闲线程执行任务;如果没有空闲线程,则创建新的线程执行任务。当线程空闲时间超过一定时间(默认 60 秒),线程会被销毁。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executor.execute(new Task());
}
executor.shutdown();
}
}
- newSingleThreadExecutor:创建一个单线程的线程池,线程池中只有一个线程。所有任务会按照提交的顺序依次执行。例如:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
executor.execute(new Task());
}
executor.shutdown();
}
}
6. 在多线程环境下,如何保证线程安全,有哪些方法?
- 使用 synchronized 关键字:
- 修饰方法:当一个方法被 synchronized 修饰时,同一时间只有一个线程能访问该方法。如果是实例方法,锁的是当前对象;如果是静态方法,锁的是类对象。例如:
public class SynchronizedMethodExample {
public synchronized void syncMethod() {
// 线程安全的代码
}
public static synchronized void staticSyncMethod() {
// 线程安全的代码
}
}
- **修饰代码块**:可以指定要锁定的对象,更加灵活。例如:
public class SynchronizedBlockExample {
private final Object lock = new Object();
public void syncBlock() {
synchronized (lock) {
// 线程安全的代码
}
}
}
- 使用 Lock 接口的实现类:如 ReentrantLock,它提供了比 synchronized 更灵活的锁机制,例如可以实现公平锁、可中断锁等。例如:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private final Lock lock = new ReentrantLock();
public void lockMethod() {
lock.lock();
try {
// 线程安全的代码
} finally {
lock.unlock();
}
}
}
7. Spring 的核心特性有哪些?
- 依赖注入(Dependency Injection,DI):依赖注入是 Spring 的核心特性之一,它是一种设计模式,将对象的创建和依赖关系的管理交给 Spring 容器。通过依赖注入,对象之间的耦合度降低,提高了代码的可维护性和可测试性。例如:
// 接口
interface MessageService {
String getMessage();
}
// 实现类
class EmailService implements MessageService {
@Override
public String getMessage() {
return "This is an email message";
}
}
// 使用依赖注入的类
class MessagePrinter {
private MessageService messageService;
public MessagePrinter(MessageService messageService) {
this.messageService = messageService;
}
public void printMessage() {
System.out.println(messageService.getMessage());
}
}
- 面向切面编程(Aspect-Oriented Programming,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() + " executed");
}
}
8. Spring Boot 相比 Spring 有什么优势?
- 快速搭建项目:Spring Boot 提供了 Spring Initializr 等工具,可以快速生成项目骨架,包含必要的依赖和配置文件,大大减少了项目搭建的时间。
- 自动配置:Spring Boot 具有自动配置的功能,它会根据项目中引入的依赖自动进行配置,减少了大量的配置文件编写。例如,引入 Spring Data JPA 依赖后,Spring Boot 会自动配置数据源和 JPA 相关的配置。
- 内嵌服务器:Spring Boot 内嵌了 Tomcat、Jetty 等服务器,无需单独部署服务器,直接运行项目即可启动应用。
- 监控和管理:Spring Boot Actuator 提供了丰富的监控和管理功能,如查看应用的健康状态、内存使用情况等。
9. MyBatis 中 #{} 和 ${} 的区别是什么?
- #{}:#{} 是预编译的,它会将参数替换为占位符(?),然后使用 PreparedStatement 进行 SQL 执行。这样可以防止 SQL 注入攻击,因为参数会被自动进行类型转换和转义处理。例如:
<select id="getUserById" parameterType="int" resultType="com.example.User">
SELECT * FROM users WHERE id = #{id}
</select>
- **{} 是直接替换,它会将参数直接插入到 SQL 语句中,不会进行预编译和转义处理。因此,如果参数是用户输入的,可能会导致 SQL 注入攻击。${} 通常用于动态表名、列名等情况。例如:
<select id="getUsersByTable" parameterType="String" resultType="com.example.User">
SELECT * FROM ${tableName}
</select>