Java 泛型与通配符全解析
Java 泛型与通配符全解析
前言:为什么需要泛型?
在 Java 5 之前,集合框架(如ArrayList
、HashMap
)存在一个显著缺陷:无法限制存储的数据类型。例如,一个ArrayList
可以同时存储String
、Integer
、Object
等任意类型的对象,这会导致两个严重问题:
- 编译期类型不安全:开发者可能误将不同类型的对象存入同一集合,编译器无法提前预警。
// Java 5之前的代码ArrayList list = new ArrayList();list.add("Hello"); // 存入Stringlist.add(123); // 存入Integer(无编译错误)list.add(new Object()); // 存入Object(无编译错误)
- 运行期强制转换风险:从集合中取出元素时,必须手动强制转换为目标类型,若类型不匹配,会抛出
ClassCastException
,导致程序崩溃。
// 运行期报错:ClassCastExceptionString str = (String) list.get(1); // 第1个元素是Integer,无法转为String
为解决这些问题,Java 5 引入了泛型(Generics) 机制。泛型的核心思想是:在定义类、接口或方法时,不指定具体的数据类型,而是通过 “类型参数” 延迟到使用时再确定。通过泛型,我们可以实现 “编译期类型检查” 和 “避免强制转换”,让代码更安全、更简洁。
图 1-1:泛型通过 “编译期类型约束” 替代 “运行期强制转换”,减少错误风险
第一章:Java 泛型基础
1.1 泛型的定义与语法
泛型的本质是 “参数化类型”—— 将类型作为参数传递给类、接口或方法。根据使用场景,泛型可分为泛型类、泛型接口和泛型方法三类。
1.1.1 泛型类:类定义时声明类型参数
泛型类的语法格式为:
class 类名<类型参数列表> { ... }
其中,“类型参数列表” 由尖括号<>
包裹,可包含一个或多个类型参数(通常用单个大写字母表示,如T
、E
、K
、V
,遵循约定俗成的命名规范):
-
T
(Type):表示 “类型”,常用在泛型类或方法中; -
E
(Element):表示 “元素”,常用在集合类中; -
K
(Key)/V
(Value):表示 “键”/“值”,常用在映射(如HashMap
)中; -
N
(Number):表示 “数值类型”,常用在数值相关的泛型中。
示例:自定义泛型类Box
// 泛型类:T为类型参数,代表“盒子中存储的对象类型”class Box\<T> {private T content; // 成员变量的类型为T(由外部指定)// 构造方法:参数类型为Tpublic Box(T content) {this.content = content;}// 普通方法:返回值类型为Tpublic T getContent() {return content;}// 普通方法:参数类型为Tpublic void setContent(T content) {this.content = content;}}
使用泛型类:指定具体类型
使用泛型类时,需在类名后通过<>
传入具体类型(如String
、Integer
),此时编译器会自动进行类型检查:
// 1. 创建存储String的Box:类型参数为StringBox\<String> stringBox = new Box<>("Java泛型"); // Java 7+支持“菱形语法”<>,无需重复写StringString str = stringBox.getContent(); // 无需强制转换,直接获取String类型// 2. 创建存储Integer的Box:类型参数为IntegerBox\<Integer> intBox = new Box<>(123);Integer num = intBox.getContent(); // 无需强制转换// 3. 错误示例:类型不匹配时编译报错(编译期安全)stringBox.setContent(456); // 编译错误:456是Integer,无法存入String类型的Box
图 1-2:泛型类通过 “类型参数” 约束成员变量、方法参数和返回值的类型,编译期拦截错误
1.1.2 泛型接口:接口定义时声明类型参数
泛型接口的语法与泛型类类似:
interface 接口名<类型参数列表> { ... }
示例:自定义泛型接口Generator
// 泛型接口:T为“生成的对象类型”interface Generator\<T> {T generate(); // 抽象方法:返回T类型的对象}
实现泛型接口时,有两种选择:
-
实现时指定具体类型:接口的类型参数被固定为某个具体类型;
-
实现类仍为泛型类:接口的类型参数由实现类的类型参数传递。
案例 1:实现时指定具体类型
// 实现Generator接口,指定T为Stringclass StringGenerator implements Generator\<String> {@Overridepublic String generate() {return "随机字符串:" + Math.random();}}// 使用:直接调用,返回String类型StringGenerator sg = new StringGenerator();String result = sg.generate(); // 无需转换
案例 2:实现类仍为泛型类
// 实现类仍为泛型类,T由外部指定class RandomGenerator\<T> implements Generator\<T> {private Class\<T> type; // 存储类型信息public RandomGenerator(Class\<T> type) {this.type = type;}@Overridepublic T generate() {try {// 通过反射创建T类型的实例(假设T有无参构造)return type.newInstance();} catch (Exception e) {throw new RuntimeException(e);}}}// 使用:指定T为IntegerRandomGenerator\<Integer> intGenerator = new RandomGenerator<>(Integer.class);Integer num = intGenerator.generate(); // 返回Integer实例
1.1.3 泛型方法:方法定义时声明类型参数
泛型方法是指在方法声明时独立声明类型参数的方法,它可以存在于普通类、泛型类或泛型接口中。其核心特点是:方法的类型参数与所在类 / 接口的类型参数无关。
泛型方法的语法格式为:
修饰符 <类型参数列表> 返回值类型 方法名(参数列表) { ... }
示例 1:普通类中的泛型方法
class CollectionUtils {// 泛型方法:T为类型参数,用于约束“输入集合”和“返回元素”的类型public static \<T> T findFirstElement(List\<T> list) {if (list == null || list.isEmpty()) {return null;}return list.get(0); // 返回T类型,与集合元素类型一致}}// 使用泛型方法List\<String> strList = Arrays.asList("A", "B", "C");String firstStr = CollectionUtils.findFirstElement(strList); // 返回StringList\<Integer> intList = Arrays.asList(1, 2, 3);Integer firstInt = CollectionUtils.findFirstElement(intList); // 返回Integer
示例 2:泛型类中的泛型方法
泛型类中的泛型方法可以使用类的类型参数,也可以声明独立的类型参数:
class Box\<T> {private T content;// 1. 使用类的类型参数T的方法(非泛型方法)public T getContent() {return content;}// 2. 独立声明类型参数U的泛型方法(与T无关)public \<U> void copyContent(Box\<U> source) {// 将source的内容(U类型)转为String,存入当前Box(T类型,需T支持String赋值)this.content = (T) source.getContent().toString();}}// 使用泛型方法Box\<String> strBox = new Box<>();Box\<Integer> intBox = new Box<>(123);// 调用copyContent:U为Integer,将intBox的内容(123)转为String存入strBoxstrBox.copyContent(intBox);System.out.println(strBox.getContent()); // 输出:123
泛型方法的关键特征:
-
方法的返回值类型前必须有
<类型参数列表>
(如<T>
),否则不是泛型方法; -
泛型方法的类型参数作用域仅限于当前方法;
-
静态方法无法使用所在泛型类的类型参数(因为静态方法属于类,泛型类的类型参数在实例化时才确定),因此静态方法若需泛型支持,必须定义为泛型方法。
1.2 泛型的类型擦除(Type Erasure)
Java 泛型的一个重要特性是类型擦除:编译器在编译时会 “擦除” 泛型的类型参数信息,将泛型代码转换为非泛型代码(即 “原始类型” 代码),以保证与 Java 5 之前的版本兼容。
类型擦除的核心规则:
-
若泛型类型参数有上界(如
<T extends Number>
),则擦除后替换为上界类型; -
若泛型类型参数无上界(如
<T>
),则擦除后替换为Object
类型; -
擦除后,编译器会自动插入桥接方法(Bridge Method)和类型转换代码,确保运行时逻辑正确。
1.2.1 无界泛型的类型擦除
以泛型类Box<T>
为例,擦除前的代码:
class Box\<T> {private T content;public T getContent() { return content; }public void setContent(T content) { this.content = content; }}
编译器擦除类型参数T
(无界),替换为Object
,生成的字节码等价于:
// 类型擦除后的“原始类型”代码(编译器自动生成)class Box {private Object content;public Object getContent() { return content; }public void setContent(Object content) { this.content = content; }}
当使用Box<String>
时,编译器会在 “取值” 时自动插入类型转换:
Box\<String> stringBox = new Box<>("Java");String str = stringBox.getContent(); // 编译后等价于:String str = (String) stringBox.getContent();
1.2.2 有界泛型的类型擦除
若泛型类型参数有上界(如<T extends Number>
),擦除后会替换为上界类型。例如:
class NumberBox\<T extends Number> {private T value;public T getValue() { return value; }public void setValue(T value) { this.value = value; }// 调用T的方法(必须是上界Number中定义的方法)public double getDoubleValue() {return value.doubleValue(); // T擦除后为Number,可直接调用doubleValue()}}
类型擦除后,T
被替换为Number
,等价于:
class NumberBox {private Number value;public Number getValue() { return value; }public void setValue(Number value) { this.value = value; }public double getDoubleValue() {return value.doubleValue();}}
使用NumberBox<Integer>
时,编译器在取值时插入Integer
的类型转换:
NumberBox\<Integer> intBox = new NumberBox<>();intBox.setValue(123); // Integer是Number的子类,无需转换Integer num = intBox.getValue(); // 编译后等价于:Integer num = (Integer) intBox.getValue();
1.2.3 类型擦除的影响与限制
类型擦除是 Java 泛型的 “底层实现机制”,但也带来了一些限制:
-
无法直接使用
new T()
创建实例擦除后
T
变为Object
或上界类型,编译器无法确定T
的具体类型,因此new T()
会编译报错:
class Box\<T> {public Box() {this.content = new T(); // 编译错误:Cannot instantiate the type T}}
解决方案:通过反射传递Class<T>
对象,调用newInstance()
创建实例(如 1.1.2 中的RandomGenerator
)。
-
无法使用
T.class
获取 Class 对象擦除后所有泛型类型的
Class
对象相同,例如Box<String>.class
和Box<Integer>.class
都等于Box.class
:
System.out.println(Box\<String>.class == Box\<Integer>.class); // 编译错误:Cannot select from parameterized typeSystem.out.println(new Box\<String>().getClass() == new Box\<Integer>().getClass()); // 输出:true
-
无法创建泛型数组
泛型数组的创建会破坏类型安全(擦除后数组元素类型变为
Object
,可能存入不同类型),因此编译器禁止直接创建:
Box\<String>\[] boxArray = new Box\<String>\[10]; // 编译错误:Cannot create a generic array of Box\<String>
解决方案:使用List<Box<String>>
替代泛型数组,或创建原始类型数组后强制转换(需谨慎):
// 不推荐:强制转换可能导致运行时错误Box\<String>\[] boxArray = (Box\<String>\[]) new Box\[10];boxArray\[0] = new Box\<String>("A");boxArray\[1] = new Box\<Integer>(123); // 编译通过,运行时无错误(擦除后为Box\[])String content = boxArray\[1].getContent(); // 运行时报错:ClassCastException
-
泛型类型参数不能是基本类型
泛型的类型参数必须是 “引用类型”(如
Integer
),不能是基本类型(如int
),因为擦除后会替换为Object
或上界类型,而基本类型无法直接赋值给Object
:
Box\<int> intBox = new Box<>(123); // 编译错误:Type argument cannot be of primitive typeBox\<Integer> intBox = new Box<>(123); // 正确:使用包装类
1.3 泛型的边界限定(Bounded Type Parameters)
默认情况下,泛型的类型参数可以是任意引用类型(无界),但有时我们需要限制类型参数的范围(如 “只能是Number
的子类”“只能是Comparable
的实现类”),这就需要通过 “边界限定” 实现。
泛型的边界限定分为上界限定和下界限定,其中上界限定用extends
,下界限定用super
(下界限定更多用于通配符,见第二章)。
1.3.1 上界限定:<T extends 类型>
上界限定的语法为<T extends B>
,表示 “类型参数T
必须是B
的子类或B
本身”(B
可以是类或接口)。其核心作用是:
-
限制类型范围:确保
T
具有B
的特性; -
调用上界方法:在泛型代码中可以直接调用
B
中定义的方法(无需强制转换)。
1.3.2 多边界限定:<T extends A & B & C>
除了单边界限定,Java 泛型还支持多边界限定—— 类型参数T必须同时满足 “是类A的子类” 且 “实现接口B和C”。语法规则为:
- 多边界用&分隔,类必须放在最前面(接口顺序可任意);
- T会继承类的属性和方法,同时实现所有接口的抽象方法。
示例:多边界限定的泛型工具类
需求:实现一个工具类,对 “可比较且可序列化的数值类型” 进行排序和序列化操作。
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Comparator;// 多边界限定:T必须是Number的子类(类),且实现Comparable<T>和Serializable接口
class NumberSerializableUtils<T extends Number & Comparable<T> & Serializable> {// 1. 对T类型数组排序(依赖Comparable接口)public void sort(T[] array) {Arrays.sort(array, Comparator.naturalOrder());}// 2. 计算T类型数组的平均值(依赖Number类的doubleValue()方法)public double calculateAverage(T[] array) {if (array == null || array.length == 0) {return 0.0;}double sum = 0.0;for (T num : array) {sum += num.doubleValue(); // 调用Number的方法}return sum / array.length;}// 3. 将T对象序列化为字节数组(依赖Serializable接口)public byte[] serialize(T obj) {try (ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos)) {oos.writeObject(obj);return bos.toByteArray();} catch (Exception e) {throw new RuntimeException("序列化失败", e);}}
}// 测试:使用Integer(满足Number、Comparable<Integer>、Serializable)
public class MultiBoundaryDemo {public static void main(String[] args) {NumberSerializableUtils<Integer> utils = new NumberSerializableUtils<>();// 1. 排序Integer[] numbers = {5, 2, 8, 1, 9};utils.sort(numbers);System.out.println("排序后数组:" + Arrays.toString(numbers)); // 输出:[1, 2, 5, 8, 9]// 2. 计算平均值double average = utils.calculateAverage(numbers);System.out.println("数组平均值:" + average); // 输出:5.0// 3. 序列化byte[] serialized = utils.serialize(123);System.out.println("序列化后字节数:" + serialized.length); // 输出:约81字节(因序列化格式固定)// 错误示例:使用自定义类若不满足边界会编译报错class CustomNumber extends Number implements Comparable<CustomNumber> {// 未实现Serializable接口,不满足多边界@Overridepublic int intValue() { return 0; }@Overridepublic long longValue() { return 0; }@Overridepublic float floatValue() { return 0; }@Overridepublic double doubleValue() { return 0; }@Overridepublic int compareTo(CustomNumber o) { return 0; }}// NumberSerializableUtils<CustomNumber> errorUtils = new NumberSerializableUtils<>();// 编译错误:CustomNumber未实现Serializable,不满足T的边界要求}
}
1.4 泛型与反射的协同:突破类型擦除限制
由于泛型存在 “类型擦除”,运行时无法直接获取泛型的具体类型参数,但通过反射 + 泛型信息保存,可突破这一限制,实现 “泛型类型的动态解析”。这在框架开发(如 Spring、MyBatis)中应用广泛。
1.4.1 泛型类型信息的保存:ParameterizedType 接口
Java 提供java.lang.reflect.ParameterizedType接口,用于表示 “参数化类型”(如List、Map<Integer, User>),通过它可获取泛型的 “原始类型” 和 “实际类型参数”。
核心 API:
- getRawType():获取泛型的原始类型(如List的原始类型是List.class);
- getActualTypeArguments():获取泛型的实际类型参数(如Map<Integer, User>的实际类型参数是[Integer.class, User.class]);
- getOwnerType():获取泛型的所有者类型(如Map.Entry<String, Integer>的所有者类型是Map.class)
1.4.2 案例:泛型 DAO 的动态 SQL 生成
需求:实现一个泛型 DAO(数据访问对象),通过反射解析 “实体类的泛型类型参数”,自动生成 “根据 ID 查询实体” 的 SQL 语句,避免为每个实体单独编写 DAO。
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;// 泛型DAO接口
interface GenericDAO<T, ID> {// 根据ID查询实体T findById(ID id);
}// 泛型DAO实现类(抽象类,通过反射解析泛型类型)
abstract class AbstractGenericDAO<T, ID> implements GenericDAO<T, ID> {// 实体类的Class对象(通过反射解析泛型获取)protected Class<T> entityClass;// ID类型的Class对象(通过反射解析泛型获取)protected Class<ID> idClass;@SuppressWarnings("unchecked")public AbstractGenericDAO() {// 1. 获取当前类的父类(AbstractGenericDAO<T, ID>)的参数化类型Type genericSuperclass = this.getClass().getGenericSuperclass();if (!(genericSuperclass instanceof ParameterizedType)) {throw new RuntimeException("AbstractGenericDAO的子类必须指定泛型类型参数");}ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;// 2. 获取泛型的实际类型参数(第0个是T,第1个是ID)Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();// 解析实体类Class(T)this.entityClass = (Class<T>) actualTypeArguments[0];// 解析ID类Class(ID)this.idClass = (Class<ID>) actualTypeArguments[1];}// 生成“根据ID查询”的SQL(基于实体类名和ID字段名)protected String generateFindByIdSQL() {// 简化逻辑:假设表名=实体类名小写,ID字段名="id"String tableName = entityClass.getSimpleName().toLowerCase();String idColumnName = "id"; // 实际框架中可通过注解(如@Id)指定return String.format("SELECT * FROM %s WHERE %s = ?", tableName, idColumnName);}@Overridepublic T findById(ID id) {String sql = generateFindByIdSQL();System.out.println("执行SQL:" + sql);System.out.println("SQL参数:" + id + "(ID类型:" + idClass.getSimpleName() + ")");// 实际框架中:通过JDBC或ORM执行SQL,返回T类型实体(此处简化返回null)return null;}
}// 实体类1:User
class User {private Long id;private String name;private Integer age;// getter/setterpublic Long getId() { return id; }public void setId(Long id) { this.id = id; }public String getName() { return name; }public void setName(String name) { this.name = name; }public Integer getAge() { return age; }public void setAge(Integer age) { this.age = age; }
}// 实体类2:Order
class Order {private String orderId; // ID类型是Stringprivate Double amount;// getter/setterpublic String getOrderId() { return orderId; }public void setOrderId(String orderId) { this.orderId = orderId; }public Double getAmount() { return amount; }public void setAmount(Double amount) { this.amount = amount; }
}// UserDAO实现(指定泛型T=User,ID=Long)
class UserDAO extends AbstractGenericDAO<User, Long> {}// OrderDAO实现(指定泛型T=Order,ID=String)
class OrderDAO extends AbstractGenericDAO<Order, String> {}// 测试:泛型DAO的动态SQL生成
public class GenericDAODemo {public static void main(String[] args) {// 1. 使用UserDAO查询UserDAO userDAO = new UserDAO();userDAO.findById(1001L);// 输出:// 执行SQL:SELECT * FROM user WHERE id = ?// SQL参数:1001(ID类型:Long)// 2. 使用OrderDAO查询OrderDAO orderDAO = new OrderDAO();orderDAO.findById("ORD20240501001");// 输出:// 执行SQL:SELECT * FROM order WHERE id = ?// SQL参数:ORD20240501001(ID类型:String)}
}
1.5 泛型的高级特性:通配符与泛型方法的协同
在复杂业务场景中,单一通配符或泛型方法可能无法满足需求,需通过 “通配符 + 泛型方法” 的协同,实现 “类型安全的动态适配”。
1.5.1 案例:通用数据转换器
需求:实现一个通用数据转换器,支持 “将源数据列表(任意类型)转换为目标数据列表(任意类型)”,且转换逻辑可自定义(通过 Converter 接口注入)。
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;// 数据转换接口(T:源类型,R:目标类型)
@FunctionalInterface
interface Converter<T, R> {R convert(T source);
}// 通用数据转换工具类(通配符+泛型方法协同)
class DataConverter {/*** 批量转换数据列表* @param sourceList 源数据列表(? extends T:支持T或其子类的列表,生产者)* @param converter 转换逻辑(T→R)* @param <T> 源数据类型* @param <R> 目标数据类型* @return 目标数据列表(List<R>:明确目标类型,消费者)*/public static <T, R> List<R> convertList(List<? extends T> sourceList, Converter<T, R> converter) {List<R> targetList = new ArrayList<>(sourceList.size());for (T source : sourceList) {R target = converter.convert(source);targetList.add(target);}return targetList;}/*** 转换并筛选数据(扩展功能:结合过滤逻辑)* @param sourceList 源数据列表(? extends T)* @param converter 转换逻辑(T→R)* @param filter 过滤逻辑(R→boolean:保留返回true的目标数据)* @param <T> 源数据类型* @param <R> 目标数据类型* @return 筛选后的目标数据列表*/public static <T, R> List<R> convertAndFilter(List<? extends T> sourceList, Converter<T, R> converter, Function<R, Boolean> filter) {List<R> targetList = new ArrayList<>();for (T source : sourceList) {R target = converter.convert(source);if (filter.apply(target)) {targetList.add(target);}}return targetList;}
}// 测试:多场景数据转换
public class DataConverterDemo {public static void main(String[] args) {// 场景1:Integer列表→String列表(转换为“数字+后缀”)List<Integer> intList = List.of(1, 2, 3, 4, 5);List<String> strList = DataConverter.convertList(intList, num -> "编号:" + num);System.out.println("场景1转换结果:" + strList);// 输出:[编号:1, 编号:2, 编号:3, 编号:4, 编号:5]// 场景2:User列表→UserDTO列表(实体→数据传输对象)class User {private Long id;private String name;private Integer age;public User(Long id, String name, Integer age) {this.id = id;this.name = name;this.age = age;}// getterpublic Long getId() { return id; }public String getName() { return name; }public Integer getAge() { return age; }}class UserDTO {private Long userId;private String userName;public UserDTO(Long userId, String userName) {this.userId = userId;this.userName = userName;}@Overridepublic String toString() {return "UserDTO{userId=" + userId + ", userName='" + userName + "'}";}}List<User> userList = List.of(new User(1L, "张三", 20),new User(2L, "李四", 25),new User(3L, "王五", 30));List<UserDTO> userDTOList = DataConverter.convertList(userList, user -> new UserDTO(user.getId(), user.getName()));System.out.println("\n场景2转换结果:" + userDTOList);// 输出:[UserDTO{userId=1, userName='张三'}, UserDTO{userId=2, userName='李四'}, UserDTO{userId=3, userName='王五'}]// 场景3:转换并筛选(保留年龄>22的UserDTO)List<UserDTO> filteredDTOList = DataConverter.convertAndFilter(userList,user -> new UserDTO(user.getId(), user.getName()),dto -> {// 从UserList中关联年龄(实际框架中可通过DTO携带或关联查询)Integer age = userList.stream().filter(u -> u.getId().equals(dto.getUserId())).findFirst().map(User::getAge).orElse(0);return age > 22;});System.out.println("\n场景3筛选结果:" + filteredDTOList);// 输出:[UserDTO{userId=2, userName='李四'}, UserDTO{userId=3, userName='王五'}]}
}
核心价值:
- List<? extends T>允许源列表是 “T 或其子类”(如传入List转换为List),提升灵活性;
- 泛型方法<T, R>明确 “源类型” 和 “目标类型”,结合Converter<T, R>确保转换逻辑的类型安全;
- 扩展方法convertAndFilter通过Function<R, Boolean>实现 “转换 + 筛选” 的一体化,减少代码冗余。
第二章 泛型通配符深度解析与实践
2.1 通配符的本质:类型的 “模糊匹配”
泛型通配符?(英文问号)的本质是 “未知类型的占位符”,用于表示 “任意类型” 或 “某类类型的
2.2 通配符的本质:类型的 “模糊匹配”
泛型通配符?(英文问号)并非 “任意类型的代名词”,其核心本质是对泛型类型的 “模糊匹配规则”—— 它不直接指定具体类型,而是通过 “范围约束”(如 “某类类型的父类”“某类类型的子类”)或 “无约束”,实现对 “一组相关泛型类型” 的统一适配,解决 “泛型类型必须完全一致才能兼容” 的刚性问题。
2.2.1 为什么需要 “模糊匹配”?—— 泛型的 “类型刚性” 痛点
在理解通配符前,需先明确泛型的 “类型刚性” 特性:泛型类型不具备多态性,即List不是List的子类,即使String是Object的子类。这种刚性会导致 “相似类型的泛型对象无法统一处理” 的痛点。
痛点案例:无法统一处理 “不同元素类型的 List”
// 需求:定义一个方法,打印任意元素类型的List
public static void printList(List<Object> list) {for (Object obj : list) {System.out.println(obj);}
}// 测试:调用方法时编译报错
List<String> strList = Arrays.asList("A", "B", "C");
List<Integer> intList = Arrays.asList(1, 2, 3);
// printList(strList); // 错误:List<String>无法转为List<Object>
// printList(intList); // 错误:List<Integer>无法转为List<Object>
原因:泛型的 “类型擦除” 后,List和List的原始类型都是List,但编译器在编译期会强制检查 “泛型类型完全一致”,避免出现 “向List中添加String,后续却按Integer读取” 的类型安全问题。
解决方案:通过通配符?实现 “模糊匹配”,让方法支持 “任意元素类型的 List”:
// 通配符实现模糊匹配:支持任意元素类型的List
public static void printList(List<?> list) {for (Object obj : list) {System.out.println(obj);}
}// 测试:成功调用
printList(strList); // 正确:List<String>匹配List<?>
printList(intList); // 正确:List<Integer>匹配List<?>
2.2.2 通配符 “模糊匹配” 的核心逻辑:匹配 “类型范围” 而非 “具体类型”
通配符的 “模糊匹配” 并非无规则,而是通过 “无界”“上界”“下界” 三种约束,定义 “可匹配的类型范围”,具体逻辑如下:
通配符形式 | 匹配规则(模糊范围) | 本质含义 | 示例匹配场景 |
---|---|---|---|
无界通配符? | 匹配 “任意引用类型” | “我不知道具体类型,但它是一个引用类型” | List<?>匹配List、List、List |
上界通配符? extends T | 匹配 “T 或 T 的子类” 的类型 | “我不知道具体类型,但它是 T 的子类或 T 本身” | List<? extends Number>匹配List、List、List |
下界通配符? super T | 匹配 “T 或 T 的父类” 的类型 | “我不知道具体类型,但它是 T 的父类或 T 本身” | List<? super Integer>匹配List、List、List |
原理图解:通配符的模糊匹配范围
图 2-5:三种通配符的模糊匹配范围示意图 —— 无界通配符覆盖所有引用类型,上界通配符覆盖 “T 及其子类” 范围,下界通配符覆盖 “T 及其父类” 范围,实现对 “一组相关类型” 的统一适配
2.2.3 通配符与泛型参数的差异:“模糊匹配” vs “明确绑定”
很多开发者会混淆通配符?与泛型参数T,但二者的核心定位完全不同:泛型参数是 “明确的类型变量”,通配符是 “模糊的类型范围匹配符”,具体差异如下:
特性维度 | 泛型参数T(如List) | 通配符?(如List<?>) |
---|---|---|
类型定位 | 明确的 “类型变量”,需在使用时绑定具体类型 | 模糊的 “类型范围匹配符”,无需绑定具体类型 |
复用逻辑 | 绑定具体类型后,复用 “该类型的逻辑” | 不绑定具体类型,复用 “跨类型的通用逻辑” |
读写权限 | 可读可写(如List可添加T类型元素) | 受范围约束(如List<?>只读,List<? super T>可写T类型) |
适用场景 | 需明确类型的业务逻辑(如Box存储T类型数据) | 通用工具逻辑(如打印任意 List、统计任意 List 元素数量) |
案例:泛型参数与通配符的场景对比
// 1. 泛型参数T:明确类型,用于业务逻辑(存储指定类型数据)
class Box<T> {private T content;// 可读:返回T类型(明确类型)public T getContent() { return content; }// 可写:接收T类型(明确类型)public void setContent(T content) { this.content = content; }
}// 使用:绑定具体类型String,存储String数据
Box<String> stringBox = new Box<>();
stringBox.setContent("Java"); // 正确:只能添加String
String content = stringBox.getContent(); // 正确:直接返回String// 2. 通配符?:模糊匹配,用于通用工具(打印任意List)
public static void printAnyList(List<?> list) {// 只读:只能按Object读取(模糊类型无法确定具体类型)for (Object obj : list) {System.out.println(obj);}// 可写限制:只能添加null(无法确定具体类型,避免类型安全问题)// list.add("A"); // 错误:无法确定List的具体类型,禁止添加非null元素list.add(null); // 正确:null是所有类型的子类
}// 使用:模糊匹配任意List,无需绑定具体类型
printAnyList(Arrays.asList("A", "B")); // 匹配List<String>
printAnyList(Arrays.asList(1, 2)); // 匹配List<Integer>
2.2.4 通配符 “模糊匹配” 的类型安全保障:读写权限约束
通配符的 “模糊匹配” 并非 “牺牲类型安全换灵活性”,而是通过 “读写权限约束” 确保类型安全,核心原则是:“模糊匹配的范围越大,读写权限越严格”,具体约束逻辑如下:
- 无界通配符List<?>:最严格的读写约束
- 可读:只能按Object读取(因无法确定具体类型,只能用所有类型的父类Object接收);
- 可写:只能添加null(唯一例外,因null是所有类型的子类,不会破坏类型安全);
- 目的:确保 “只读取不修改” 的场景安全(如打印、统计元素数量)。
- 上界通配符List<? extends T>:只读优化的约束
安全问题示例:
List<? extends Number> numList = new ArrayList<Integer>();
// numList.add(123); // 错误:若允许添加Integer,后续numList若指向List<Double>会崩溃
// numList.add(3.14); // 错误:同理,无法确定numList的具体子类类型
- 可读:按T类型读取(因匹配的是 “T 或其子类”,所有元素都可安全转为T);
- 可写:禁止添加非null元素(无法确定具体是T的哪个子类,添加T可能导致 “子类列表存入父类对象” 的安全问题);
- 目的:确保 “只读取(按 T 类型)不修改” 的场景安全(如数值列表求和、筛选 T 类型属性)。
下界通配符List<? super T>:可写优化的约束
安全保障示例:
List<? super Integer> intSuperList = new ArrayList<Number>();
intSuperList.add(123); // 正确:Number列表可接收Integer(子类对象)
intSuperList.add(456); // 正确:符合“父类列表存子类对象”的多态规则List<? super Integer> objList = new ArrayList<Object>();
objList.add(789); // 正确:Object列表可接收Integer
- 可读:按Object读取(因匹配的是 “T 或其父类”,元素类型可能是T、T的父类、T的祖父类,只能用Object接收);
- 可写:允许添加T或T的子类(因 “父类列表可安全接收子类对象”,符合多态规则);
目的:确保 “只添加(T 类型)不依赖读取类型” 的场景安全(如批量添加 T 类型元素、数据收集)。 - 原理图解:通配符的读写权限约束逻辑
图 2-6:通配符的读写权限约束逻辑 —— 无界通配符因 “匹配范围最大”,读写权限最严格;上界通配符优化 “读取权限”(按 T 读取),下界通配符优化 “写入权限”(按 T 写入),最终在 “灵活性” 与 “类型安全” 间实现平衡
2.2.5 通配符 “模糊匹配” 的典型应用场景
通配符的 “模糊匹配” 核心用于 “通用工具类”“框架组件” 等需要 “跨类型适配” 的场景,以下是典型应用场景总结:
- 通用工具方法:处理 “任意类型或某类相关类型” 的通用逻辑,如打印、统计、复制等;
- 示例:printList(List<?>)(打印任意 List)、countNonEmpty(List<?>)(统计任意 List 非空元素)。
- 框架层组件:框架无法预知用户使用的具体类型,需通过通配符适配 “多类型场景”;
- 示例:ORM 框架的PageResult<? extends T>(适配任意实体的分页结果)、Spring 的BeanFactory.getBean(String)(返回任意类型的 Bean)。
- 复杂类型嵌套:处理 “泛型嵌套” 场景中 “部分类型不确定” 的情况,如Map<String, List<?>>(适配 value 为任意 List 的 Map);
- 示例:配置解析工具(解析key=String,value=任意List的配置)、报表生成工具(处理key=维度,value=任意数值列表的报表数据)。
- 类型安全的多态适配:在 “不破坏类型安全” 的前提下,实现 “多类型泛型对象的统一管理”;
- 示例:定义List<List<? extends Number>>存储 “不同数值类型的 List”(如List、List),统一进行求和计算。