少女祈祷中...
NEKO
知识库

java 泛型面试要点

一、类型擦除(Type Erasure)

Q1:什么是 Java 泛型的类型擦除?

答: Java 泛型是编译期概念,编译后泛型信息会被擦除,运行时不存在泛型。具体表现为:

  • 无界类型 T 被擦除为 Object

  • 有界类型 T extends Number 被擦除为 Number

  • 泛型参数会被替换为原始类型(raw type)

List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true,都是 java.util.ArrayList

Q2:类型擦除后,编译器做了哪些补偿?

答: 主要做两件事:

  1. 插入强制类型转换(在取值时)

  2. 生成桥接方法(Bridge Method,用于保证多态)

List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // 编译后实际是 (String) list.get(0)

桥接方法示例:

class Node<T> {
    public void set(T t) {}
}
class MyNode extends Node<String> {
    @Override
    public void set(String s) {} // 编译后会生成一个 set(Object) 桥接方法
}

Q3:为什么 Java 要使用类型擦除?

答: 核心原因是向后兼容。Java 5 引入泛型时,必须保证:

  • 老版本的非泛型代码(如 List)仍能运行

  • 泛型代码和非泛型代码能互相调用

如果采用"具体化泛型"(如 C# 那样运行时保留),字节码格式和老代码都要大改,成本过高。


Q4:类型擦除带来了哪些限制?

答: 常见限制有:

限制

说明

不能 new T()

运行时不知道 T 的具体类型

不能 new T[]

同上,数组需要具体类型

不能 instanceof T

泛型被擦除

不能定义 static 泛型变量

静态成员属于类,与实例泛型无关

不能重载 method(List<String>) 和 method(List<Integer>)

擦除后签名相同,编译报错

基本类型不能作为泛型参数

只能用包装类(List<int> 非法)

// 编译错误:name clash
void method(List<String> list) {}
void method(List<Integer> list) {}

Q5:如何绕过"不能 new T()"的限制?

答: 通过传入 Class<T> 对象,利用反射创建:

public static <T> T create(Class<T> clazz) 
        throws Exception {
    return clazz.getDeclaredConstructor().newInstance();
}

或者使用 Supplier<T>:

public static <T> T create(Supplier<T> supplier) {
    return supplier.get();
}

Q6:类型擦除下,List<String> 和 List<Integer> 能通过反射互相添加元素吗?

答: 可以。因为运行时泛型被擦除,反射绕过了编译期检查:

List<Integer> list = new ArrayList<>();
Method add = list.getClass().getMethod("add", Object.class);
add.invoke(list, "字符串"); // 成功
System.out.println(list); // [字符串]

这也是"泛型只是编译期糖"的经典证明。


二、通配符 extends / super

Q7:<? extends T> 和 <? super T> 分别代表什么?

答:

  • <? extends T>:上界通配符,表示类型是 T 或 T 的子类

  • <? super T>:下界通配符,表示类型是 T 或 T 的父类

List<? extends Number> list1; // 可以是 List<Integer>、List<Double>
List<? super Integer> list2;  // 可以是 List<Integer>、List<Number>、List<Object>

Q8:为什么 List<? extends Number> 不能 add 元素?

答: 因为编译器只知道它是 Number 的某个未知子类,无法确定具体类型。

List<? extends Number> list = new ArrayList<Integer>();
list.add(1);        // 编译错误
list.add(1.0);      // 编译错误
Number n = list.get(0); // 可以读,返回值至少是 Number

如果允许添加,就可能把 Double 放进 List<Integer>,破坏类型安全。

结论:上界通配符只能读,不能写。


Q9:为什么 List<? super Integer> 可以 add,但 get 只能得到 Object?

答:

List<? super Integer> list = new ArrayList<Number>();
list.add(1);        // 可以,Integer 一定是 Integer 或其父类
Object o = list.get(0); // 只能保证是 Object

因为 list 可能是 List<Number> 或 List<Object>,读出来的类型无法确定为 Integer,只能当 Object。

结论:下界通配符可以写,但读只能读成 Object。


Q10:<?> 和 <? extends Object> 有什么区别?

答: 本质上几乎等价,都是无界通配符,但语义上:

  • <?> 表示"任意类型",更简洁

  • <? extends Object> 显式写出上界

两者都只能读成 Object,不能写入(除 null 外)。

List<?> list = new ArrayList<String>();
list.add(null);    // 唯一允许
Object o = list.get(0);

Q11:下面代码为什么编译不通过?

List<? extends Number> list = new ArrayList<Integer>();
list.add(1);

答: 因为 list 的静态类型是 List<? extends Number>,编译器不知道它实际是 List<Integer> 还是 List<Double>。若允许 add(1),就可能往 List<Double> 里放 Integer,破坏类型安全。

这就是"上界通配符不能写"的典型场景。


三、PECS 原则

Q12:什么是 PECS 原则?

答: PECS = Producer Extends, Consumer Super

由 Joshua Bloch 在《Effective Java》中提出,用于指导通配符的选择:

  • 如果参数是生产者(只读取数据),用 <? extends T>

  • 如果参数是消费者(只写入数据),用 <? super T>

  • 如果既读又写,则不要用通配符


Q13:请用代码说明 PECS 的应用

答:

// 生产者:只从 src 读,用 extends
public static <T> void copy(List<? super T> dest, 
                            List<? extends T> src) {
    for (T t : src) {   // src 是生产者
        dest.add(t);    // dest 是消费者
    }
}

调用:

List<Number> dest = new ArrayList<>();
List<Integer> src = List.of(1, 2, 3);
copy(dest, src); // 正确

这是 JDK Collections.copy 的原型。


Q14:为什么 Collections.copy 的第一个参数是 List<? super T>?

答:

public static <T> void copy(List<? super T> dest, 
                            List<? extends T> src)
  • src 只被读取(生产者)→ extends

  • dest 只被写入(消费者)→ super

这样 copy(List<Number>, List<Integer>) 才能合法,否则如果写成 List<T> dest,就无法把 List<Integer> 复制到 List<Number>。


Q15:Collections.max 的签名是什么?为什么这样设计?

答:

public static <T extends Object & Comparable<? super T>> 
T max(Collection<? extends T> coll)
  • Collection<? extends T>:集合是生产者,只读取元素

  • Comparable<? super T>:比较器是消费者,T 可以调用父类的 compareTo

这是 PECS 在 JDK 源码中的经典体现。


Q16:如果既读又写,应该怎么做?

答: 不要用通配符,直接用具体泛型类型:

// 不要这样
public void process(List<?> list) {
    list.add(list.get(0)); // 编译错误
}

// 应该这样
public <T> void process(List<T> list) {
    list.add(list.get(0)); // 正确
}

四、综合与进阶

Q17:List<Object> 和 List<?> 有什么区别?

答:

对比项

List<Object>

List<?>

能否 add

可以 add 任意 Object

只能 add null

能否 get

得到 Object

得到 Object

能否接收 List<String>

不能

可以

语义

明确是 Object 的列表

任意类型的列表

List<Object> l1 = new ArrayList<String>(); // 编译错误
List<?> l2 = new ArrayList<String>();      // 正确

Q18:List<? extends Number> 能赋值给 List<Number> 吗?

答: 不能。

List<? extends Number> a = new ArrayList<Integer>();
List<Number> b = a; // 编译错误

因为 a 可能是 List<Integer>,如果赋给 List<Number>,就能往里面 add Double,破坏类型安全。


Q19:什么是"协变"和"逆变"?Java 数组和泛型有何不同?

答:

  • 协变:Integer[] 是 Number[] 的子类型(Java 数组支持)

  • 逆变:List<Number> 能接收 List<Object>(Java 泛型通过 super 模拟)

  • 不变:List<Integer> 不是 List<Number> 的子类型(Java 泛型默认)

Number[] arr = new Integer[10]; // 协变,编译通过
arr[0] = 1.0; // 运行时 ArrayStoreException

List<Number> list = new ArrayList<Integer>(); // 编译错误,泛型不变

数组的协变是"不安全的",泛型的不变性正是为了修复这个缺陷。


Q20:如何在方法中同时使用 PECS 和类型擦除的知识?

答: 典型例子——写一个通用的合并方法:

public static <T> void merge(List<? super T> dest, 
                             List<? extends T> src) {
    for (T t : src) {
        dest.add(t);
    }
}
  • src 是生产者 → extends

  • dest 是消费者 → super

  • 编译后 T 被擦除为 Object(无界)或上界类型

  • 桥接方法保证多态调用

评论