《互联网大厂 Java 面试:核心知识、框架与中间件的全方位考验》
一位年轻的求职者王铁牛,怀揣着进入互联网大厂的梦想,来到这里参加 Java 岗位的面试。严肃的面试官正襟危坐,开始了这场考验之旅。
第一轮提问 面试官:首先问几个 Java 核心知识的问题。Java 中基本数据类型有哪些? 王铁牛:有 byte、short、int、long、float、double、char、boolean 这八种。 面试官:不错,回答得很准确。那 Java 中多态的实现方式有哪些? 王铁牛:主要有继承和接口两种方式。通过子类重写父类的方法或者实现接口的抽象方法来实现多态。 面试官:非常棒。那在 Java 里,静态变量和实例变量的区别是什么? 王铁牛:静态变量属于类,所有实例共享一份;实例变量属于对象,每个对象都有自己的一份。
第二轮提问 面试官:接下来聊聊 JUC、JVM 和多线程。JUC 包下有哪些常用的工具类? 王铁牛:有 CountDownLatch、CyclicBarrier、Semaphore 这些。 面试官:很好。那 JVM 的内存区域是如何划分的? 王铁牛:主要分为方法区、堆、虚拟机栈、本地方法栈和程序计数器。 面试官:看来你基础很扎实。那多线程编程中,如何避免死锁? 王铁牛:呃……这个嘛,我想想……好像是要按顺序获取锁。
第三轮提问 面试官:现在谈谈一些框架和中间件。Spring 的核心特性有哪些? 王铁牛:有 IoC(控制反转)和 AOP(面向切面编程)。 面试官:不错。那 Spring Boot 是如何简化 Spring 开发的? 王铁牛:它有自动配置,能快速搭建项目。 面试官:看来你对基础还是了解的。那 MyBatis 中 #{} 和 ${} 的区别是什么? 王铁牛:这个……我有点模糊了,好像一个是预编译,一个不是。
面试接近尾声,面试官推了推眼镜,说道:“今天的面试就到这里,你的基础部分回答得还不错,对于一些概念有一定的理解。但在一些复杂问题上,回答得不够深入和准确。我们后续会综合评估,你先回家等通知吧。”
问题答案
- Java 中基本数据类型有哪些?
Java 中的基本数据类型分为四大类八种。
- 整数类型:byte(1 字节,范围 -128 到 127)、short(2 字节)、int(4 字节)、long(8 字节)。
- 浮点类型:float(4 字节)、double(8 字节)。
- 字符类型:char(2 字节,用于表示单个字符)。
- 布尔类型:boolean(只有 true 和 false 两个值)。
- 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();
}
}
- **接口**:类实现接口,并实现接口中的抽象方法。通过接口引用指向实现类对象,调用接口方法时会执行实现类的具体逻辑。例如:
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 里,静态变量和实例变量的区别是什么?
- 所属不同:静态变量属于类,它是类的所有实例共享的一份数据;实例变量属于对象,每个对象都有自己独立的一份实例变量。
- 内存分配:静态变量在类加载时就被分配内存,存储在方法区;实例变量在创建对象时分配内存,存储在堆中。
- 访问方式:静态变量可以通过类名直接访问,也可以通过对象访问;实例变量只能通过对象访问。例如:
class Test {
static int staticVar = 10;
int instanceVar = 20;
public static void main(String[] args) {
System.out.println(Test.staticVar); // 通过类名访问静态变量
Test t = new Test();
System.out.println(t.instanceVar); // 通过对象访问实例变量
}
}
- 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 {
// 模拟线程执行任务
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread finished");
latch.countDown();
}).start();
}
latch.await();
System.out.println("All threads have finished");
}
}
- **CyclicBarrier**:让一组线程达到一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行。
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 is waiting at the barrier");
barrier.await();
System.out.println("Thread continues after the barrier");
} catch (Exception 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 acquired a permit");
Thread.sleep(1000);
semaphore.release();
System.out.println("Thread released a permit");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
- JVM 的内存区域是如何划分的?
- 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 JDK 1.7 之前,方法区也被称为永久代;JDK 1.8 及以后,使用元空间替代了永久代。
- 堆:是 Java 虚拟机所管理的内存中最大的一块,几乎所有的对象实例都在这里分配内存。堆是垃圾收集器管理的主要区域,分为新生代和老年代,新生代又可分为 Eden 区和两个 Survivor 区。
- 虚拟机栈:每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,每个栈帧对应一个方法的调用。栈帧中存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 本地方法栈:与虚拟机栈类似,不过它是为虚拟机使用到的本地方法服务的。
- 程序计数器:可以看作是当前线程所执行的字节码的行号指示器。每个线程都有一个独立的程序计数器,它是线程私有的。
- 多线程编程中,如何避免死锁?
- 按顺序获取锁:线程按照相同的顺序获取锁,避免不同线程以不同的顺序获取锁而导致死锁。例如,线程 A 和线程 B 都需要获取锁 L1 和锁 L2,那么它们都按照先获取 L1 再获取 L2 的顺序。
- 限时获取锁:使用
tryLock方法,并设置超时时间。如果在规定时间内没有获取到锁,则放弃并释放已持有的锁。例如:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockAvoidance {
private static Lock lock1 = new ReentrantLock();
private static Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
try {
if (lock1.tryLock()) {
try {
Thread.sleep(100);
if (lock2.tryLock()) {
try {
System.out.println("Thread 1 acquired both locks");
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
if (lock2.tryLock()) {
try {
if (lock1.tryLock()) {
try {
System.out.println("Thread 2 acquired both locks");
} finally {
lock1.unlock();
}
}
} finally {
lock2.unlock();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
- **减少锁的持有时间**:尽量缩短线程持有锁的时间,减少死锁的可能性。
7. Spring 的核心特性有哪些? - IoC(控制反转):也称为依赖注入(DI)。它将对象的创建和依赖关系的管理从代码中转移到 Spring 容器中。通过 Spring 容器来创建和管理对象,对象之间的依赖关系由容器注入。例如:
<bean id="userService" class="com.example.UserService">
<property name="userDao" ref="userDao"/>
</bean>
<bean id="userDao" class="com.example.UserDaoImpl"/>
public class UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
// 业务方法
}
- **AOP(面向切面编程)**:它允许在不修改原有业务逻辑的基础上,对程序进行增强。例如,在方法执行前后添加日志记录、事务管理等功能。通过定义切面、切入点和通知来实现。例如:
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 execution");
}
}
- Spring Boot 是如何简化 Spring 开发的?
- 自动配置:Spring Boot 根据项目中引入的依赖,自动进行配置。例如,当引入 Spring Data JPA 和数据库驱动时,Spring Boot 会自动配置数据源、JPA 实体管理器等。
- 起步依赖:提供了一系列的起步依赖,将常用的依赖组合在一起。例如,
spring-boot-starter-web包含了 Spring MVC、Tomcat 等开发 Web 应用所需的依赖。 - 嵌入式服务器:Spring Boot 内置了 Tomcat、Jetty 等嵌入式服务器,无需单独部署服务器,直接运行 Spring Boot 应用即可。
- 命令行界面(CLI):Spring Boot CLI 允许使用 Groovy 脚本快速开发 Spring 应用,无需创建项目结构和配置文件。
- MyBatis 中 #{} 和 ${} 的区别是什么?
- #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 ? 占位符,然后使用
PreparedStatement进行预编译和参数设置。这样可以防止 SQL 注入攻击。例如:
- #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 ? 占位符,然后使用
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
- **${}**:是字符串替换,MyBatis 在处理 ${} 时,会将 ${} 直接替换为传入的参数值。这种方式可能会导致 SQL 注入攻击,因为参数值直接拼接到 SQL 语句中。例如:
<select id="getUserByTableName" parameterType="String" resultType="User">
SELECT * FROM ${tableName}
</select>
一般情况下,优先使用 #{},只有在需要动态传入表名、列名等情况时才使用 ${},并且要确保传入的参数是安全的。