互联网大厂 Java 面试:核心知识、框架与中间件的全方位考验
一位严肃的面试官坐在桌前,对面是略显紧张的求职者王铁牛,一场关于 Java 技术的面试即将拉开帷幕。
第一轮面试开始 面试官:首先问你几个 Java 基础问题。Java 中多态的实现方式有哪些? 王铁牛:多态的实现方式主要有方法重载和方法重写。方法重载是在一个类中,有多个方法名相同,但参数列表不同;方法重写是子类重写父类的方法。 面试官:回答得不错。那说说 HashMap 的底层数据结构是什么? 王铁牛:HashMap 的底层是数组 + 链表 + 红黑树。当链表长度超过 8 且数组长度大于 64 时,链表会转化为红黑树。 面试官:很好。再问你,ArrayList 和 LinkedList 的区别是什么? 王铁牛:ArrayList 底层是动态数组,随机访问快,插入和删除慢;LinkedList 底层是双向链表,插入和删除快,随机访问慢。 面试官:非常棒,基础掌握得很扎实。
第二轮面试开始 面试官:接下来聊聊 JUC 和多线程。线程池有哪些创建方式? 王铁牛:可以通过 Executors 工具类创建,像 newFixedThreadPool、newCachedThreadPool 等,也可以通过 ThreadPoolExecutor 手动创建。 面试官:那 ThreadPoolExecutor 构造函数的参数有哪些,分别代表什么含义? 王铁牛:有 corePoolSize 核心线程数,maximumPoolSize 最大线程数,keepAliveTime 线程空闲存活时间,unit 时间单位,workQueue 任务队列,threadFactory 线程工厂,handler 任务拒绝策略。 面试官:不错。说说 JVM 的内存模型,它分为哪几个部分? 王铁牛:JVM 内存模型分为堆、栈、方法区、本地方法栈、程序计数器。堆是存放对象实例的地方,栈存放局部变量等。 面试官:回答得很清晰,看来对多线程和 JVM 有一定的理解。
第三轮面试开始 面试官:现在来谈谈框架相关的。Spring 的 IOC 和 AOP 是什么,能简单解释一下吗? 王铁牛:IOC 是控制反转,把对象的创建和依赖关系的管理交给 Spring 容器;AOP 是面向切面编程,能在不修改原有代码的基础上增加额外功能。 面试官:那 Spring Boot 是如何实现自动配置的? 王铁牛:呃……这个……好像是通过一些配置文件和注解吧,具体的我有点不太清楚。 面试官:再问你,MyBatis 的一级缓存和二级缓存有什么区别? 王铁牛:一级缓存和二级缓存……好像一个是局部的,一个是全局的,具体细节我记不太准了。 面试官:Dubbo 是做什么的,它的工作原理是什么? 王铁牛:Dubbo 是分布式服务框架,原理嘛……就是服务注册和发现之类的,具体的不太好说。
面试总结:面试官看着王铁牛,严肃地说:“从前面的回答来看,你对 Java 基础、JUC、JVM 等方面的知识掌握得还不错,回答问题比较准确清晰,说明你在这些基础内容上下了不少功夫。然而在框架和中间件的部分,对于一些深入的问题,比如 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");
}
}
- HashMap 的底层数据结构是什么?
- HashMap 的底层是数组 + 链表 + 红黑树。数组被称为哈希桶,每个桶存储一个链表或红黑树的头节点。当调用
put方法插入键值对时,首先根据键的哈希值计算出在数组中的位置。如果该位置为空,直接插入新节点;如果该位置已经有节点,就遍历链表或红黑树,若找到相同的键则更新值,若未找到则插入新节点。当链表长度超过 8 且数组长度大于 64 时,链表会转化为红黑树,以提高查找效率;当红黑树节点数小于 6 时,会退化为链表。
- HashMap 的底层是数组 + 链表 + 红黑树。数组被称为哈希桶,每个桶存储一个链表或红黑树的头节点。当调用
- ArrayList 和 LinkedList 的区别是什么?
- 数据结构:ArrayList 底层是动态数组,数组是连续的内存空间;LinkedList 底层是双向链表,每个节点包含数据、指向前一个节点的引用和指向后一个节点的引用。
- 随机访问:ArrayList 支持高效的随机访问,通过索引可以直接访问元素,时间复杂度为 O(1);LinkedList 随机访问效率低,需要从头或尾开始遍历链表,时间复杂度为 O(n)。
- 插入和删除:ArrayList 在中间或开头插入、删除元素时,需要移动后续元素,时间复杂度为 O(n);LinkedList 在插入和删除元素时,只需要修改相邻节点的引用,时间复杂度为 O(1),但如果要插入或删除指定位置的元素,需要先定位到该位置,时间复杂度为 O(n)。
- 线程池有哪些创建方式?
- 通过 Executors 工具类创建:
newFixedThreadPool:创建一个固定大小的线程池,线程数量固定,当有新任务提交时,如果线程池中有空闲线程则执行任务,否则任务会被放入队列等待。newCachedThreadPool:创建一个可缓存的线程池,线程数量不固定,当有新任务提交时,如果线程池中有空闲线程则执行任务,否则会创建新线程。如果线程空闲时间超过 60 秒,会被回收。newSingleThreadExecutor:创建一个单线程的线程池,只有一个线程执行任务,保证任务按顺序执行。newScheduledThreadPool:创建一个可定时执行任务的线程池。
- 通过 ThreadPoolExecutor 手动创建:可以根据具体需求设置线程池的各种参数,如核心线程数、最大线程数、线程空闲存活时间等。
- 通过 Executors 工具类创建:
- ThreadPoolExecutor 构造函数的参数有哪些,分别代表什么含义?
corePoolSize:核心线程数,线程池初始化时创建的线程数量,当有新任务提交时,会优先使用核心线程执行任务。maximumPoolSize:最大线程数,线程池允许的最大线程数量。当核心线程都在执行任务,且任务队列已满时,会创建新线程,直到达到最大线程数。keepAliveTime:线程空闲存活时间,当线程空闲时间超过该时间,且线程数量大于核心线程数时,多余的线程会被回收。unit:时间单位,如TimeUnit.SECONDS表示秒。workQueue:任务队列,用于存储等待执行的任务。常见的任务队列有ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)、SynchronousQueue(同步队列)等。threadFactory:线程工厂,用于创建线程。可以自定义线程工厂来设置线程的名称、优先级等属性。handler:任务拒绝策略,当线程池的线程数量达到最大线程数,且任务队列已满时,新提交的任务会被拒绝,此时会调用任务拒绝策略来处理。常见的拒绝策略有AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程执行任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。
- JVM 的内存模型,它分为哪几个部分?
- 堆(Heap):是 JVM 中最大的一块内存区域,用于存储对象实例和数组。堆是所有线程共享的,垃圾回收主要针对堆进行。堆可以分为新生代和老年代,新生代又分为 Eden 区、Survivor 区(有两个 Survivor 区,分别为 From 和 To)。
- 栈(Stack):每个线程都有自己的栈,用于存储局部变量、方法调用信息等。栈中的数据是线程私有的,每个方法在执行时会创建一个栈帧,栈帧中包含局部变量表、操作数栈、动态链接、方法出口等信息。当方法执行完毕,栈帧会被弹出。
- 方法区(Method Area):用于存储类的元数据,如类的信息、常量池、静态变量等。方法区是所有线程共享的,在 JDK 1.8 之前,方法区也被称为永久代,JDK 1.8 及以后,使用元空间(Metaspace)来替代永久代。
- 本地方法栈(Native Method Stack):与栈类似,但它是为本地方法(使用 native 关键字修饰的方法)服务的。
- 程序计数器(Program Counter Register):可以看作是当前线程所执行的字节码的行号指示器。每个线程都有自己独立的程序计数器,它是线程私有的。
- Spring 的 IOC 和 AOP 是什么,能简单解释一下吗?
- IOC(控制反转):传统的程序中,对象的创建和依赖关系的管理由程序本身负责,而在 Spring 中,把对象的创建和依赖关系的管理交给 Spring 容器。Spring 容器通过读取配置文件或注解,创建对象并注入依赖。例如,一个
UserService类依赖于UserDao类,在传统方式中,UserService类需要自己创建UserDao对象,而在 Spring 中,UserService类只需要声明对UserDao的依赖,Spring 容器会自动创建UserDao对象并注入到UserService中。 - AOP(面向切面编程):是一种编程范式,它允许在不修改原有代码的基础上,增加额外的功能。AOP 把一些通用的功能(如日志记录、事务管理等)封装成切面,在程序运行时,通过代理机制将切面织入到目标对象的方法中。例如,在方法执行前后添加日志记录,在方法执行过程中进行事务管理等。
- IOC(控制反转):传统的程序中,对象的创建和依赖关系的管理由程序本身负责,而在 Spring 中,把对象的创建和依赖关系的管理交给 Spring 容器。Spring 容器通过读取配置文件或注解,创建对象并注入依赖。例如,一个
- Spring Boot 是如何实现自动配置的?
- Spring Boot 的自动配置是基于条件注解和类路径扫描实现的。当 Spring Boot 应用启动时,会自动扫描类路径下的
META - INF/spring.factories文件,该文件中定义了一系列的自动配置类。每个自动配置类都使用了@Configuration注解,并且通过@Conditional系列注解来判断是否满足自动配置的条件。例如,@ConditionalOnClass表示只有当类路径下存在指定的类时,才会进行自动配置;@ConditionalOnMissingBean表示只有当容器中不存在指定类型的 Bean 时,才会创建该 Bean。Spring Boot 还会根据application.properties或application.yml中的配置信息,对自动配置进行定制。
- Spring Boot 的自动配置是基于条件注解和类路径扫描实现的。当 Spring Boot 应用启动时,会自动扫描类路径下的
- MyBatis 的一级缓存和二级缓存有什么区别?
- 一级缓存:是 SqlSession 级别的缓存,同一个 SqlSession 中执行相同的查询语句时,会先从一级缓存中查找,如果缓存中存在则直接返回结果,否则从数据库中查询,并将结果存入一级缓存。当 SqlSession 关闭或执行了增删改操作时,一级缓存会被清空。一级缓存是默认开启的。
- 二级缓存:是 mapper 级别的缓存,多个 SqlSession 可以共享同一个 mapper 的二级缓存。二级缓存需要手动开启,在 mapper.xml 中添加
<cache/>标签或在 mapper 接口上添加@CacheNamespace注解。当执行增删改操作时,会清空该 mapper 的二级缓存。二级缓存的作用范围更广,可以提高不同 SqlSession 之间的查询效率。
- Dubbo 是做什么的,它的工作原理是什么?
- 作用:Dubbo 是一个高性能的分布式服务框架,用于解决分布式系统中服务之间的调用问题。它提供了服务注册与发现、远程调用、负载均衡、集群容错等功能,使得服务之间可以方便地进行通信和协作。
- 工作原理:
- 服务注册:服务提供者在启动时,将自己提供的服务信息(如服务接口、服务地址等)注册到服务注册中心(如 Zookeeper)。
- 服务发现:服务消费者在启动时,从服务注册中心获取服务提供者的信息,并缓存到本地。
- 远程调用:服务消费者通过代理对象调用服务提供者的服务,代理对象将请求信息封装成网络数据包,通过网络发送到服务提供者。
- 负载均衡:当有多个服务提供者时,Dubbo 会根据负载均衡策略(如随机、轮询等)选择一个服务提供者进行调用。
- 集群容错:当服务调用失败时,Dubbo 会根据集群容错策略(如失败重试、快速失败等)进行处理。