互联网大厂 Java 面试:核心知识、框架与中间件大考验
王铁牛怀揣着对互联网大厂的向往,坐在了面试室里,对面是神情严肃的面试官,一场决定他能否进入大厂的面试就此拉开帷幕。
第一轮面试 面试官:“我们先从 Java 核心知识开始。Java 中多态的实现方式有哪些?” 王铁牛:“这个我知道,Java 多态的实现方式主要有方法重载和方法重写。方法重载是在一个类中,有多个方法名相同,但参数列表不同;方法重写是子类重写父类的方法。” 面试官:“回答得不错。那 JVM 的内存区域是如何划分的?” 王铁牛:“JVM 内存区域主要分为堆、栈、方法区等。堆是存储对象实例的地方,栈主要存储局部变量等,方法区存储类的信息、常量等。” 面试官:“很好。在多线程中,synchronized 关键字的作用是什么?” 王铁牛:“synchronized 关键字可以保证在同一时刻,只有一个线程能访问被它修饰的代码块或方法,起到线程同步的作用。” 面试官:“你的基础很扎实,回答得很准确。”
第二轮面试 面试官:“接下来聊聊 JUC 相关的。说说 CountDownLatch 的使用场景。” 王铁牛:“CountDownLatch 可以让一个或多个线程等待其他线程完成操作后再继续执行。比如在一个多线程任务中,主线程需要等待所有子线程完成任务后再进行汇总操作,就可以用 CountDownLatch。” 面试官:“回答得可以。那线程池的核心参数有哪些?” 王铁牛:“线程池的核心参数有 corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(线程空闲存活时间)、unit(时间单位)、workQueue(任务队列)和 threadFactory(线程工厂)、handler(任务拒绝策略)。” 面试官:“不错。HashMap 在 JDK1.7 和 JDK1.8 中有什么区别?” 王铁牛:“在 JDK1.7 中,HashMap 是数组 + 链表的结构;在 JDK1.8 中,是数组 + 链表 + 红黑树的结构。当链表长度超过 8 且数组长度大于 64 时,链表会转化为红黑树,提高查询效率。” 面试官:“非常好,看来你对这些知识点掌握得很深入。”
第三轮面试 面试官:“现在考察一下框架相关的。Spring 的 IOC 容器的实现原理是什么?” 王铁牛:“呃……IOC 就是控制反转,大概就是把对象的创建和管理交给容器,具体原理嘛……我有点不太说得清。” 面试官:“那 Spring Boot 是如何实现自动配置的?” 王铁牛:“好像是有一些自动配置类,但是具体怎么实现的,我不太记得了。” 面试官:“MyBatis 中如何进行分页查询?” 王铁牛:“我记得可以用 RowBounds 或者在 SQL 里写分页语句,但是具体细节我不太确定。” 面试官:“Dubbo 的负载均衡策略有哪些?” 王铁牛:“这个我就不太了解了,只听说过有几种策略,但具体说不上来。”
面试官总结:“王铁牛,通过这次面试,我能看出你对 Java 的一些基础核心知识掌握得还不错,像 Java 多态、JVM 内存区域划分、多线程的同步机制等,你都能准确回答,说明你有一定的知识储备。在 JUC 和集合相关的问题上,你也能清晰地阐述 CountDownLatch 的使用场景、线程池的核心参数以及 HashMap 在不同 JDK 版本的区别,表现得也比较好。
然而,在框架和中间件部分,你暴露出了明显的不足。对于 Spring 的 IOC 容器实现原理、Spring Boot 的自动配置机制、MyBatis 的分页查询以及 Dubbo 的负载均衡策略等问题,你回答得不够清晰甚至不太了解。这些知识点在实际的项目开发中是非常重要的,特别是在我们这样的互联网大厂,对于框架和中间件的使用和理解要求较高。
综合你的整体表现,我们需要进一步评估你的能力是否符合岗位需求。你先回家等通知吧,后续我们会及时和你联系。”
答案详解
- Java 中多态的实现方式
- 方法重载:在同一个类中,多个方法具有相同的方法名,但参数列表不同(参数的类型、个数、顺序不同)。方法重载是编译时多态,编译器根据调用方法时传递的参数来决定调用哪个重载的方法。例如:
public class OverloadExample {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
- **方法重写**:子类继承父类后,重写父类的方法。方法重写要求方法名、参数列表和返回值类型都相同,访问修饰符不能比父类的更严格。方法重写是运行时多态,在运行时根据对象的实际类型来决定调用哪个类的重写方法。例如:
class Animal {
public void makeSound() {
System.out.println("Animal makes sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
- JVM 的内存区域划分
- 堆(Heap):是 JVM 中最大的一块内存区域,所有对象实例和数组都在堆上分配内存。堆是线程共享的,垃圾回收主要针对堆进行。
- 栈(Stack):包括虚拟机栈和本地方法栈。虚拟机栈为 Java 方法服务,每个方法在执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。本地方法栈为本地方法服务。栈是线程私有的,每个线程都有自己的栈。
- 方法区(Method Area):用于存储类的信息、常量、静态变量、即时编译器编译后的代码等。方法区是线程共享的。在 JDK1.8 之后,方法区被元空间(MetaSpace)取代,元空间使用的是本地内存。
- 程序计数器(Program Counter Register):是一块较小的内存区域,它可以看作是当前线程所执行的字节码的行号指示器。每个线程都有一个独立的程序计数器,是线程私有的。
- synchronized 关键字的作用
- 同步方法:当一个方法被 synchronized 修饰时,同一时刻只有一个线程能访问该方法。例如:
public class SynchronizedExample {
public synchronized void method() {
// 同步代码
}
}
- **同步代码块**:可以指定要同步的对象,同一时刻只有一个线程能访问该对象的同步代码块。例如:
public class SynchronizedExample {
private Object lock = new Object();
public void method() {
synchronized (lock) {
// 同步代码
}
}
}
synchronized 关键字通过获取对象的锁来实现线程同步,保证了线程安全,但会影响性能。 4. CountDownLatch 的使用场景 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);
System.out.println(Thread.currentThread().getName() + " completed");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}).start();
}
// 主线程等待所有子线程完成
latch.await();
System.out.println("All threads completed, main thread continues");
}
}
- 线程池的核心参数
- corePoolSize:核心线程数,线程池创建后,会一直保持的线程数量。当提交的任务数小于 corePoolSize 时,线程池会创建新的线程来执行任务。
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数量。当任务队列满了,且线程数小于 maximumPoolSize 时,线程池会创建新的线程来执行任务。
- keepAliveTime:线程空闲存活时间,当线程空闲时间超过 keepAliveTime 时,多余的线程会被销毁。
- unit:keepAliveTime 的时间单位,如 TimeUnit.SECONDS 等。
- workQueue:任务队列,用于存储提交的任务。常见的任务队列有 ArrayBlockingQueue、LinkedBlockingQueue 等。
- threadFactory:线程工厂,用于创建线程。可以自定义线程工厂来设置线程的名称、优先级等。
- handler:任务拒绝策略,当任务队列满了,且线程数达到 maximumPoolSize 时,新提交的任务会被拒绝。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(让提交任务的线程执行任务)等。
- HashMap 在 JDK1.7 和 JDK1.8 中的区别
- 数据结构:JDK1.7 中,HashMap 是数组 + 链表的结构;JDK1.8 中,是数组 + 链表 + 红黑树的结构。
- 插入方式:JDK1.7 采用头插法,新元素插入到链表头部;JDK1.8 采用尾插法,新元素插入到链表尾部。
- 扩容机制:JDK1.7 在扩容时会重新计算每个元素的 hash 值和位置,并且会出现链表反转的问题;JDK1.8 在扩容时会根据元素的 hash 值和数组长度的关系,将元素分配到原位置或原位置 + 旧数组长度的位置,避免了链表反转。
- 性能:在 JDK1.8 中,当链表长度超过 8 且数组长度大于 64 时,链表会转化为红黑树,提高了查询效率。
- Spring 的 IOC 容器的实现原理
Spring 的 IOC(控制反转)容器的核心是 BeanFactory 和 ApplicationContext。IOC 容器的实现原理主要包括以下步骤:
- 配置文件解析:Spring 会读取配置文件(如 XML 配置文件或 Java 注解配置),解析其中的 Bean 定义信息。
- Bean 定义注册:将解析得到的 Bean 定义信息存储在 BeanDefinitionRegistry 中,每个 Bean 定义包含 Bean 的名称、类名、属性等信息。
- Bean 实例化:当需要获取某个 Bean 时,IOC 容器会根据 Bean 定义信息创建 Bean 实例。如果 Bean 的作用域是单例的,容器会缓存该 Bean 实例,下次获取时直接返回缓存的实例;如果是原型的,每次获取都会创建一个新的实例。
- 依赖注入:在 Bean 实例化过程中,IOC 容器会根据 Bean 定义中的依赖关系,将依赖的 Bean 注入到当前 Bean 中。依赖注入的方式有属性注入、构造函数注入等。
- Spring Boot 的自动配置原理
Spring Boot 的自动配置是基于 Spring 的条件注解和自动配置类实现的。主要步骤如下:
- 启动类注解:Spring Boot 应用的启动类上有 @SpringBootApplication 注解,该注解包含了 @EnableAutoConfiguration 注解,用于开启自动配置功能。
- 自动配置类加载:Spring Boot 会从 classpath 中加载 META - INF/spring.factories 文件,该文件中定义了所有的自动配置类。
- 条件注解筛选:自动配置类中使用了各种条件注解(如 @ConditionalOnClass、@ConditionalOnMissingBean 等),Spring Boot 会根据这些条件注解来判断是否需要加载该自动配置类。例如,@ConditionalOnClass 表示只有当指定的类存在于 classpath 中时,才会加载该自动配置类;@ConditionalOnMissingBean 表示只有当容器中不存在指定的 Bean 时,才会加载该自动配置类。
- 配置生效:如果自动配置类通过了条件注解的筛选,它会向容器中注册相应的 Bean,完成自动配置。
- MyBatis 中进行分页查询的方法
- RowBounds 方式:RowBounds 是 MyBatis 提供的一个简单的分页工具,它是基于内存分页的。例如:
List<Student> students = sqlSession.selectList("com.example.mapper.StudentMapper.selectStudents", null, new RowBounds(0, 10));
这里的 0 表示偏移量,10 表示每页显示的记录数。这种方式会将所有记录查询出来,然后在内存中进行分页,不适合大数据量的分页查询。 - SQL 分页语句:在 SQL 语句中使用分页关键字,如 MySQL 中的 LIMIT 关键字。例如:
<select id="selectStudents" resultType="com.example.entity.Student">
SELECT * FROM student LIMIT #{offset}, #{limit}
</select>
在 Java 代码中传递偏移量和每页显示的记录数:
Map<String, Integer> params = new HashMap<>();
params.put("offset", 0);
params.put("limit", 10);
List<Student> students = sqlSession.selectList("com.example.mapper.StudentMapper.selectStudents", params);
- Dubbo 的负载均衡策略
- RandomLoadBalance:随机负载均衡策略,随机选择一个服务提供者。这种策略简单高效,适用于各个服务提供者性能相近的场景。
- RoundRobinLoadBalance:轮询负载均衡策略,按照顺序依次选择服务提供者。这种策略可以保证每个服务提供者被均匀地调用,但在服务提供者性能差异较大时,可能会导致性能问题。
- LeastActiveLoadBalance:最少活跃调用数负载均衡策略,优先选择活跃调用数最少的服务提供者。活跃调用数表示正在处理的请求数量,这种策略可以让性能较好的服务提供者处理更多的请求。
- ConsistentHashLoadBalance:一致性哈希负载均衡策略,根据请求的某些关键信息(如请求参数)计算哈希值,将请求路由到固定的服务提供者。这种策略可以保证相同的请求总是路由到同一个服务提供者,适用于缓存等场景。