Java泛型:类型参数<T>与通配符?
学习背景:
前段时间师姐分享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再聊。
更多推荐



所有评论(0)