互联网大厂 Java 面试:核心知识、框架与组件大考验
在互联网大厂的一间明亮的面试室内,严肃的面试官坐在桌子的一侧,对面坐着略显紧张的求职者王铁牛。面试开始,一场对 Java 核心知识的考验拉开了帷幕。
第一轮提问 面试官:首先问你几个基础的 Java 核心知识问题。Java 中多态的实现方式有哪些? 王铁牛:多态的实现方式主要有两种,一种是方法重载,在同一个类中,多个方法可以有相同的名字,但参数列表不同;另一种是方法重写,子类可以重写父类的方法。 面试官:回答得不错。那说说 HashMap 的底层数据结构是什么? 王铁牛:HashMap 的底层数据结构是数组 + 链表 + 红黑树。当链表长度超过 8 且数组长度大于 64 时,链表会转换为红黑树,以提高查找效率。 面试官:很好。再问你,ArrayList 是线程安全的吗?如果不是,有什么替代方案? 王铁牛:ArrayList 不是线程安全的。可以使用 Vector 或者使用 Collections.synchronizedList 方法将 ArrayList 包装成线程安全的列表,也可以使用 CopyOnWriteArrayList。 面试官:非常棒,基础掌握得很扎实。
第二轮提问 面试官:接下来聊聊 JUC 和多线程相关的。什么是线程池?线程池有什么作用? 王铁牛:线程池就是预先创建好一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。它的作用主要是减少线程创建和销毁的开销,提高系统的性能和稳定性。 面试官:那你说说线程池的几个重要参数分别代表什么含义? 王铁牛:呃……这个,有核心线程数,还有……最大线程数,其他的我有点记不清了。 面试官:没关系,再问你,JVM 的内存模型是怎样的? 王铁牛:JVM 内存模型嘛,好像有堆、栈,还有……其他的我也说不太清楚了。 面试官:这部分知识还需要加强啊。
第三轮提问 面试官:现在考察一下框架相关的知识。Spring 的核心特性有哪些? 王铁牛:Spring 的核心特性有依赖注入和面向切面编程。依赖注入可以降低代码的耦合度,面向切面编程可以实现一些通用的功能,比如日志记录、事务管理等。 面试官:那 Spring Boot 和 Spring 有什么区别和联系? 王铁牛:这个……Spring Boot 好像是对 Spring 的简化,让开发更方便,但具体的区别我说不太明白。 面试官:再问你,MyBatis 的工作原理是什么? 王铁牛:我只知道 MyBatis 是一个持久层框架,具体的工作原理我不太清楚。 面试官:看来你对框架的理解还不够深入。
面试接近尾声,面试官整理了一下手中的资料,看着王铁牛说:“今天的面试就到这里了,你回去等通知吧。在 Java 核心知识的基础部分你掌握得还可以,对一些简单的概念回答得比较准确,这点值得肯定。但是在 JUC、JVM、框架等方面,你还有很多需要学习和提升的地方。对于线程池的参数、JVM 内存模型、Spring Boot 与 Spring 的区别以及 MyBatis 的工作原理等知识,你回答得不够清晰或者干脆回答不上来。希望你在后续可以加强这些方面的学习,提升自己的技术能力。我们会在之后的时间内综合评估你的表现,再给你答复,祝你好运。”
以下是问题的详细答案:
- Java 中多态的实现方式有哪些?
- 方法重载(Overloading):在同一个类中,多个方法可以有相同的名字,但参数列表(参数的类型、个数、顺序)不同。方法重载是编译时多态,编译器根据调用方法时传递的参数来决定调用哪个重载方法。例如:
public class OverloadingExample {
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");
}
}
- HashMap 的底层数据结构是什么?
- HashMap 的底层数据结构是数组 + 链表 + 红黑树。数组被称为哈希桶,每个桶存储一个链表或红黑树的头节点。当向 HashMap 中插入一个键值对时,首先通过键的哈希值计算出在数组中的索引位置,然后将键值对插入到该位置对应的链表或红黑树中。
- 当链表长度超过 8 且数组长度大于 64 时,链表会转换为红黑树,以提高查找效率。当红黑树的节点数小于 6 时,红黑树会转换回链表。
- ArrayList 是线程安全的吗?如果不是,有什么替代方案?
- ArrayList 不是线程安全的。在多线程环境下,如果多个线程同时对 ArrayList 进行读写操作,可能会导致数据不一致、数组越界等问题。
- 替代方案:
- Vector:Vector 是线程安全的,它的方法都使用了 synchronized 关键字进行同步,保证了线程安全,但性能较低。
- Collections.synchronizedList:可以使用 Collections.synchronizedList 方法将 ArrayList 包装成线程安全的列表。例如:
List<String> list = new ArrayList<>();
List<String> synchronizedList = Collections.synchronizedList(list);
- CopyOnWriteArrayList:CopyOnWriteArrayList 是一个线程安全的列表,它采用写时复制的策略。当进行写操作时,会复制一份新的数组,在新数组上进行操作,操作完成后再将新数组赋值给原数组。读操作不需要加锁,因此读操作的性能较高,但写操作的性能较低,适用于读多写少的场景。
4. 什么是线程池?线程池有什么作用? - 线程池是一种线程使用模式。线程池预先创建好一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。任务执行完后,线程不会销毁,而是返回线程池等待下一个任务。 - 作用: - 减少线程创建和销毁的开销:创建和销毁线程需要消耗系统资源,使用线程池可以避免频繁创建和销毁线程,提高系统的性能。 - 提高系统的响应速度:当有任务提交时,线程池中已经有空闲的线程可以立即执行任务,不需要等待线程的创建。 - 便于线程的管理:线程池可以对线程进行统一的管理,例如设置线程的最大数量、线程的存活时间等。 5. 线程池的几个重要参数分别代表什么含义? - corePoolSize:核心线程数,线程池在初始化时会创建 corePoolSize 个线程,当有任务提交时,首先会使用这些核心线程来执行任务。 - maximumPoolSize:最大线程数,线程池允许创建的最大线程数。当核心线程都在执行任务,且任务队列已满时,线程池会创建新的线程,直到线程数达到 maximumPoolSize。 - keepAliveTime:线程的存活时间,当线程空闲时间超过 keepAliveTime 时,线程会被销毁,直到线程数减少到 corePoolSize。 - unit:keepAliveTime 的时间单位,例如 TimeUnit.SECONDS 表示秒。 - workQueue:任务队列,用于存储提交的任务。当核心线程都在执行任务时,新提交的任务会被放入任务队列中等待执行。常见的任务队列有 ArrayBlockingQueue、LinkedBlockingQueue 等。 - threadFactory:线程工厂,用于创建线程。可以通过自定义线程工厂来设置线程的名称、优先级等属性。 - handler:任务拒绝策略,当线程池的线程数达到 maximumPoolSize 且任务队列已满时,新提交的任务会被拒绝,此时会调用任务拒绝策略来处理被拒绝的任务。常见的任务拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程来执行任务)等。 6. JVM 的内存模型是怎样的? - JVM 内存模型主要包括以下几个部分: - 程序计数器(Program Counter Register):每个线程都有一个独立的程序计数器,它指向当前线程正在执行的字节码指令的地址。如果线程正在执行的是一个 Java 方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地方法,计数器的值为空。 - Java 虚拟机栈(Java Virtual Machine Stacks):每个线程在创建时都会创建一个虚拟机栈,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法在执行时都会创建一个栈帧,栈帧入栈表示方法开始执行,栈帧出栈表示方法执行结束。 - 本地方法栈(Native Method Stacks):与虚拟机栈类似,本地方法栈用于支持本地方法的执行,本地方法是使用非 Java 语言实现的方法。 - Java 堆(Java Heap):Java 堆是 JVM 中最大的一块内存区域,所有的对象实例和数组都在堆上分配内存。堆是线程共享的,是垃圾回收的主要区域。 - 方法区(Method Area):方法区也是线程共享的内存区域,用于存储类的信息、常量、静态变量、即时编译器编译后的代码等数据。在 JDK 1.8 及以后,方法区被元空间(Metaspace)取代,元空间使用本地内存,而不是 JVM 内存。 - 运行时常量池(Runtime Constant Pool):运行时常量池是方法区的一部分,用于存储编译期生成的各种字面量和符号引用,以及在运行时动态生成的常量。 7. Spring 的核心特性有哪些? - 依赖注入(Dependency Injection,DI):依赖注入是 Spring 的核心特性之一,它允许对象之间的依赖关系由外部容器来管理,而不是在对象内部硬编码。通过依赖注入,可以降低代码的耦合度,提高代码的可维护性和可测试性。例如:
public class UserService {
private UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
// 其他方法
}
- 面向切面编程(Aspect-Oriented Programming,AOP):面向切面编程是一种编程范式,它允许在不修改原有代码的情况下,对程序的功能进行增强。Spring AOP 可以实现一些通用的功能,如日志记录、事务管理、安全控制等。例如:
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeMethod() {
System.out.println("Before method execution");
}
}
- Spring Boot 和 Spring 有什么区别和联系?
- 联系:Spring Boot 是基于 Spring 构建的,它继承了 Spring 的核心特性,如依赖注入、面向切面编程等。Spring Boot 可以使用 Spring 的各种模块和功能。
- 区别:
- 配置简化:Spring 需要大量的配置文件(如 XML 配置或 Java 配置类)来配置各种组件和 Bean,而 Spring Boot 采用了约定大于配置的原则,通过自动配置机制,减少了开发者的配置工作量。例如,在 Spring 中配置一个数据源需要编写大量的配置代码,而在 Spring Boot 中,只需要在配置文件中设置几个简单的属性即可。
- 快速开发:Spring Boot 提供了各种 Starter 依赖,开发者只需要添加相应的 Starter 依赖,Spring Boot 会自动配置相关的组件和环境,从而实现快速开发。例如,添加 spring-boot-starter-web 依赖可以快速搭建一个 Web 应用。
- 嵌入式服务器:Spring Boot 内置了嵌入式服务器(如 Tomcat、Jetty 等),可以直接将应用打包成可执行的 JAR 文件,通过 java -jar 命令运行,而不需要额外的服务器环境。
- MyBatis 的工作原理是什么?
- MyBatis 的工作原理主要包括以下几个步骤:
- 读取配置文件:MyBatis 首先会读取配置文件(如 mybatis-config.xml 和映射文件),配置文件中包含了数据库连接信息、映射关系等。
- 创建 SqlSessionFactory:根据配置文件创建 SqlSessionFactory,SqlSessionFactory 是 MyBatis 的核心工厂类,用于创建 SqlSession。
- 创建 SqlSession:通过 SqlSessionFactory 创建 SqlSession,SqlSession 是 MyBatis 与数据库交互的核心对象,它提供了执行 SQL 语句的方法。
- 执行 SQL 语句:通过 SqlSession 执行 SQL 语句,可以是查询语句、插入语句、更新语句或删除语句。MyBatis 会根据映射文件中的配置,将 SQL 语句与 Java 对象进行映射。
- 处理结果集:执行 SQL 语句后,MyBatis 会将查询结果集映射为 Java 对象,或者将 Java 对象的数据插入到数据库中。
- 关闭 SqlSession:操作完成后,需要关闭 SqlSession,释放资源。
- MyBatis 的工作原理主要包括以下几个步骤:
例如,一个简单的 MyBatis 查询示例:
// 读取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 创建 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 创建 SqlSession
try (SqlSession session = sqlSessionFactory.openSession()) {
// 获取 Mapper 接口的代理对象
UserMapper userMapper = session.getMapper(UserMapper.class);
// 执行查询方法
User user = userMapper.selectUserById(1);
System.out.println(user);
}