互联网大厂面试:Java 核心、JUC、JVM 等知识大考验
在互联网大厂的一间安静面试室内,严肃的面试官坐在桌前,对面坐着紧张又期待的求职者王铁牛。面试开始,一场对 Java 多项核心知识的考验拉开帷幕。
第一轮提问 面试官:“我们先从基础的 Java 核心知识开始。你能说说 Java 中多态的实现方式有哪些吗?” 王铁牛:“多态的实现方式主要有两种,一种是方法重载,在同一个类中,方法名相同但参数列表不同;另一种是方法重写,子类重写父类的方法。” 面试官:“回答得不错。那说说 Java 的访问修饰符有哪些,分别有什么作用?” 王铁牛:“Java 有四种访问修饰符,public 表示公共的,任何类都可以访问;protected 表示受保护的,同一个包内的类和不同包的子类可以访问;默认(无修饰符)只能被同一个包内的类访问;private 表示私有的,只有本类可以访问。” 面试官:“很好,看来基础掌握得挺扎实。那在 Java 中,String、StringBuilder 和 StringBuffer 有什么区别?” 王铁牛:“String 是不可变的,每次对 String 进行操作都会生成一个新的 String 对象。StringBuilder 是非线程安全的,效率较高,适合单线程环境下的字符串拼接。StringBuffer 是线程安全的,它的方法都加了同步锁,适合多线程环境下的字符串操作。” 面试官:“非常棒,基础很牢固。”
第二轮提问 面试官:“接下来我们聊聊 JUC 和多线程相关的知识。你能解释一下什么是线程池吗?” 王铁牛:“线程池就是预先创建好一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务,任务执行完后线程不会销毁,而是返回线程池等待下一个任务。这样可以减少线程创建和销毁的开销,提高系统性能。” 面试官:“回答正确。那线程池有哪些核心参数,分别有什么作用?” 王铁牛:“线程池的核心参数有 corePoolSize(核心线程数),表示线程池保持存活的最小线程数量;maximumPoolSize(最大线程数),是线程池允许创建的最大线程数量;keepAliveTime(线程空闲时间),当线程空闲超过这个时间就会被销毁;unit(时间单位),用于指定 keepAliveTime 的时间单位;workQueue(工作队列),用于存储等待执行的任务;threadFactory(线程工厂),用于创建线程;handler(拒绝策略),当线程池和工作队列都满了,新任务提交时的处理策略。” 面试官:“不错。那你说说 JUC 中的 CountDownLatch 是做什么用的,简单举个使用场景。” 王铁牛:“CountDownLatch 是一个同步工具类,它可以让一个或多个线程等待其他线程完成操作。比如在一个大型任务中,有多个子任务需要并行执行,主线程需要等待所有子任务都完成后才能继续执行,这时就可以使用 CountDownLatch。” 面试官:“回答得很清晰,对这部分知识掌握得挺好。”
第三轮提问 面试官:“现在我们来谈谈框架相关的内容。先说说 Spring 的核心特性有哪些?” 王铁牛:“Spring 的核心特性主要有 IoC(控制反转)和 AOP(面向切面编程)。IoC 是将对象的创建和依赖关系的管理交给 Spring 容器,而不是由对象自己创建和管理。AOP 是在不修改原有代码的基础上,对程序进行增强,比如日志记录、事务管理等。” 面试官:“说得没错。那 Spring Boot 相对于 Spring 有什么优势?” 王铁牛:“Spring Boot 简化了 Spring 应用的开发过程,它提供了自动配置的功能,减少了大量的配置文件。而且它内置了服务器,比如 Tomcat,方便快速部署和运行。另外,Spring Boot 还提供了各种 starter 依赖,方便集成各种功能。” 面试官:“很好。那 MyBatis 中 #{} 和 {} 的区别是什么?” **王铁牛**:“呃……这个……好像 #{} 是预编译的,能防止 SQL 注入,{} 就是直接替换吧,具体的我有点记不太清了。” 面试官:“这里需要更准确清晰的回答哦。还有,Dubbo 是做什么的,它的主要组件有哪些?” 王铁牛:“Dubbo 是一个分布式服务框架,用于实现服务的远程调用。组件嘛,好像有服务提供者、服务消费者,其他的我有点模糊了。” 面试官:“看来对框架的一些细节还需要加强。”
面试接近尾声,面试官表情严肃地对王铁牛说:“今天的面试就到这里,你在基础的 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 eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog is eating");
}
}
- Java 的访问修饰符有哪些,分别有什么作用:
- public:公共的,任何类都可以访问被 public 修饰的类、方法、变量等。
- protected:受保护的,同一个包内的类可以访问,不同包的子类也可以访问。
- 默认(无修饰符):只能被同一个包内的类访问。
- private:私有的,只有本类可以访问,其他类无法直接访问。
- String、StringBuilder 和 StringBuffer 有什么区别:
- String:是不可变的,一旦创建,其内容不能被修改。每次对 String 进行操作(如拼接、替换等),都会生成一个新的 String 对象,会产生大量的临时对象,效率较低。例如:
String s = "Hello";
s = s + " World"; // 这里会创建新的 String 对象
- **StringBuilder**:是非线程安全的,内部使用可变的字符数组来存储字符串。适合在单线程环境下进行大量的字符串拼接操作,效率较高。例如:
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" World");
- **StringBuffer**:是线程安全的,它的方法都加了 synchronized 关键字进行同步。适合在多线程环境下进行字符串操作,但由于同步的开销,效率相对 StringBuilder 较低。例如:
StringBuffer sb = new StringBuffer();
sb.append("Hello");
sb.append(" World");
- 什么是线程池: 线程池是一种线程使用模式。线程创建和销毁会消耗系统资源,线程池预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务,任务执行完后线程不会销毁,而是返回线程池等待下一个任务。这样可以减少线程创建和销毁的开销,提高系统性能和响应速度,同时还可以对线程进行统一的管理和监控。
- 线程池有哪些核心参数,分别有什么作用:
- corePoolSize:核心线程数,线程池保持存活的最小线程数量。当有新任务提交时,如果线程池中的线程数量小于 corePoolSize,会创建新的线程来执行任务。
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数量。当工作队列已满,且线程池中的线程数量小于 maximumPoolSize 时,会创建新的线程来处理任务。
- keepAliveTime:线程空闲时间,当线程空闲超过这个时间就会被销毁,但核心线程默认不会销毁,除非设置了 allowCoreThreadTimeOut 为 true。
- unit:时间单位,用于指定 keepAliveTime 的时间单位,如 TimeUnit.SECONDS 等。
- workQueue:工作队列,用于存储等待执行的任务。常见的工作队列有 ArrayBlockingQueue、LinkedBlockingQueue 等。
- threadFactory:线程工厂,用于创建线程。可以自定义线程工厂来设置线程的名称、优先级等属性。
- handler:拒绝策略,当线程池和工作队列都满了,新任务提交时的处理策略。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(让提交任务的线程来执行任务)等。
- JUC 中的 CountDownLatch 是做什么用的,简单举个使用场景: CountDownLatch 是一个同步工具类,它允许一个或多个线程等待其他线程完成操作。它通过一个计数器来实现,计数器的初始值为需要等待的线程数量。每个线程完成任务后,计数器减 1,当计数器的值为 0 时,等待的线程会被唤醒继续执行。 使用场景:比如在一个大型任务中,有多个子任务需要并行执行,主线程需要等待所有子任务都完成后才能继续执行。示例代码如下:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int taskCount = 3;
CountDownLatch latch = new CountDownLatch(taskCount);
for (int i = 0; i < taskCount; 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 tasks are completed");
}
}
- Spring 的核心特性有哪些:
- IoC(控制反转):将对象的创建和依赖关系的管理交给 Spring 容器,而不是由对象自己创建和管理。通过依赖注入(DI)的方式,将对象所依赖的其他对象注入到该对象中。例如,在 Spring 中可以使用 XML 配置或注解的方式来实现依赖注入:
// 接口
public interface UserService {
void sayHello();
}
// 实现类
public class UserServiceImpl implements UserService {
@Override
public void sayHello() {
System.out.println("Hello");
}
}
// 使用注解注入
@Component
public class UserController {
@Autowired
private UserService userService;
public void doSomething() {
userService.sayHello();
}
}
- **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");
}
}
- Spring Boot 相对于 Spring 有什么优势:
- 自动配置:Spring Boot 提供了大量的自动配置类,根据项目中引入的依赖自动进行配置,减少了大量的配置文件。例如,引入 Spring Boot 的 Web 依赖后,它会自动配置嵌入式服务器(如 Tomcat)和 Spring MVC。
- 内置服务器:Spring Boot 内置了服务器,如 Tomcat、Jetty 等,不需要额外部署服务器,方便快速部署和运行。可以直接通过 java -jar 命令启动应用。
- starter 依赖:Spring Boot 提供了各种 starter 依赖,方便集成各种功能。例如,引入 spring-boot-starter-web 就可以快速搭建一个 Web 应用,引入 spring-boot-starter-data-jpa 就可以集成 JPA 进行数据库操作。
- 监控和管理:Spring Boot Actuator 提供了丰富的监控和管理功能,如健康检查、指标监控、日志管理等,方便对应用进行运维和管理。
- MyBatis 中 #{} 和 ${} 的区别是什么:
- #{}:是预编译的,在 SQL 语句中使用 #{} 时,MyBatis 会将其替换为占位符?,然后使用 PreparedStatement 进行参数设置。这样可以防止 SQL 注入攻击,因为参数会被自动进行转义处理。例如:
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
- **${}**:是直接替换,MyBatis 会将 ${} 中的内容直接替换到 SQL 语句中。这种方式可能会导致 SQL 注入攻击,因为参数不会进行转义处理。一般用于动态表名、动态列名等场景。例如:
<select id="getUsersByTable" parameterType="String" resultType="User">
SELECT * FROM ${tableName}
</select>
- Dubbo 是做什么的,它的主要组件有哪些:
Dubbo 是一个分布式服务框架,用于实现服务的远程调用,解决了分布式系统中服务之间的通信和协作问题。它提供了高性能、透明化的远程服务调用方案,支持多种协议和注册中心。
主要组件有:
- 服务提供者(Provider):暴露服务的一方,将自己的服务注册到注册中心。
- 服务消费者(Consumer):调用服务的一方,从注册中心获取服务提供者的地址,然后调用服务。
- 注册中心(Registry):负责服务的注册和发现,服务提供者将服务信息注册到注册中心,服务消费者从注册中心获取服务提供者的地址。常见的注册中心有 Zookeeper、Nacos 等。
- 监控中心(Monitor):统计服务的调用次数、调用时间等信息,方便对服务进行监控和调优。
- 调用协议(Protocol):定义了服务调用的协议,如 Dubbo 协议、HTTP 协议等。
- 集群容错(Cluster):当服务提供者有多个实例时,提供集群容错机制,如负载均衡、失败重试等。