泛型(Generic)是Java中用于实现"类型安全"和"代码复用"的核心特性,其核心思想是"将类型参数化"------即定义类、接口或方法时不指定具体类型,而是在使用时动态指定,以此避免强制类型转换、减少运行时ClassCastException风险。PECS原则则是泛型通配符使用的核心指导原则,解决了泛型类型"不协变"的问题。
一、Java泛型基本用法
Java泛型的核心使用形式分为三类:泛型类、泛型接口、泛型方法。以下结合具体案例逐一说明,所有案例均符合Java 8+语法规范。
1. 泛型类:类级别的类型参数化
泛型类是在类定义时声明"类型参数"(用尖括号<>包裹,常用T、E、K、V等作为占位符),实例化类时指定具体类型,使类的属性、方法能适配多种类型。
1.1 定义格式
java
// 格式:class 类名<类型参数1, 类型参数2,...> { ... }
// 常用占位符约定:T(Type)表示通用类型,E(Element)表示集合元素类型,K(Key)键类型,V(Value)值类型
public class 泛型类名<T> {
// 泛型属性
private T data;
// 泛型构造器
public 泛型类名(T data) {
this.data = data;
}
// 泛型方法(类级泛型参数)
public T getData() {
return data;
}
}
1.2 实例:通用容器类
java
// 泛型容器类:适配任意类型的"包装容器"
public class Container<T> {
private T content; // 泛型属性:存储任意类型数据
// 构造器:接收泛型类型参数
public Container(T content) {
this.content = content;
}
// getter:返回泛型类型
public T getContent() {
return content;
}
// setter:设置泛型类型
public void setContent(T content) {
this.content = content;
}
}
1.3 使用泛型类
java
public class GenericTest {
public static void main(String[] args) {
// 1. 实例化时指定具体类型:String
Container<String> strContainer = new Container<>("Hello Generic");
String strContent = strContainer.getContent(); // 无需强转,直接获取String
System.out.println(strContent); // 输出:Hello Generic
// 2. 实例化时指定具体类型:Integer
Container<Integer> intContainer = new Container<>(100);
Integer intContent = intContainer.getContent(); // 直接获取Integer
System.out.println(intContent); // 输出:100
// 3. 未指定泛型类型(原始类型):编译警告,默认视为Object
Container rawContainer = new Container(3.14);
Object objContent = rawContainer.getContent();
Double doubleContent = (Double) objContent; // 需手动强转,有风险
}
}
2. 泛型接口:接口级别的类型参数化
泛型接口与泛型类类似,在接口定义时声明类型参数,实现接口时需指定具体类型(或继续保留泛型参数),常用于定义"通用行为模板"。
2.1 定义格式
java
// 格式:interface 接口名<类型参数> { ... }
public interface 泛型接口名<T> {
T process(T data); // 泛型方法:参数和返回值均为泛型类型
}
2.2 实例:数据处理器接口
java
// 泛型接口:定义"数据处理"的通用行为
public interface DataProcessor<T> {
T process(T input); // 处理输入数据,返回同类型结果
}
2.3 实现泛型接口
实现泛型接口有两种方式:① 指定具体类型;② 保留泛型参数(成为泛型类)。
java
// 方式1:实现时指定具体类型(String)
public class StringProcessor implements DataProcessor<String> {
@Override
public String process(String input) {
// 具体逻辑:将字符串转为大写
return input.toUpperCase();
}
}
// 方式2:保留泛型参数(成为泛型类)
public class NumberProcessor<T extends Number> implements DataProcessor<T> {
@Override
public T process(T input) {
// 具体逻辑:对数字进行累加(以Integer为例,实际可适配所有Number子类)
if (input instanceof Integer) {
return (T) Integer.valueOf(input.intValue() + 10);
}
return input;
}
}
2.4 使用泛型接口实现类
java
public class GenericInterfaceTest {
public static void main(String[] args) {
// 方式1:使用指定具体类型的实现类
DataProcessor<String> strProcessor = new StringProcessor();
String result1 = strProcessor.process("java");
System.out.println(result1); // 输出:JAVA
// 方式2:使用保留泛型参数的实现类
DataProcessor<Integer> intProcessor = new NumberProcessor<>();
Integer result2 = intProcessor.process(20);
System.out.println(result2); // 输出:30
}
}
3. 泛型方法:方法级别的类型参数化
泛型方法是在"方法本身"声明类型参数(与类/接口的泛型参数独立),适用于"单个方法需要适配多种类型"的场景,无需将整个类设为泛型。
3.1 定义格式
java
// 格式:访问修饰符 <类型参数> 返回值类型 方法名(参数列表) { ... }
// 关键:类型参数声明在返回值类型之前
public <T> T 泛型方法名(T 参数) {
// 逻辑...
return 参数;
}
3.2 实例:通用数据转换方法
java
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
// 工具类:包含泛型方法
public class ConvertUtils {
// 泛型方法1:单个对象转换
public static <S, T> T convert(S source, Function<S, T> converter) {
return converter.apply(source);
}
// 泛型方法2:集合批量转换
public static <S, T> List<T> convertList(List<S> sourceList, Function<S, T> converter) {
List<T> targetList = new ArrayList<>();
for (S source : sourceList) {
targetList.add(converter.apply(source));
}
return targetList;
}
}
3.3 使用泛型方法
java
import java.util.Arrays;
import java.util.List;
public class GenericMethodTest {
public static void main(String[] args) {
// 1. 单个对象转换:String → Integer
String numStr = "123";
Integer num = ConvertUtils.convert(numStr, Integer::valueOf);
System.out.println(num); // 输出:123
// 2. 集合批量转换:Integer → String
List<Integer> intList = Arrays.asList(1, 2, 3);
List<String> strList = ConvertUtils.convertList(intList, String::valueOf);
System.out.println(strList); // 输出:[1, 2, 3]
}
}
二、PECS原则详解
在讲解PECS原则前,需先明确一个核心问题:Java泛型默认是"不协变"的。例如:List<Integer> 不是 List<Number> 的子类,即使 Integer 是Number 的子类。这种设计是为了类型安全(避免向List<Number> 中添加 Double 元素,导致 List<Integer> 读取时出错)。
为了解决"泛型不协变"导致的灵活性问题,Java引入了泛型通配符 ?,而PECS原则就是通配符使用的"黄金法则"。
1. PECS原则定义
PECS 是 Producer Extends, Consumer Super 的缩写,直译为:
-
Producer Extends :如果泛型对象是"生产者",仅向外提供数据,只读,则使用
? extends T(上限通配符); -
Consumer Super :如果泛型对象是"消费者",仅接收外部数据,只写,则使用
? super T(下限通配符)。
核心目标:在保证类型安全的前提下,提升泛型的灵活性。
2. 拆解PECS:Producer Extends(生产者用extends)
"生产者"指的是泛型对象的核心作用是"提供数据"(比如读取集合中的元素),此时我们只需要从对象中获取数据,不需要向其中添加数据。
使用 ? extends T 表示:泛型对象的类型是 T 或 T 的子类,这样可以保证获取到的数据都能安全地向上转型为 T。
2.1 案例:求和工具(生产者场景)
需求:实现一个工具方法,对"数字集合"求和(数字可以是Integer、Double、Long等Number子类)。此时集合是"生产者"(提供数字供求和)。
java
import java.util.List;
public class MathUtils {
// 生产者场景:用 ? extends Number(Number及其子类都可传入)
public static double sum(List<? extends Number> numberList) {
double total = 0.0;
// 只读:从集合中获取元素(生产者提供数据)
for (Number num : numberList) {
total += num.doubleValue(); // 所有Number子类都有doubleValue()方法
}
return total;
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<Double> doubleList = List.of(1.1, 2.2, 3.3);
// 正确:Integer和Double都是Number子类,可传入
System.out.println(sum(intList)); // 输出:6.0
System.out.println(sum(doubleList)); // 输出:6.6
// 错误:String不是Number子类,编译不通过
// List<String> strList = List.of("1", "2");
// sum(strList);
}
}
2.2 关键注意:生产者不可写
使用 ? extends T 的泛型对象,无法向其中添加非null元素。因为编译器无法确定集合的具体类型(是T还是T的某个子类),添加元素可能破坏类型安全。
java
public static void testProducerWrite() {
List<? extends Number> numList = new ArrayList<Integer>(); // 实际是Integer集合
// 错误:编译不通过,无法确定numList的具体类型
// numList.add(10); // 即使是Integer,也无法添加(编译器无法保证安全)
// numList.add(3.14); // Double更不行
// 唯一例外:可以添加null(null是所有类型的默认值)
numList.add(null);
}
3. 拆解PECS:Consumer Super(消费者用super)
"消费者"指的是泛型对象的核心作用是"接收数据"(比如向集合中添加元素),此时我们只需要向对象中写入数据,不需要从中读取数据。
使用 ? super T 表示:泛型对象的类型是T 或 T 的父类,这样可以保证向其中添加的 T 类型元素能安全地向下转型为父类类型。
3.1 案例:数据添加工具(消费者场景)
需求:实现一个工具方法,向集合中添加多个Integer元素。此时集合是"消费者"(接收Integer数据)。
java
import java.util.ArrayList;
import java.util.List;
public class CollectionUtils {
// 消费者场景:用 ? super Integer(Integer及其父类都可传入)
public static void addIntegers(List<? super Integer> list) {
// 只写:向集合中添加元素(消费者接收数据)
list.add(1);
list.add(2);
list.add(3);
}
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
List<Number> numberList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
// 正确:Integer、Number、Object都是Integer的父类(或自身),可传入
addIntegers(intList);
addIntegers(numberList);
addIntegers(objList);
System.out.println(intList); // 输出:[1, 2, 3]
System.out.println(numberList); // 输出:[1, 2, 3]
System.out.println(objList); // 输出:[1, 2, 3]
// 错误:String不是Integer的父类,编译不通过
// List<String> strList = new ArrayList<>();
// addIntegers(strList);
}
}
3.2 关键注意:消费者读取需强转
使用 ? super T 的泛型对象,从其中读取元素时,只能向上转型为Object(因为编译器无法确定集合的具体类型是T的哪个父类),需要手动强转,有类型安全风险。
java
public static void testConsumerRead() {
List<? super Integer> objList = new ArrayList<Object>();
objList.add(10);
// 读取时只能转为Object
Object obj = objList.get(0);
// 需手动强转,有风险(若集合中存在非Integer元素,运行时抛ClassCastException)
Integer num = (Integer) obj;
System.out.println(num); // 输出:10
}
4. PECS原则总结表
| 场景类型 | 通配符用法 | 核心作用 | 读写权限 | 典型案例 |
|---|---|---|---|---|
| 生产者(提供数据) | ? extends T | 适配T及其子类,保证读取数据类型安全 | 只读(不可写非null元素) | 集合求和、数据筛选 |
| 消费者(接收数据) | ? super T | 适配T及其父类,保证写入数据类型安全 | 只写(读取需强转) | 集合添加元素、数据批量插入 |
| 读写都需要 | 不使用通配符(指定具体类型) | 类型固定,保证读写完全安全 | 可读写 | 普通业务数据集合 |
三、核心注意事项
-
泛型是"编译期特性",运行时会进行"类型擦除":即
List<Integer>和List<String>在运行时都是List.class,无法通过反射直接获取泛型类型。 -
通配符
?是"未知类型",不能用于定义泛型类/接口/方法(只能用于方法参数或局部变量)。 -
PECS原则的核心是"读写分离":根据泛型对象的核心作用(提供数据/接收数据)选择通配符,避免盲目使用通配符导致类型安全问题。
-
避免过度使用泛型:简单场景(如固定类型的集合)无需使用泛型,过度泛型化会增加代码复杂度。
四、总结
-
Java泛型基本用法核心是"类型参数化",分为泛型类、泛型接口、泛型方法三类,核心价值是类型安全和代码复用;
-
PECS原则是泛型通配符的使用指南:生产者(只读)用
? extends T,消费者(只写)用? super T; -
使用泛型时需平衡"灵活性"和"类型安全":通配符提升灵活性,但限制读写权限;具体类型保证读写安全,但灵活性较低。