Skip to content

泛型与类型安全 ​

学习目标 ​

  • 定义泛型类、接口和泛型方法,减少强制类型转换。
  • 用 extends、super 和 PECS 表达生产者与消费者边界。
  • 理解类型擦除、原始类型、通配符和泛型数组限制。

核心知识点 ​

专业术语 ​

  • 类型参数:如 T、K、V 的类型变量,在编译期约束输入和输出。
  • 通配符:? 表示未知类型,? extends T 为上界,? super T 为下界。
  • PECS:Producer Extends、Consumer Super,来源用 extends,写入目标用 super。
  • 类型擦除:泛型检查主要在编译期完成,运行时通常只保留原始边界。
  • 堆污染:参数化类型引用指向不符合声明类型对象的状态。

白话解释与边界 ​

List<Integer> 不是 List<Number> 的子类型,因为允许替换会把 Double 放入整数列表。List<? extends Number> 可以安全读成 Number,但不能添加任意数字;List<? super Integer> 可以添加 Integer,但读出只能当 Object。泛型不是运行时标签,不能直接写 instanceof List<String>。

详细集合 API 已拆成可检索页面:List 常用 API、Set 去重与集合运算、Queue 与 Deque、Map 常用 API、集合排序与不可变集合。

常用用法 ​

泛型类:让字段和方法保持同一类型 ​

类型参数由调用方具体化,编译器会检查写入和读取,不需要手动强转。

java
// 语义:类型参数由调用方具体化,编译器会检查写入和读取,不需要手动强转。
final class Box<T> {
    private final T value;

    Box(T value) {
        this.value = value;

    }

    T value() {
        return value;
    }
}

public class GenericClassDemo {
    public static void main(String[] args) {
        Box<Integer> box = new Box<>(42);
        System.out.println(box.value());
        // 输出:42
    }
}

泛型方法:让参数和返回值建立关系 ​

方法级类型参数写在返回类型前;编译器通常可根据实参推断 T。

java
// 语义:方法级类型参数写在返回类型前。
public class GenericMethodDemo {
    static <T> T first(T left, T right) {
        return left;
    }

    public static void main(String[] args) {
        String value = first("java", "api");
        System.out.println(value);
        // 输出:java
    }
}

extends:从生产者读取 ​

extends 允许 List<Integer>、List<Double> 等不同具体列表作为来源;方法只读,不向 source 添加数字。

java
// 语义:extends 允许 List&lt。
import java.util.List;

public class ExtendsDemo {
    static double sum(List<? extends Number> values) {
        double total = 0;

        for (Number value : values) {
            total += value.doubleValue();
            // 输入:total += value.doubleValue();;total 的更新表达式为 value.doubleValue(),按 += 写回。
            // 关键变化:total += value.doubleValue();;读取 value.doubleValue() 的结果并累加到 total,total 状态更新。
        }
        return total;
    }

    public static void main(String[] args) {
        System.out.println(sum(List.of(1, 2, 3)));
        // 输出:6.0
    }
}

super:向消费者写入 ​

super 能安全接收 Integer 写入 List<Integer>、List<Number> 或 List<Object>;读取结果只能按 Object 使用。

java
// 语义:super 能安全接收 Integer 写入 List&lt。
import java.util.ArrayList;
import java.util.List;

public class SuperDemo {
    static void addNumbers(List<? super Integer> target) {
        target.add(1);
        target.add(2);
    }

    public static void main(String[] args) {
        List<Number> numbers = new ArrayList<>();
        addNumbers(numbers);
        System.out.println(numbers);
        // 输出:[1, 2]
    }
}

PECS:复制来源到目标 ​

来源用 extends,目标用 super;这是 Collections.copy、addAll 等集合 API 背后的常见设计。

java
// 语义:来源用 extends,目标用 super。
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class Pecsdemo {
    static <T> void copy(
            Iterable<? extends T> source,
            Collection<? super T> target) {
        for (T value : source) {
            target.add(value);
        }
    }

    public static void main(String[] args) {
        List<Integer> source = List.of(1, 2);
        List<Number> target = new ArrayList<>();
        copy(source, target);
        System.out.println(target);
        // 输出:[1, 2]
    }
}

有界类型参数:限制可用操作 ​

上界让方法安全调用 Comparable;空列表仍是调用方前置条件,应在真实 API 中先校验。

java
// 语义:上界让方法安全调用 Comparable。
import java.util.List;

public class BoundedTypeDemo {
    static <T extends Comparable<? super T>> T max(List<? extends T> values) {
        T result = values.get(0);
        // 关键变化:T result = values.get(0);;读取 values 的索引 0 元素并写入 result;values 内容不变。
        for (T value : values) {
            if (value.compareTo(result) > 0) {
                result = value;

            }
        }
        return result;
    }

    public static void main(String[] args) {
        System.out.println(max(List.of(3, 1, 4)));
        // 输出:4
    }
}

不常用但需要知道 ​

使用原始类型:只在兼容旧 API 时保留 ​

原始类型绕过泛型检查,可能把错误推迟到运行时;新代码应使用参数化类型,并把必要的 suppress 警告限制在最小边界。

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

public class RawTypeDemo {
    public static void main(String[] args) {
        List raw = new ArrayList();
        raw.add("java");
        @SuppressWarnings("unchecked")
        List<String> names = raw;

        System.out.println(names.get(0));
        // 输出:java
    }
}

泛型数组:使用数组构造器或集合替代 ​

不能直接 new T[],因为 T 通常被擦除;需要数组时让调用方提供数组构造器,动态结果优先返回 List<T>。

java
import java.util.Arrays;

public class GenericArrayBoundaryDemo {
    static <T> T[] copy(T[] source, java.util.function.IntFunction<T[]> factory) {
        return Arrays.copyOf(source, source.length, factory);
        // 关键变化:return Arrays.copyOf(source, source.length, factory);;调用 copyOf,实参为 source, source.length, factory;复制 source 内容并按 factory 创建运行时类型;返回新数组。
    }

    public static void main(String[] args) {
        String[] values = copy(new String[]{"a"}, String[]::new);
        System.out.println(values[0]);
        // 输出:a
    }
}

了解类型擦除对运行时判断的影响 ​

运行时只能判断它是 List,不能直接判断 List<String>;元素类型安全主要由编译器在源代码阶段保证。

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

public class ErasureDemo {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        System.out.println(names instanceof List);
        // 输出:true
    }
}

简单案例 ​

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

public class GenericsOverviewDemo {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("java");
        String first = names.get(0);
        // 关键变化:String first = names.get(0);;读取 names 的索引 0 元素并写入 first;names 内容不变。
        System.out.println(first);
        // 输出:java
    }
}

参数化类型让读取结果直接是 String,避免把 Object 手动强转为 String。

易混点 ​

  • List<Integer> 与 List<Number> 不协变;元素有继承关系不等于容器有继承关系。
  • extends 主要读取,super 主要写入;? extends 类型通常不能添加非 null 值。
  • 泛型擦除不代表泛型无用,编译期检查仍是主要收益。
  • 原始类型、未检查强转和不安全 varargs 可能造成堆污染。

课后小问 ​

  1. 为什么 copy 的 source 使用 ? extends T、target 使用 ? super T? 答案:source 是生产者只读出 T,target 是消费者可以接收 T。 解析:这种方向让 List<Integer> 能复制到 Collection<Number>,同时阻止向来源列表写入错误类型。

  2. 为什么不能直接 new T[]? 答案:泛型类型参数通常在运行时被擦除,数组却需要知道实际元素类型。 解析:让调用方提供 IntFunction<T[]> 或改用 List<T>,可以把运行时类型信息放在安全边界上。

本节小结 ​

  • 泛型通过编译期类型关系减少强转,常见 API 使用类型参数和通配符表达方向。
  • PECS 用 extends 表示生产者,用 super 表示消费者。
  • 有界参数可以限制可用操作,原始类型和泛型数组则是兼容边界。
  • 类型擦除限制运行时判断,但不削弱编译期类型检查价值。

快速回顾 ​

  • 能定义泛型类和泛型方法。
  • 能用 PECS 判断 extends 与 super。
  • 能解释泛型不变性和类型擦除。
  • 能说明泛型数组、原始类型和堆污染的风险。