java 中的泛型

概述

泛型是在编译器工作,编译完成后泛型信息会被擦除(Type Erasure),运行时 JVM 根本不知道你用了什么泛型。泛型主要是能在编译器校验类型是否安全,并且能强制消除转换,并能提高代码的复用率。

泛型的作用

泛型解决了三个核心问题:

  • 类型安全 :编译期就能发现类型错误,而不是等到运行时才爆 ClassCastException
  • 消除强制转换 :不再需要手动 (String) list.get(0),编译器自动插入转换代码
  • 代码复用:一套逻辑适配多种数据类型,避免为每种类型写重复代码
bash 复制代码
// 没有泛型:不安全,需要手动强转
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);  // 如果里面混入了 Integer,运行时直接炸

// 有泛型:编译期就能发现错误
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);  // 无需强转,类型安全
// list.add(123);         // 编译报错!

泛型接口、泛型类、泛型方法、泛型字段的定义

泛型类

在类名后声明类型参数,整个类都可以使用:

bash 复制代码
public class Box<T> {
    private T value;  // 泛型字段

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

// 使用
Box<String> strBox = new Box<>();
strBox.set("hello");
String s = strBox.get();  // 无需强转

// 多类型参数
public class Pair<K, V> {
    private K key;
    private V value;
}

泛型接口

在接口名后声明类型参数:

bash 复制代码
public interface Repository<T> {
    void save(T entity);
    T findById(int id);
}

// 实现方式一:指定具体类型
public class UserRepository implements Repository<User> {
    @Override
    public void save(User entity) { /* ... */ }
    @Override
    public User findById(int id) { /* ... */ }
}

// 实现方式二:延续泛型
public class GenericRepository<T> implements Repository<T> {
    @Override
    public void save(T entity) { /* ... */ }
    @Override
    public T findById(int id) { /* ... */ }
}

泛型方法

类型参数声明在返回值之前,独立于类的泛型参数:

bash 复制代码
public class Util {
    // 静态泛型方法(不能引用类的泛型参数)
    public static <T> T getFirst(List<T> list) {
        return list.get(0);
    }

    // 有界泛型方法
    public static <T extends Number> double sum(T[] numbers) {
        double total = 0;
        for (T num : numbers) {
            total += num.doubleValue();
        }
        return total;
    }
}

// 调用时编译器自动推断类型
Integer first = Util.getFirst(List.of(1, 2, 3));

泛型字段

泛型字段定义在泛型类内部,使用类声明的类型参数:

bash 复制代码
public class Container<T> {
    private T data;           // 泛型字段
    private List<T> items;    // 泛型集合字段

    // ❌ 编译错误:静态成员不能使用类的泛型参数
    // private static T staticData;

    // ✅ 静态方法需要自己声明泛型参数
    public static <U> U getDefault(U defaultValue) {
        return defaultValue;
    }
}

通配符

通配符解决的是泛型灵活性 问题------List<Integer> 不是 List<Number> 的子类(泛型不支持协变),通配符让 API 更灵活。

| 通配符 | 含义 | 能读 | 能写 | 适用场景 | | --- | --- | --- | --- | --- | | <?> | 任意类型 | Object | 只能 null | 只遍历,不关心类型 | | <? extends T> | T 或其子类 | T | 只能 null | 生产者(只读) | | <? super T> | T 或其父类 | Object | T 及其子类 | 消费者(只写) |

PECS 原则

PECS 原则是 Java 泛型通配符使用的核心指导方针,全称是 Producer Extends, Consumer Super,意思是:如果这个集合/容器是"生产者"(只读),用 ? extends T;如果是"消费者"(只写),用 ? super T。

? extends T(上界通配符)

表示"某个 T 或 T 的子类",但具体是哪个子类,编译器不知道。编译器只知道它是 T 的某个子类型,但不知道具体是哪个,写入时无法保证类型安全

bash 复制代码
List<? extends Number> list = new ArrayList<Integer>();

这里 list 可能是 List<Integer>List<Double>List<Float>......编译器无法确定具体是哪个。

? super T(下界通配符)

表示"某个 T 或 T 的父类",但具体是哪个父类,编译器不知道。编译器只知道它是 T 的某个父类,但不知道具体是哪个,可以写入,但是可以读,但是是Object,作用不大。

bash 复制代码
List<? super Integer> list = new ArrayList<Number>();

List<? super Integer> list = new ArrayList();

这里 list 可能是 List、List、List......编译器无法确定具体是哪个。

PECS 原则总结

| 角色 | 含义 | 通配符 | 能做什么 | | --- | --- | --- | --- | | Producer(生产者) | 从中读取数据 | ? extends T | 能 get() ,不能 add() | | Consumer(消费者) | 往里写入数据 | ? super T | 能 add()get() 只能得到 Object |

一句话记忆读用 extends,写用 super。又取又放 ,不用通配符,用具体泛型。

?和T 的区别

  • **T**(类型参数) :定义类/方法时声明的确定类型占位符 。调用时确定具体类型后,方法内所有 T 都代表同一个类型。
  • **?**(通配符) :使用时表示未知类型。编译器不知道具体是什么,只知范围。

类型一致性

T 能确保多个参数类型一致,? 无法保证:

bash 复制代码
// T 保证 dest 和 src 元素类型相同
public <T extends Number> void copy(List<T> dest, List<T> src) { ... }

// ? 无法保证两个 List 类型相同(dest 可能是 List<Integer>,src 可能是 List<Double>)
public void copy(List<? extends Number> dest, List<? extends Number> src) { ... }

读写能力

T 可读可写,? 受 PECS 原则限制:但是T不能使用下界通配符

bash 复制代码
// T 可读可写
public <T extends Number> void process(List<T> list) {
    T item = list.get(0);   // 读 
    list.add(item);         // 写 
}

// ? extends 只能读不能写
public void process(List<? extends Number> list) {
    Number item = list.get(0);  // 读 
    // list.add(new Integer(1)); // 写 编译报错!编译器不知具体是 Integer 还是 Double
}

声明变量

T 可声明局部变量,? 不行:

bash 复制代码
public <T extends Number> void method1(List<T> list) {
    T temp = list.get(0);  // ✅
}

public void method2(List<? extends Number> list) {
    // ? temp = list.get(0);  // ❌ 无法用 ? 声明变量
    Number temp = list.get(0);  // 只能退化为上界类型
}

限定能力

| 能力 | T | ? | | --- | --- | --- | | 上界 extends | ✅ <T extends Number> | ✅ <? extends Number> | | 下界 super | ❌ 不支持 | ✅ <? super Number> | | 多重限定 | ✅ <T extends A & B> | ❌ 不支持 |

PESC(黄金法则)的应用场景

生产者和消费者协同 --- 经典 Collections.copy

这是 PECS 最经典的综合应用。src 是生产者(往外读),dest 是消费者(往里写):

bash 复制代码
public static <T> void copy(List<? super T> dest,    // 消费者:往里写
                            List<? extends T> src) { // 生产者:往外读
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, src.get(i));
    }
}

// 调用:从 Integer 列表复制到 Number 列表
List<Integer> src = List.of(1, 2, 3);
List<Number> dest = new ArrayList<>(Arrays.asList(0, 0, 0));
Collections.copy(dest, src);  // ✅ dest 是 Number,能装下 Integer

相关推荐
s_w.h1 小时前
【 linux 】线程互斥与同步
java·linux·服务器·开发语言
学长毕业设计1 小时前
基于SpringBoot的校园二手物品交易系统(源码+文档+讲解视频)
java·spring boot·后端
观无1 小时前
若依AOP
java
是2的10次方啊2 小时前
日志不是越多越好:开发优化、常见坑与线上排查命令
java
ofoxcoding2 小时前
Claude Fable 5.1 API 接入指南:模型标识、五级定价策略、effort 参数与缓存 TTL 配置详解
java·spring·缓存·ai
zww89491112 小时前
相册印刷项目实战指南:从设计到成品的完整技术流程
java·小程序·eclipse
程序猫.2 小时前
双指针问题
java·数据结构·算法
拾光Ծ2 小时前
【MySQL】复合查询、表的连接
java·数据库·sql·mysql
MetaLite2 小时前
Java通用枚举驱动下拉框-元数据接口与前端契约
java·前端·状态模式