什么是泛型?为什么要使用它?

#Java#面试八股

什么是泛型?为什么要使用它?

结论:泛型(generics)把"类型"变成参数,让同一个类或方法在编译期获得类型检查与自动类型转换,避免 Object + 强转带来的 ClassCastException。它的三重收益是:类型安全(错误提前到编译期)、消除强制转换(代码更干净)、代码复用(一套逻辑适配多种类型)。Java 的泛型是编译期特性,通过**类型擦除(type erasure)**实现,运行期不存在 List<String> 这个类型。

1. 没有泛型时有多痛

List list = new ArrayList();      // 原始类型,元素都是 Object
list.add("订单A");
list.add(100);

String s = (String) list.get(1);  // 运行期抛 ClassCastException

问题有三:编译期发现不了类型错误、每取一个元素都要强转、API 无法表达"这个集合装的是什么"。泛型把这三件事一次性解决。

维度无泛型有泛型
类型错误发现时机运行期(可能是生产环境)编译期
取元素必须强转自动插入 checkcast,源码无感
类型表达能力注释里写"这里放 String"List<String> 写进签名
代码复用每种类型写一份,或全用 Object一份代码 + 类型参数
可读性差,靠约定好,靠编译器

2. 泛型的三种形态

泛型类:    class Box<T>            { private T value; }
泛型接口:  interface Mapper<T, R>  { R map(T t); }
泛型方法:  static <T> T pick(List<T> list)
通配符:    List<? extends Number>(读)、List<? super Integer>(写)

3. 代码:从原始类型到泛型

import java.util.ArrayList;
import java.util.List;

public class GenericDemo {
    /** 泛型方法 + 上界:T 必须可比较 */
    public static <T extends Comparable<T>> T max(List<T> list) {
        if (list.isEmpty()) throw new IllegalArgumentException("empty");
        T best = list.get(0);
        for (T t : list) {
            if (t.compareTo(best) > 0) best = t;
        }
        return best;
    }

    /** 泛型类 */
    static class Box<T> {
        private T value;
        Box(T value) { this.value = value; }
        T get() { return value; }
    }

    public static void main(String[] args) {
        List<String> names = new ArrayList<>();   // 钻石操作符,类型推断
        names.add("Tom");
        // names.add(1);                           // 编译期报错,而不是运行期
        String first = names.get(0);               // 不需要强转

        Box<Integer> box = new Box<>(42);
        int v = box.get();                         // 自动拆箱

        System.out.println(max(List.of(3, 9, 5)));  // 9
    }
}

4. 擦除:为什么运行期看不到泛型

源码:  List<String> list = new ArrayList<>();
        list.add("a");
        String s = list.get(0);

编译后(擦除后):
        List list = new ArrayList();
        list.add("a");
        String s = (String) list.get(0);   ← 编译器插入 checkcast

泛型参数 T 的上界默认是 Object;
<T extends Number> 擦除后变成 Number。

由此产生四条限制:

限制原因绕法
不能 new T()运行期不知道 T 是什么传入 Class<T> 或 Supplier<T>
不能 new T[]数组需要具体组件类型(T[]) new Object[n] + @SuppressWarnings,或用 List<T>
不能 instanceof List<String>擦除后都是 List用 instanceof List<?> 判断,再逐个检查元素
静态成员不能用 T静态成员属于类,不随实例参数化改成泛型方法

5. 常见追问

追问回答要点
泛型可以用基本类型吗?不能,List<int> 非法,必须用包装类 Integer(装箱开销)
List<Object> 和 List<String> 是父子关系吗?不是,泛型不协变(invariant),这是为了类型安全
? extends 和 ? super 怎么选?PECS 原则:生产者用 extends(只读),消费者用 super(只写)
泛型重载会怎样?f(List<String>) 与 f(List<Integer>) 擦除后签名相同,编译报错
桥接方法是什么?子类重写泛型方法时,编译器生成擦除签名的桥方法做转发(详见擦除专题)
为什么 Arrays.asList 返回的 List 不能 add?返回的是固定大小的内部实现,泛型只保证类型,不保证可变性

小结

泛型的价值是把类型错误从运行期提前到编译期,并让 API 自解释。它是编译期语法糖,底层靠擦除实现,所以有"不能 new T、不能泛型数组、基本类型不行"这些限制。回答时把"为什么用"和"擦除的代价"一起讲,才算完整。

笔记加载中…