楼层: 首页/ 软件技术/ Java 基础/ 泛型:给集合贴个"内容标签"
05

泛型:给集合贴个"内容标签"

Generics · Wildcards · Type Erasure

上面代码里的 List<String>,那个尖括号里的 String 就是泛型。人话:泛型 = 告诉编译器"这个盒子里装的是什么",让编译器帮你把关类型。没有泛型的时代,往 List 里塞了个 Integer,取出来强转成 String,运行时直接炸——泛型就是来治这个病的。

泛型类、泛型方法、通配符 ? extends / ? super

// 泛型类:Box 里装什么类型,由使用时决定
class Box<T> {
    private T value;
    public void set(T v) { this.value = v; }
    public T get() { return value; }
}

public class GenericDemo {
    // 泛型方法:类型参数写在返回值前面
    public static <T> T first(List<T> list) {
        return list.get(0);
    }

    public static void main(String[] args) {
        Box<String> b = new Box<>();
        b.set("hello");
        String s = b.get();   // 编译器知道是 String,不用强转
    }
}

论PECS 原则:? extends 和 ? super 怎么记

这是泛型里最绕的点,背一个口诀:PECS = Producer Extends, Consumer Super(生产者用 extends,消费者用 super)。

? extends T(上界通配):"一切 T 的子类"。你只能往外读(当作 T),不能往里塞(因为不知道具体是哪个子类)。比如方法要"读一组动物"来叫一声,参数写成 List<? extends Animal>,Dog 列表、Cat 列表都能传进来。

? super T(下界通配):"一切 T 的父类"。你只能往里写 T,读出来只能当 Object。典型场景:Collections.copy(dest, src) 的目标列表。

人话记忆:要往外拿数据,用 extends;要往里存数据,用 super。不确定时,先别用通配符,直接写 <T> 泛型方法,更安全。

PECS 完整示例:什么时候用 extends,什么时候用 super

// 类层次:Animal <- Dog <- Puppy
class Animal { String name() { return "动物"; } }
class Dog extends Animal { String name() { return "狗"; } }
class Puppy extends Dog { String name() { return "小狗"; } }

public class PECSDemo {

    // —— 生产者:只往外读,用 ? extends Animal ——
    // 你可以传 List<Dog>、List<Puppy> 进来,读出来都当 Animal 用
    static void printAll(List<? extends Animal> animals) {
        for (Animal a : animals) {        // 读:OK,编译器知道至少是 Animal
            System.out.println(a.name());
        }
        // animals.add(new Dog());      // 写:编译报错!不知道具体是哪个子类
    }

    // —— 消费者:只往里写,用 ? super Dog ——
    // 你可以传 List<Animal>、List<Object> 进来,往里塞 Dog
    static void addDogs(List<? super Dog> dogs) {
        dogs.add(new Dog());          // 写:OK,Dog 一定是 ? super Dog 的子类
        dogs.add(new Puppy());      // 子类也能写,Puppy 也是 Dog 的子类
        // Dog d = dogs.get(0);         // 读:编译报错!读出来只能当 Object
    }

    public static void main(String[] args) {
        List<Dog> dogs = new ArrayList<>();
        printAll(dogs);                   // 读:传 List<Dog> 给 extends Animal 参数

        List<Animal> animals = new ArrayList<>();
        addDogs(animals);                 // 写:传 List<Animal> 给 super Dog 参数
    }
}
类型擦除(Type Erasure)

Java 泛型是"编译器的骗局":编译后,List<String> 和 List<Integer> 在运行时都是同一个 List,泛型信息被擦掉了。由此带来两个坑:① 不能 new T()、不能 new T[10](运行时不知道 T 是谁);② list instanceof List<String> 编译不过,只能 list instanceof List。面试爱问,工作中知道"擦除"俩字就够避开大部分坑。

章末面试 · 泛型(4 题)

1.(概念题)什么是类型擦除?会带来什么限制?

查看答案

答案:Java 泛型只在编译期检查,编译后泛型信息被擦除,List<String> 和 List<Integer> 运行时都是 List。限制:不能 new T()、不能 new T[10]、不能 instanceof List<String>。

2.(代码题)PECS 口诀是什么?List<? extends Animal> 能读能写吗?

查看答案

答案:Producer Extends, Consumer Super。? extends Animal 只能往外读(当 Animal),不能往里写(不知道具体子类)。? super Dog 只能往里写 Dog,读出来只能当 Object。

3.(代码题)下面这段能编译吗?List<String> list = new ArrayList<Integer>();

查看答案

答案:不能。泛型不协变,ArrayList<Integer> 不是 List<String> 的子类。要表达"什么都行的列表"用 List<?>。

4.(思考题)为什么不能 new T()?怎么绕过?

查看答案

答案:类型擦除后运行时不知道 T 是什么,无法 new。绕过:传个 Class<T> 进来,用 clazz.newInstance() 或反射。