每日进步1点点,一年就是365点点。加油 Javaer
系列介绍
欢迎来到"Java面试基础篇"系列!本系列旨在帮助Java开发者系统性地准备面试,每天精选至少5道经典面试题,涵盖Java基础、进阶、框架等各方面知识。坚持学习21天,助你面试通关!
基础面试题:
每日5题Java面试系列基础(1)
每日5题Java面试系列基础(2)
每日5题Java面试系列基础(3)
每日5题Java面试系列基础(4)
每日5题Java面试系列基础(5)
每日5题Java面试系列基础(6)
每日5题Java面试系列基础(7)
一、泛型相关问题
1. 什么是类型擦除?它有什么局限性
类型擦除是Java泛型实现的核心机制,它通过编译时类型检查、运行时擦除类型信息的方式实现了泛型,同时保持了与老版本Java的兼容性。具体来说,编译器会在编译时进行类型检查,然后将泛型类型替换为它们的限定类型(通常是Object),并在必要时插入类型转换。
类型擦除带来的局限性主要有:
- 运行时类型信息丢失:无法在运行时获取泛型的具体类型参数,例如
List<String>和List<Integer>在运行时都是List - 实例化限制:不能直接实例化类型参数,如
new T()是不允许的 - 数组创建限制:不能创建泛型数组,如
new T[10] - 方法重载问题:由于擦除后签名相同,不能仅靠泛型类型参数不同来重载方法
- 原始类型使用:为了兼容性,允许使用原始类型,但这会失去类型安全性
在JDK团队设计泛型时,类型擦除是权衡后的选择。虽然带来了这些限制,但它确保了二进制兼容性,使现有代码能继续运行。在Java 8中,通过类型推断的改进和@SafeVarargs等注解,部分缓解了这些限制。
2. 泛型通配符<?>、<? extends T>和<? super T>有什么区别
这三种通配符体现了Java泛型中PECS原则(Producer-Extends, Consumer-Super)的核心思想:
-
无界通配符
<?>:- 表示完全未知的类型
- 只能调用与类型无关的方法(如
size()、clear()) - 不能添加任何元素(除了null),因为类型未知
- 常用于方法参数,表示方法不依赖具体类型
-
上界通配符
<? extends T>:- 表示T或T的某个子类型
- 适合从集合中读取元素(生产者场景)
- 不能安全地添加元素(除了null),因为实际类型可能是T的任何子类
- 例如:
List<? extends Number>可以包含Number、Integer或Double
-
下界通配符
<? super T>:- 表示T或T的某个超类型
- 适合向集合中添加元素(消费者场景)
- 读取时只能保证得到Object类型
- 例如:
List<? super Integer>可以添加Integer及其子类,但读取时只能保证是Object
在实际设计中,PECS原则指导我们:当数据结构作为生产者(提供数据)时使用extends,作为消费者(接收数据)时使用super。例如,Collections.copy方法的签名就完美体现了这一点:
public static <T> void copy(List<? super T> dest, List<? extends T> src)
3. Java泛型与C#泛型的区别是什么
Java和C#的泛型虽然概念相似,但实现机制和特性有显著差异:
-
实现方式:
- Java使用类型擦除,在运行时没有泛型类型信息
- C#在CLR中直接支持泛型,运行时保留完整类型信息
-
性能:
- C#值类型的泛型避免了装箱拆箱,性能更好
- Java对基本类型仍需装箱,直到Java 8的专门化泛型出现
-
类型约束:
- C#支持更丰富的约束(如
where T : new()) - Java只支持extends约束
- C#支持更丰富的约束(如
-
反射支持:
- C#可以反射获取泛型参数类型
- Java由于类型擦除,运行时无法获取
-
协变/逆变:
- C#通过in/out关键字支持声明点变体
- Java通过通配符在使用点实现变体
从架构角度看,C#的实现更"真实"但需要运行时支持,Java的实现更注重兼容性和迁移路径。Java 10引入的局部变量类型推断(var)和未来可能出现的专门化泛型(Valhalla项目)正在缩小这一差距。
4. 泛型在集合框架中的应用有哪些
泛型在Java集合框架中的应用是它最成功的案例之一:
-
类型安全集合:
- 如
List<String>确保只能包含字符串 - 消除了强制类型转换的需要
- 如
-
集合工具类:
Collections类中的算法如sort、binarySearch都泛型化- 例如:
<T extends Comparable<? super T>> void sort(List<T> list)
-
集合接口设计:
- 核心接口如
Collection<E>,List<E>,Map<K,V>都参数化 - 迭代器
Iterator<E>也相应泛型化
- 核心接口如
-
特殊集合实现:
CheckedCollection等包装类使用泛型进行运行时类型检查EnumSet<E extends Enum<E>>专门优化枚举集合
-
Java 8 Stream API集成:
- Stream操作链充分利用泛型进行类型推断
- 如
<R> Stream<R> map(Function<? super T,? extends R> mapper)
集合框架的泛型设计体现了’契约式设计’思想。例如,List<E>的toArray(T[] a)方法实现非常精妙:它既利用了泛型提供类型安全,又通过运行时检查确保数组类型正确,同时优化了数组重用场景。
二、SPI相关问题
1. SPI与API的区别是什么
SPI(Service Provider Interface)和API虽然相关,但关注点完全不同:
-
控制方向:
- API是提供给调用者的契约,控制权在实现者
- SPI是提供给实现者的契约,控制权在调用者
-
使用场景:
- API用于应用程序直接调用的功能
- SPI用于框架扩展和插件机制
-
依赖关系:
- API通常由调用者依赖提供者
- SPI通常由提供者依赖调用者(反向依赖)
-
变化频率:
- API需要保持高稳定性
- SPI可能随框架演进而变化
架构视角:
从架构层次看,API是水平分层的接口,而SPI是垂直分层的扩展点。好的SPI设计应该遵循’好莱坞原则’:‘不要调用我们,我们会调用你’。
2. 如何实现自己的SPI扩展点
实现自定义SPI扩展点需要以下几个关键步骤:
-
定义服务接口:
public interface MyService { void execute(); } -
创建服务配置文件:
- 在
META-INF/services/目录下创建以接口全限定名命名的文件 - 文件内容为实现类的全限定名
- 在
-
实现服务提供者:
public class MyServiceImpl implements MyService { @Override public void execute() { // 实现逻辑 } } -
使用ServiceLoader加载服务:
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class); for (MyService service : loader) { service.execute(); }
高级实现技巧:
- 支持优先级:可以通过在实现类上添加
@Priority注解或自定义注解实现 - 延迟加载:结合
java.util.function.Supplier实现按需加载 - 依赖注入:在SPI实现中集成DI容器
- 条件加载:通过配置文件或系统属性控制哪些实现被加载
3. Spring的SPI机制与Java标准SPI有何不同
Spring框架的SPI机制在Java标准SPI基础上进行了大量增强:
-
加载机制:
- 标准SPI使用
ServiceLoader,Spring使用SpringFactoriesLoader - Spring的配置文件是
META-INF/spring.factories,支持键值对形式
- 标准SPI使用
-
功能扩展:
- Spring支持
@Conditional等条件化加载 - 支持通过
Environment进行属性驱动 - 自动与ApplicationContext集成
- Spring支持
-
性能优化:
- Spring会缓存加载结果,避免重复解析
- 支持更灵活的类加载策略
-
典型应用:
- 自动配置(
@EnableAutoConfiguration) - 应用上下文初始化器
- 自动配置导入选择器
- 自动配置(
架构意义:
Spring的SPI机制是其’约定优于配置’哲学的核心实现。例如,Spring Boot的自动配置就是通过spring.factories中定义的EnableAutoConfiguration实现类列表驱动的,这种设计实现了高度的可扩展性和模块化。
4. SPI机制的性能如何?有哪些优化方法
标准SPI机制的性能特点及优化方案:
性能瓶颈:
- 类加载开销:每次
ServiceLoader.load()都会重新解析配置文件 - 反射开销:通过反射实例化服务类
- 同步开销:
ServiceLoader内部使用同步块
优化方案:
-
缓存机制:
- 缓存已加载的服务实例
- 如Spring的
SpringFactoriesLoader内部缓存
-
预加载策略:
- 在启动阶段预先加载常用SPI
- 使用后台线程并行加载
-
减少反射:
- 使用MethodHandle替代反射
- 预生成字节码(如GraalVM的native image)
-
选择性加载:
// 使用iterator()的懒加载特性 ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class); Iterator<MyService> iterator = loader.iterator(); if (iterator.hasNext()) { MyService service = iterator.next(); // 使用第一个找到的实现 } -
类加载优化:
- 使用特定ClassLoader加速资源查找
- 避免在热路径中频繁创建ServiceLoader实例
在云原生环境下,可以结合编译时处理(如APT)提前生成服务注册代码,完全避免运行时配置文件解析。例如,Micronaut框架就采用了这种方案来优化启动性能。

被折叠的 条评论
为什么被折叠?



