下载工作台
Java 编程实战

泛型:类型安全的复用

试读上半部分 · 解锁后可读全文

第 13 章 · 泛型:类型安全的复用

本章目标:理解泛型解决的类型安全与代码复用问题;能定义泛型类、泛型接口、泛型方法;掌握类型参数界限extends/super);理解通配符PECS 原则;理解类型擦除及其限制;为 toolkit-demo 实现通用的 Result<T>PageResult<T>Repository<T, ID>;对照 python-devTypeVarGeneric

学时建议:4~5 小时(含 1.5 小时跟练)

前置:完成 ch01~ch12;已学 ch08 集合ch09 Stream


13.1 场景说明:为什么还要学泛型?

ch08 中你写过 List<String>Map<String, Integer>——<> 里就是泛型。但当 toolkit-demo 需要「既能装商品又能装订单的分页结果」「既能查商品又能查用户的仓储接口」时,每个类型各写一遍显然不可维护:

没有泛型:ProductPage / OrderPage / UserPage …… 三份几乎相同的代码
有了泛型:PageResult<T> 一份代码,编译器保证类型安全
不用泛型用泛型
List 裸类型,取出要强转List<Product> 取出即是 Product
强转错误运行时才炸(ClassCastException类型错误编译期拦截
每类一份重复代码一份代码服务所有类型
说明:本章所有路径、域名、密钥均为教学虚构,练习仍在 ~/learn-java/toolkit-demo 中进行。

13.2 泛型类与泛型接口

类型参数写在类名后的 <> 中,惯例用单个大写字母:T(类型)、E(元素)、K/V(键值)、R(结果)。

package demo.generic;

/** 通用二元组 — 演示多类型参数 */
public class Pair<K, V> {

    private final K key;
    private final V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K key() { return key; }
    public V value() { return value; }
}

使用处由调用方决定具体类型,编译器全程检查:

Pair<String, Double> price = new Pair<>("BK-001", 59.9);
Double p = price.value();          // 无需强转
// Integer bad = price.value();   // 编译错误:类型不匹配

泛型接口(衔接 ch06 接口与 ch11 Repository):

package demo.generic;

import java.util.List;
import java.util.Optional;

/** 通用仓储契约 — 任何实体类型都能复用 */
public interface Repository<T, ID> {

    void save(T entity);

    Optional<T> findById(ID id);

    List<T> findAll();

    boolean deleteById(ID id);
}
要点说明
类型参数是占位符编译时用实际类型代入检查
菱形 <>Java 7+ 右侧可省略类型,由左侧推断
裸类型(raw type)Repository 不带参数等于放弃检查,禁止在新代码使用

13.3 泛型方法

方法级泛型:类型参数声明在返回类型之前<> 里,与类是否泛型无关。

package demo.generic;

import java.util.List;

public class ListUtils {

    /** 取列表第一个元素;空列表返回 null(教学简化) */
    public static <T> T first(List<T> list) {
        return list.isEmpty() ? null : list.get(0);
    }

    /** 交换数组两个位置 — 数组也能用泛型方法 */
    public static <T> void swap(T[] arr, int i, int j) {
        T tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

调用时类型由实参推断,通常无需显式指定:

String s = ListUtils.first(List.of("a", "b"));   // T 推断为 String
Integer n = ListUtils.first(List.of(1, 2, 3));   // T 推断为 Integer

13.4 类型参数界限:bounded type

限制类型参数必须满足某个上界,才能在泛型代码里调用上界的方法:

package demo.generic;

import java.util.List;

public class Stats {

    /** T 必须是 Number 的子类,才能调用 doubleValue() */
    public static <T extends Number> double sum(List<T> list) {
        double total = 0;
        for (T item : list) {
            total += item.doubleValue();
        }
        return total;
    }

    /** T 必须可比较,才能找最大值 */
    public static <T extends Comparable<T>> T max(List<T> list) {
        if (list.isEmpty()) {
            throw new IllegalArgumentException("空列表无最大值");
        }
        T best = list.get(0);
        for (T item : list) {
            if (item.compareTo(best) > 0) {
                best = item;
            }
        }
        return best;
    }
}
界限写法含义
<T extends Number>上界:T 是 Number 或其子类(类/接口都用 extends
<T extends Comparable<T>>自限定:T 能与 T 比较,常见于排序场景
<T extends A & B>多上界:同时是 A 的子类且实现 B(类只能一个且在前)

13.5 通配符与 PECS 原则

List<Number>List<Integer> 没有父子关系——List<Integer> 不能赋值给 List<Number>(否则能往 Integer 列表里塞 Double,破坏安全)。需要「某种类型的列表」时用通配符 ?

// 生产者:从列表读出 Number 用 —— 上界通配符
static double total(List<? extends Number> nums) {
    double sum = 0;
    for (Number n : nums) {
        sum += n.doubleValue();
    }
    return sum;
}

// 消费者:往列表写入 Integer —— 下界通配符
static void fillDefaults(List<? super Integer> target) {
    target.add(0);
    target.add(100);
}

PECS 口诀(Effective Java):Producer Extends,Consumer Super。

角色通配符能做的不能做的
生产者(只读出来)? extends T读出为 T写入(null 除外)
消费者(只写进去)? super T写入 T 或其子类读出具体类型(只能当 Object)
回忆 ch09:Stream.map 的签名 map(Function<? super T, ? extends R> mapper) 正是 PECS 的教科书应用——函数消费 T、生产 R。

13.6 类型擦除与限制

泛型只在编译期存在,编译后类型参数被「擦除」为上限(通常是 Object)。这带来一组必须记住的限制:

限制原因替代方案
不能 new T()运行时不知道 T 是什么传入 Supplier<T>Class<T>
不能 T.class / new T[10]同上List<T> 替代泛型数组
不能 instanceof List<String>运行时只剩 Listinstanceof List<?> 再逐个检查元素
静态成员不能用 T静态属于类而非实例静态方法自己声明 <T>
基本类型不能作参数擦除后是 Object用包装类 Integer 而非 int

擦除的实际效果(编译前后对比):

// 源码
class Box<T> { private T value; }

// 编译后(等效)
class Box { private Object value; }

面试高频List<String>List<Integer> 在运行时是同一个类,反射 getClass() 结果相同——这就是擦除。


以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。