学习背景:

        前段时间师姐分享CRTP(自引用泛型约束)时,发现对泛型中的T和?概念有点模糊,赶紧找来资料学习一下,总结如下:

一、基础前置类

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

二、核心概念定义

2.1 类型参数(命名泛型:<T>)

定义

        在类/方法头部提前声明有名字的类型代号(T/E/K/R),可添加 extends 边界约束;整个类/方法作用域内可重复复用该类型,实现入参、返回值、局部变量统一绑定同一类型。

设计初衷 & 解决的核心问题

        Java 早期无泛型时,集合统一为原始类型,存取元素全靠强制类型转换,极易出现运行时类型转换异常,且无法约束多参数类型一致性。

        类型参数 <T> 的核心目的: 1. 编译期锁定统一类型,彻底规避手动强转、运行时类型报错; 2. 解决「入参、返回值、局部变量需要严格同类型绑定」的场景; 3. 实现类全局泛型统一、方法精准类型返回,支持链式调用、数据读写复用。

核心特征

  • 声明位置:方法返回值前、类名后方 <>

  • 约束规则:只能使用 extends 做上界约束,不支持 super

  • 读写权限:可读可写,无集合读写限制

  • 类型能力:可以作为返回值,精准保留子类类型

  • 复用性:全局可复用,类型统一固定

2.2 通配符(匿名泛型:?)

定义

        仅能写在泛型容器 List/Map 内部的匿名占位符,用于临时限制当前容器的类型范围;无名字、无法复用,仅对当前这一处容器生效。

设计初衷 & 解决的核心问题

        仅靠类型参数 <T> 存在严重短板:类型参数强制单一类型统一,API 死板、兼容性差、简单场景代码冗余,且无法控制集合读写权限。

        通配符 ? 的核心目的: 1. 解决 <T> 无法兼容「多种子类集合、混合类型集合」的问题,提升 API 通用性; 2. 简化只读/只写轻量场景,无需冗余声明 <T>,精简代码; 3. 实现 PECS 读写权限隔离,编译期限制集合读写,保护原数据安全; 4. 解决泛型嵌套、父类容器接收等类型参数难以实现的兼容场景。

核心特征

  • 书写位置:仅限容器尖括号内,不能写在方法/类头部

  • 约束规则:同时支持 ? extends(上界)、? super(下界)

  • 读写权限:存在严格限制,? extends 只读、? super 只写

  • 类型能力:无法返回精确子类,读取数据只能得到父类/Object

  • 复用性:匿名临时生效,无法全局复用类型

三、类型参数 <T> 完整详解

3.1 标准语法示例

泛型方法
// <T extends Animal>:类型参数边界声明,先定义、后使用
public <T extends Animal> T getFirst(List<T> list) {
    return list.get(0);
}
泛型类
// 类全局统一泛型类型 T
class Box<T> {
    private T data;
    public T getData() { return data; }
    public void setData(T data) { this.data = data; }
}

3.2 四大实际开发场景

场景1:入参与返回值类型统一,精准返回子类

需求:传入子类集合,直接返回对应子类对象,无需手动强转

public <T extends Animal> T getFirst(List<T> list) {
    return list.get(0);
}

// 调用测试
List<Dog> dogList = List.of(new Dog());
Dog dog = getFirst(dogList); // 直接拿到Dog类型,无强转
场景2:类全局统一泛型(通用返回体、工具包装类)

需求:全局统一接口返回格式,适配任意业务数据类型

// 统一接口返回包装类(项目通用)
public class Result<T> {
    private Integer code;
    private String msg;
    private T data;

    // 静态泛型方法,快速构建成功返回体
    public static <T> Result<T> success(T data) {
        Result<T> res = new Result<>();
        res.setData(data);
        res.setCode(200);
        res.setMsg("操作成功");
        return res;
    }

    // getter/setter 省略
}

// 使用测试
Result<Dog> result = Result.success(new Dog());
Dog dog = result.getData();
场景3:集合同时读、同时写(拷贝、合并数据)

通配符 ? extends 会禁止写入操作,需要同时读写集合时,必须使用类型参数 T

// 集合数据拷贝,既读源集合,又写目标集合
public <T extends Animal> void copyList(List<T> source, List<T> target) {
    for (T animal : source) {
        target.add(animal);
    }
}
场景4:多参数强制为同一类型

需求:约束多个入参为完全相同类型,避免类型错乱对比

public <T> boolean sameObj(T a, T b) {
    return Objects.equals(a, b);
}

3.3 优缺点总结

优点:类型统一、支持精准链式返回、集合读写自由、可全局复用

缺点:API死板,单次调用仅支持单一子类集合;简单场景使用会造成代码冗余

四、通配符 ? 完整详解

4.1 两种核心标准语法

上界通配符 ? extends X(只读)

含义:接收 X 及其所有子类,只能读、不能写,用于数据安全读取场景

public void printAnimals(List<? extends Animal> list) {
    for (Animal a : list) {
        System.out.println(a);
    }
    // list.add(new Dog()); // 编译报错:禁止写入,保护原集合
}
下界通配符 ? super X(只写)

含义:接收 X 及其所有父类,只能写、读取丢失精准类型,用于数据收集场景

public void addDog(List<? super Dog> container) {
    container.add(new Dog()); // 可正常写入子类对象
}

// 调用支持多种父类容器,兼容性极强
addDog(new ArrayList<Dog>());
addDog(new ArrayList<Animal>());
addDog(new ArrayList<Object>());

4.2 三大实际开发场景(带完整代码)

场景1:仅遍历读取,兼容多种子类集合

类型参数 T 不支持混合子类集合,通配符可完美兼容所有子类、混合集合

public void printAnimals(List<? extends Animal> list) {
    list.forEach(System.out::println);
}

// 全部合法,兼容性拉满
printAnimals(List.of(new Dog()));
printAnimals(List.of(new Cat()));
printAnimals(List.of(new Dog(), new Cat())); // 支持混合子类列表
场景2:简单只读工具,简化代码,无多余泛型声明

仅做读取操作、无需类型复用时,无需声明 T,通配符更简洁

// 冗余写法(没必要使用类型参数)
public <T extends Animal> void print1(List<T> list) {
    list.forEach(System.out::println);
}

// 简洁优雅写法(通配符最优解)
public void print2(List<? extends Animal> list) {
    list.forEach(System.out::println);
}
场景3:编译期数据保护,禁止方法篡改外部集合

通过通配符语法特性,从编译层面禁止修改原集合,避免业务bug

public void safeRead(List<? extends Animal> list) {
    Animal a = list.get(0); // ✅ 正常读取数据
    list.add(new Dog());    // ❌ 编译报错,杜绝误修改原数据
}

4.3 优缺点总结

优点:API兼容性强、代码简洁、可精准控制集合读写权限、支持多子类兼容

缺点:无法复用类型、不能返回精确子类、集合存在读写限制

五、类型参数<T> vs 通配符? 核心对比总表

对比维度

类型参数 <T>

通配符 ?

是否有名称

有自定义代号 T/E/K

匿名,无名称

书写位置

类/方法头部 <> 声明

仅容器内部 List<?>

边界关键字

仅支持 extends

? extends / ? super 都支持

类型复用能力

整个方法/类可反复复用

仅当前容器生效,无法复用

返回精确子类

支持,直接返回T

不支持,只能返回父类/Object

集合读写权限

可读可写,无任何限制

? extends只读;? super只写

集合兼容性

单次调用仅支持单一子类

支持多子类、混合列表传入

核心适用场景

链式Builder、数据拷贝、统一返回类型、通用泛型类

遍历打印、数据导出、只读工具、参数范围限定

七、高频错误写法汇总(避坑必看)

错误1:方法头部声明使用通配符 ?

规则:方法/类头部 <> 只能声明命名类型参数(T/E),禁止使用匿名 ?

// 非法语法,编译报错
public <? extends Animal> Animal test(List<?> list) {}

错误2:容器内部写类型参数约束 T extends X

规则:List<> 内部只能使用已定义的类型/通配符,不能写类型约束声明


// 非法语法,编译报错 
public Animal test(List<T extends Animal> list) {}

错误3:类型参数边界使用 super

规则:命名类型参数的边界仅支持 extends,绝对不支持 super

// 非法语法,编译报错 
public <T super Animal> void test(List<T> list) {}

错误4:混淆编译期类型与运行时对象(链式断链问题)

核心原因:编译期只识别方法返回值声明类型,不看运行时实际对象,导致子类方法丢失

abstract class BaseBuilder {
    // 返回值为父类类型,编译期类型锁定父类
    public BaseBuilder name(String name){return this;}
}
class UserBuilder extends BaseBuilder {
    public User build(){return new User();}
}

// 编译报错:name()返回BaseBuilder,父类无build()方法
new UserBuilder().name("张三").build();

八、一句话总结

1. <T>:类型参数(Java为了通用性、灵活性,连形参类型都设置为变量);用来给参数类型起名时统一类型,传什么类型返回什么类型;读写自由、可复用(整个方法、类,都是这一个类型);

2. ?:通配符(是个占位符,可以匹配任何类型);用来补救<T>的“短板”;匿名临时过滤,兼容多子类(支持多个类型)、限制读写、比<T>更轻量化;

        CRTP(自引用泛型约束)就有点复杂了,我们下个blog再聊。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐