Java 泛型(Generic)& 通配符(Wildcard)详解
Java 泛型(Generic)& 通配符(Wildcard)详解
Java 泛型是 Java 5 引入的核心特性,本质是 “参数化类型”—— 允许在定义类、接口、方法时,将类型作为 “参数” 传入,使用时再指定具体类型。核心价值是 在编译时强制类型安全,避免类型转换错误,同时实现代码复用。
通配符(?)是泛型的补充,用于 灵活匹配未知类型,解决泛型类型之间的兼容性问题(如 List<String> 与 List<Object> 不兼容),常见于集合、框架工具类的参数声明。
一、泛型概述:为什么需要泛型?
在没有泛型的 Java 1.4 及之前,只能通过 Object 类型实现 “通用” 逻辑,但存在两大问题:
-
类型不安全:可向集合中添加任意类型对象,运行时才会抛出类型转换异常;
-
冗余的强制转换:取出数据时必须手动强制转换,代码繁琐且易出错。
示例:无泛型 vs 有泛型
// 1. 无泛型(Java 1.4 及之前)
List list = new ArrayList();
list.add("Java"); // 存入 String
list.add(123); // 存入 Integer(编译不报错,运行时可能出问题)
String str = (String) list.get(1); // 运行时抛出 ClassCastException
// 2. 有泛型(Java 5+)
List<String> strList = new ArrayList<>();
strList.add("Java"); // 仅允许存入 String
// strList.add(123); // 编译直接报错(类型安全)
String str2 = strList.get(0); // 无需强制转换(编译时确定类型)
泛型的核心优势
-
类型安全:编译时检查类型,禁止存入不匹配的类型;
-
消除强制转换:编译器自动推断类型,取出数据无需手动转换;
-
代码复用:同一套逻辑可适配多种类型(如
ArrayList<String>、ArrayList<Integer>共用ArrayList类); -
可读性强:代码中明确指定类型,语义清晰。
二、泛型的核心用法
泛型的使用场景主要分为三类:泛型类、泛型接口、泛型方法,核心语法是用 <类型参数> 声明(常用 T、E、K、V 等作为类型参数名,仅为约定,可自定义)。
1. 泛型类:类定义时声明泛型参数
泛型类是指在类名后声明泛型参数,实例化时指定具体类型。
语法结构
// 声明泛型类:<T> 是类型参数(T = Type,可替换为 E/K/V 等)
public class 类名<T> {
// 泛型字段(不能是 static,因为泛型类型实例化时确定)
private T data;
// 泛型构造器
public 类名(T data) {
this.data = data;
}
// 泛型方法(返回值为泛型类型)
public T getData() {
return data;
}
}
实战案例:自定义泛型容器
// 泛型类:通用数据容器
public class GenericBox<T> {
private T content;
// 泛型构造器
public GenericBox(T content) {
this.content = content;
}
// 泛型方法:设置数据
public void setContent(T content) {
this.content = content;
}
// 泛型方法:获取数据
public T getContent() {
return content;
}
public static void main(String[] args) {
// 实例化时指定具体类型:String 类型
GenericBox<String> stringBox = new GenericBox<>("Hello Generic");
String str = stringBox.getContent(); // 无需强制转换
System.out.println("String 容器:" + str);
// 实例化时指定具体类型:Integer 类型
GenericBox<Integer> intBox = new GenericBox<>(100);
int num = intBox.getContent();
System.out.println("Integer 容器:" + num);
// 错误:不能存入不匹配的类型(编译报错)
// stringBox.setContent(123);
}
}
2. 泛型接口:接口定义时声明泛型参数
泛型接口与泛型类语法类似,实现接口时需指定具体类型,或继续保留泛型参数。
语法结构
public interface 接口名<T> {
T process(T data); // 泛型方法
}
实战案例:泛型生成器接口
// 泛型接口:生成器(生成指定类型的对象)
public interface Generator<T> {
T generate(); // 生成 T 类型对象
}
// 实现接口:指定具体类型(String 生成器)
class StringGenerator implements Generator<String> {
@Override
public String generate() {
return "随机字符串:" + Math.random();
}
}
// 实现接口:保留泛型参数(通用数字生成器)
class NumberGenerator<T extends Number> implements Generator<T> {
private final Class<T> clazz;
public NumberGenerator(Class<T> clazz) {
this.clazz = clazz;
}
@Override
public T generate() throws IllegalAccessException, InstantiationException {
// 简单示例:通过反射生成实例(实际需根据类型处理)
return clazz.newInstance();
}
}
// 测试
public class GenericInterfaceDemo {
public static void main(String[] args) throws Exception {
Generator<String> stringGen = new StringGenerator();
System.out.println(stringGen.generate());
Generator<Integer> intGen = new NumberGenerator<>(Integer.class);
System.out.println(intGen.generate()); // 输出 Integer 默认值 0
}
}
3. 泛型方法:方法单独声明泛型参数
泛型方法是指 方法本身声明泛型参数(与类 / 接口的泛型无关),即使所在类不是泛型类,也可定义泛型方法。
核心语法
// 泛型方法:<T> 必须放在返回值前,声明方法的泛型参数
public <T> T 方法名(T 参数) {
// 逻辑
}
关键特点
-
泛型方法的
<T>是独立的,与类的泛型参数(若有)无关; -
静态方法不能使用类的泛型参数,需定义为泛型方法;
-
调用时可省略泛型声明(Java 7+ 支持类型推断)。
实战案例:泛型工具方法
public class GenericMethodDemo {
// 泛型方法:交换数组中两个位置的元素(适配任意类型数组)
public static <T> void swap(T[] array, int i, int j) {
if (i < 0 || i >= array.length || j < 0 || j >= array.length) {
return;
}
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// 泛型方法:获取数组中第一个元素
public static <T> T getFirstElement(T[] array) {
return array.length > 0 ? array[0] : null;
}
public static void main(String[] args) {
// 测试 String 数组
String[] strArray = {"A", "B", "C"};
swap(strArray, 0, 2); // 类型推断:T=String,无需显式声明
System.out.println("交换后:" + Arrays.toString(strArray)); // [C, B, A]
System.out.println("第一个元素:" + getFirstElement(strArray)); // C
// 测试 Integer 数组
Integer[] intArray = {1, 2, 3};
swap(intArray, 1, 2);
System.out.println("交换后:" + Arrays.toString(intArray)); // [1, 3, 2]
}
}
三、泛型边界限定:<T extends ...>
默认情况下,泛型参数 T 可以是任意引用类型(如 String、Integer、自定义类)。但有时需要限制 T 的范围(如仅允许数字类型),此时需使用 边界限定。
核心语法
// 上界限定:T 必须是「类A」或其子类(类A可以是类、接口) <T extends 类A> // 多边界限定:T 必须实现多个接口(类在前,接口在后,用 & 连接) <T extends 类A & 接口B & 接口C>
关键说明
-
上界限定用
extends(无论类还是接口,统一用extends,不用implements); -
限定后,可在泛型中调用上界类型的方法(类型安全);
-
最多只能有一个类作为上界(若有类,必须放在第一个),接口可多个。
实战案例:泛型边界限定
// 泛型类:仅支持 Number 及其子类(Integer、Double 等)
public class NumberProcessor<T extends Number> {
// 计算数组元素的总和(可调用 Number 的 doubleValue() 方法)
public double sum(T[] array) {
double total = 0.0;
for (T num : array) {
total += num.doubleValue(); // 调用上界 Number 的方法
}
return total;
}
public static void main(String[] args) {
// 正确:Integer 是 Number 子类
NumberProcessor<Integer> intProcessor = new NumberProcessor<>();
Integer[] intArray = {1, 2, 3};
System.out.println("整数总和:" + intProcessor.sum(intArray)); // 6.0
// 正确:Double 是 Number 子类
NumberProcessor<Double> doubleProcessor = new NumberProcessor<>();
Double[] doubleArray = {1.5, 2.5, 3.5};
System.out.println("小数总和:" + doubleProcessor.sum(doubleArray)); // 7.5
// 错误:String 不是 Number 子类(编译报错)
// NumberProcessor<String> stringProcessor = new NumberProcessor<>();
}
}
// 多边界限定:T 必须是 CharSequence 且实现 Comparable
class MultiBoundDemo<T extends CharSequence & Comparable<T>> {
public int compare(T a, T b) {
return a.compareTo(b); // 调用 Comparable 的方法
}
public static void main(String[] args) {
MultiBoundDemo<String> demo = new MultiBoundDemo<>();
System.out.println(demo.compare("A", "B")); // -1(String 满足双边界)
}
}
四、通配符详解:?、? extends T、? super T
通配符(?)是泛型的 “占位符”,用于 匹配未知的泛型类型,解决泛型类型的兼容性问题(如 List<String> 不能赋值给 List<Object>)。
核心问题:泛型的 “不可变性”
Java 泛型是 不可变的(Invariant),即 List<String> 不是 List<Object> 的子类,即使 String 是 Object 的子类。例如:
List<String> strList = new ArrayList<>(); // 错误:编译报错(泛型不可变) // List<Object> objList = strList;
原因:若允许赋值,objList 可添加 Integer(Object 子类),导致 strList 中混入非 String 类型,破坏类型安全。
通配符的核心作用:在不破坏类型安全的前提下,实现泛型类型的灵活匹配。
1. 无界通配符:?(匹配任意类型)
无界通配符 ? 表示 “任意引用类型”,适用于 仅读取数据,不修改数据 的场景(如打印集合元素)。
语法与场景
// 无界通配符:List<?> 可接收 List<String>、List<Integer> 等任意泛型 List
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
关键限制
-
可读取数据(返回
Object类型,需按需转换); -
不可添加数据(除
null外),因为无法确定集合的具体类型,添加任何类型都可能破坏类型安全。
实战案例
public class UnboundedWildcardDemo {
// 无界通配符:打印任意类型的集合
public static void printCollection(Collection<? extends Object> list) {
public static void printCollection(Collection<?> collection) {
for (Object obj : collection) {
System.out.println(obj);
}
}
public static void main(String[] args) {
List<String> strList = Arrays.asList("A", "B", "C");
List<Integer> intList = Arrays.asList(1, 2, 3);
// 无界通配符适配任意泛型集合
printCollection(strList); // 打印 A、B、C
printCollection(intList); // 打印 1、2、3
List<?> wildcardList = strList;
// 错误:不可添加非 null 数据(编译报错)
// wildcardList.add("D");
wildcardList.add(null); // 仅允许添加 null
}
}
2. 上界通配符:? extends T(匹配 T 及其子类)
上界通配符 ? extends T 表示 “T 或 T 的子类”,适用于 仅读取数据(生产者) 的场景(如获取集合中的元素,计算最大值)。
语法与场景
// 上界通配符:List<? extends Number> 可接收 List<Integer>、List<Double> 等
public static double sumList(List<? extends Number> list) {
double total = 0.0;
for (Number num : list) {
total += num.doubleValue(); // 可调用 T(Number)的方法
}
return total;
}
关键限制
-
可读取数据(返回
T类型,类型安全); -
不可添加数据(除
null外),因为无法确定集合的具体子类型(如List<? extends Number>可能是List<Integer>或List<Double>,添加Integer可能破坏Double集合)。
实战案例
public class BoundedUpperWildcardDemo {
// 上界通配符:计算 Number 子类集合的总和
public static double calculateSum(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.5, 2.5);
// 适配 Number 子类集合
System.out.println("整数总和:" + calculateSum(intList)); // 6.0
System.out.println("小数总和:" + calculateSum(doubleList)); // 4.0
List<? extends Number> numList = intList;
// 错误:不可添加数据(编译报错)
// numList.add(4);
}
}
3. 下界通配符:? super T(匹配 T 及其父类)
下界通配符 ? super T 表示 “T 或 T 的父类”,适用于 仅修改数据(消费者) 的场景(如向集合中添加元素)。
语法与场景
// 下界通配符:List<? super Integer> 可接收 List<Integer>、List<Number>、List<Object>
public static void addIntegers(List<? super Integer> list) {
list.add(1); // 可添加 T(Integer)及其子类(如 int 自动装箱)
list.add(2);
}
关键限制
-
可添加数据(仅允许添加
T及其子类,类型安全); -
读取数据时返回
Object类型(需强制转换,不推荐读取)。
实战案例
public class BoundedLowerWildcardDemo {
// 下界通配符:向集合中添加 Integer 元素
public static void fillWithIntegers(List<? super Integer> list) {
list.add(10);
list.add(20);
list.add(30);
}
public static void main(String[] args) {
// 场景 1:List<Integer>(T 本身)
List<Integer> intList = new ArrayList<>();
fillWithIntegers(intList);
System.out.println("Integer 集合:" + intList); // [10, 20, 30]
// 场景 2:List<Number>(T 的父类)
List<Number> numList = new ArrayList<>();
fillWithIntegers(numList);
numList.add(40.5); // 可添加其他 Number 子类
System.out.println("Number 集合:" + numList); // [10, 20, 30, 40.5]
// 场景 3:List<Object>(T 的祖父类)
List<Object> objList = new ArrayList<>();
fillWithIntegers(objList);
objList.add("Hello"); // 可添加其他 Object 子类
System.out.println("Object 集合:" + objList); // [10, 20, 30, Hello]
List<? super Integer> superList = objList;
// 正确:可添加 Integer 及其子类
superList.add(40);
// 错误:不可添加父类类型(如 Number)
// superList.add(50.5);
// 读取数据:返回 Object,需强制转换
Object obj = superList.get(0);
Integer num = (Integer) obj;
}
}
4. 通配符核心原则:PECS
PECS 是 Producer Extends, Consumer Super 的缩写,是选择通配符的黄金法则:
-
Producer(生产者)
:仅提供数据(读取),用
? extends T(上界通配符);-
例:
List<? extends Number>是 Number 的 “生产者”,只能读取 Number 类型数据;
-
-
Consumer(消费者)
:仅接收数据(写入),用
? super T(下界通配符);-
例:
List<? super Integer>是 Integer 的 “消费者”,只能写入 Integer 类型数据;
-
-
若既读又写:不使用通配符,直接指定具体泛型类型(如
List<Integer>)。
PECS 原则示例
// 生产者:读取数据(extends)
public static <T> T getMax(List<? extends T> list, Comparator<T> comparator) {
if (list.isEmpty()) return null;
T max = list.get(0);
for (T t : list) {
if (comparator.compare(t, max) > 0) {
max = t;
}
}
return max;
}
// 消费者:写入数据(super)
public static <T> void addAll(List<? super T> dest, List<? extends T> src) {
for (T t : src) { // src 是生产者(extends)
dest.add(t); // dest 是消费者(super)
}
}
五、泛型的常见误区与限制
1. 类型擦除(Type Erasure)
Java 泛型是 编译时特性,编译后泛型信息会被 “擦除”:
-
无边界泛型(
T)擦除为Object; -
有上界泛型(
T extends Number)擦除为上界类型(Number)。
类型擦除的影响
public class TypeErasureDemo {
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 编译后泛型信息被擦除,两者类型相同(都是 List)
System.out.println(strList.getClass() == intList.getClass()); // true
// 错误:不能用 instanceof 判断泛型类型(编译报错)
// if (strList instanceof List<String>) {}
}
}
2. 不能创建泛型数组
泛型数组会破坏类型安全(数组是协变的,而泛型是不可变的),编译直接报错:
// 错误:不能创建泛型数组(编译报错) // List<String>[] strListArray = new List<String>[10]; // 正确:用无界通配符数组(但需谨慎使用) List<?>[] wildcardArray = new List<?>[10]; wildcardArray[0] = new ArrayList<String>();
3. 静态成员不能依赖泛型参数
泛型参数是实例级别的(每个实例的泛型类型可能不同),静态成员属于类级别,无法依赖实例的泛型参数:
public class GenericStaticDemo<T> {
// 错误:静态字段不能使用泛型参数(编译报错)
// private static T staticData;
// 正确:静态泛型方法(自己声明泛型参数)
public static <E> E staticGenericMethod(E data) {
return data;
}
}
4. 不能捕获泛型异常
泛型类型不能作为异常类型在 catch 中使用:
// 错误:不能捕获泛型异常(编译报错)
// try {
// } catch (T e) {
// }
六、泛型在实际开发中的应用
1. 集合框架(最常用)
Java 集合框架(ArrayList、HashMap、HashSet 等)全部基于泛型实现,例如:
// HashMap<K, V>:键值对泛型
Map<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("张三", 90);
scoreMap.put("李四", 85);
Integer score = scoreMap.get("张三"); // 无需强制转换
2. 自定义工具类
泛型可实现通用工具类,适配多种类型,例如:
// 泛型工具类:数组工具
public class ArrayUtils {
// 泛型方法:数组转集合
public static <T> List<T> toList(T[] array) {
return Arrays.asList(array);
}
// 泛型方法:集合转数组
public static <T> T[] toArray(List<T> list, T[] array) {
return list.toArray(array);
}
}
3. 框架开发(如 Spring、MyBatis)
框架大量使用泛型简化 API,例如:
-
Spring 的
JdbcTemplate:queryForObject(String sql, Class<T> requiredType); -
MyBatis 的
Mapper接口:public interface UserMapper { User selectById(Integer id); }(泛型适配返回值类型)。
总结
泛型与通配符的核心是 “类型安全 + 代码复用”:
-
泛型
:通过参数化类型,编译时强制类型安全,消除强制转换;
-
核心用法:泛型类、泛型接口、泛型方法;
-
边界限定:
<T extends ...>限制泛型类型范围。
-
-
通配符
:解决泛型不可变性问题,灵活匹配未知类型;
-
无界
?:适配任意类型,仅读不写; -
上界
? extends T:生产者,仅读不写; -
下界
? super T:消费者,仅写不读; -
黄金法则:PECS(Producer Extends, Consumer Super)。
-
-
常见限制:类型擦除、不能创建泛型数组、静态成员不依赖泛型参数。
更多推荐
所有评论(0)