Java泛型
一、什么是泛型?
Java泛型是JDK 1.5引入的一个核心语法特性,本质上是 参数化类型 (Parameterized Type)。通俗地说,就是把类型(如String、Integer)当作参数一样传递给类、接口或方法。
先来看一个没有使用泛型的例子:
List list = new ArrayList();
list.add("Hello");
list.add(123); // 编译没问题,但容易误操作
String s = (String) list.get(0); // 必须强制转型,且转型风险高
有了泛型后:
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 编译报错,类型安全
String s = list.get(0); // 自动转型,无需强转
二、泛型的核心作用是什么?
泛型主要有三大核心作用 ,分别对应编程中的“安全、易用、重用”:
1. 编译时类型安全检查(最强优势)
泛型将运行时的 ClassCastException(类型转换异常)提前到 编译期间 。只要你往 List<String>里放Integer,IDE就会立刻报红线。这极大地提高了程序的稳定性和健壮性。
2. 消除强制类型转换(代码更简洁)
在没有泛型时,从集合中取元素必须手动 (String)强转。有了泛型,编译器自动帮你插入隐式的强转,代码更加干净利落。
3. 实现通用算法和代码复用(即“模板”思想)
开发者可以编写一套代码,适配多种数据类型。例如,你写一个排序方法,不需要为 Integer写一遍、为 Double写一遍,只需定义 <T>,传入什么类型就处理什么类型。
三、泛型具体怎么使用呢?
泛型的使用分为如下三种:
一、泛型类
泛型类通常用于封装不依赖具体类型的数据结构,比如集合、容器、POJO包装类。
定义语法 :class 类名 <T> { }
// 定义一个通用的“结果包装类”
public class Result<T> {
private int code;
private String message;
private T data; // 数据字段类型不确定
public Result(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public T getData() {
return data; // 返回类型是 T
}
public void setData(T data) {
this.data = data;
}
}
//使用示例
public class Main {
public static void main(String[] args) {
// 实例化时指定 T 为 String
Result<String> result1 = new Result<>(200, "成功", "Hello Java");
String data1 = result1.getData(); // 直接拿 String,无需强转
// 实例化时指定 T 为 Integer
Result<Integer> result2 = new Result<>(200, "成功", 100);
int data2 = result2.getData(); // 直接拿 int(自动拆箱)
}
}
二、泛型接口
泛型接口常用于定义通用生产者和消费者接口,具体的实现类在实现时指定具体的类型。
定义语法 :interface 接口名 <T> { }
// 定义一个通用处理器接口
public interface Processor<T, R> {
R process(T input); // 输入 T,输出 R
}
// -------- 实现方式1:实现时直接指定具体类型(类不再是泛型) ----------
public class StringToIntegerProcessor implements Processor<String,Integer>{
@Override
public Integer process(String input) {
return Integer.parseInt(input); // 明确知道处理 String 返回 Integer
}
}
// -------- 实现方式2:实现类也保持泛型(延迟到使用时确定) ----------
public class IdentityProcessor<T> implements Processor<T,T> {
@Override
public T process(T input) {
return input;//输入是什么类型,返回就是什么类型
}
}
//使用示例
public class Main {
public static void main(String[] args) {
StringToIntegerProcessor p1 = new StringToIntegerProcessor();
Integer num = p1.process("123"); // 返回 Integer
IdentityProcessor<Double> p2 = new IdentityProcessor<>();
Double d = p2.process(3.14); // 返回 Double
}
}
三、泛型方法
只要方法返回类型前面有一个独立的 <T>(或 <E>),它就是泛型方法。它和类本身是不是泛型没有必然关系 。
import java.util.Arrays;
import java.util.List;
public class GenericMethodUtils {
// 这里的 <T> 声明在 static 和返回值之间,表明这是一个泛型方法
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
//泛型方法结合集合(将数组转成 List)
public static <E> List<E> arrayToList(E[] array) {
return Arrays.asList(array);
}
public static void main(String[] args) {
String[] names = {"A", "B", "C"};
swap(names, 0, 2); // 编译器自动推断 T 为 String
System.out.println(Arrays.toString(names)); // 输出 [C, B, A]
Integer[] nums = {1, 2, 3};
List<Integer> list = arrayToList(nums); // 自动推断 E 为 Integer
}
}
需要注意的是:假设你定义了一个泛型类,静态方法想使用类的泛型 T,这是语法不允许的!!!
// 错误示范
public class Container<T> {
private T value;
// 编译报错!因为静态方法随着类加载而存在,而 T 是 new 对象时才确定
// public static T getDefaultValue() { return null; }
// 正确写法:静态方法自己声明自己的泛型,即便字母也叫 T,它和类的 T 也是两个完全不同的变量
public static <T> T getDefaultValue(T defaultValue) {
return defaultValue;
}
}
四、泛型的类型擦除机制是什么?
简单来说,泛型只存在于编译期,在编译生成的字节码中,泛型类型信息会被“擦除”并替换为原始类型(Raw Type),JVM运行时根本不知道 List<String>和 List<Integer>的区别。
一、具体是怎么擦的呢?
编译器擦除时,遵循两条铁律:
- 无限定类型(如
<T>) :直接擦除为Object。 - 有限定类型(如
<T extends Number>) :擦除为第一个边界类型(这里是Number)。
来看下面的代码示例:
//编译前
public class Box<T> {
private T value;
public T getValue() { return value; }
}
//编译后(等效于)
public class Box {
private Object value;
public Object getValue() { return value; } // 所有 T 都变成了 Object
}
//编译前(带边界的)
public class Box<T extends Number> {
private T value;
public T getValue() { return value; }
}
// 擦除后变成了 Number,而不是 Object
public class Box {
private Number value;
public Number getValue() { return value; }
}
二、桥接方法
桥接方法是擦除带来的编译器“魔法”,它保证了在泛型场景下的多态性。先来看下面的这段代码:
class Parent {
public Object say(Object arg) { return arg; }
}
class Child extends Parent {
public String say(String arg) { return arg; } // 注意:参数类型变了!
}
这其实不是重写 (参数不同),只是重载。但在泛型场景下,编译器为了保留多态,会偷偷生成桥接方法!!
class GenericParent<T> {
public T say(T arg) { return arg; }
}
class GenericChild extends GenericParent<String> {
@Override
public String say(String arg) { return arg; }
}
擦除后,GenericParent 的 say 变成了 Object say(Object)。为了让 GenericChild 的 String say(String) 正确重写它,编译器在 GenericChild 字节码中自动生成了一个桥接方法 :
// 这是编译器偷偷加在 GenericChild 字节码里的
public Object say(Object arg) {
return say((String) arg); // 帮你强转,并调用你写的真实方法
}
三、擦除带来的限制
| 限制 | 错误示例 | 原因 |
|---|---|---|
不能 instanceof 具体泛型 |
if (list instanceof List<String>) |
运行时都是 List,泛型参数不存在 |
不能 new T() |
T obj = new T(); |
擦除后不知道 T 是什么,无法分配内存 |
| 不能创建泛型数组 | T[] arr = new T[10]; |
数组必须知道确切类型,擦除后无法保证类型安全 |
| 不能静态引用类的泛型 | static T staticVar; |
静态属性和类加载绑定,但 T 属于实例化时确定 |
还有一个需要注意的就是方法重载冲突:
// 编译报错!擦除后两个方法的签名都是 print(List),在JVM中无法区分。
public void print(List<String> list) {}
public void print(List<Integer> list) {}
四、插入强制转型
既然类型会被“擦除”,那么编译时检查是怎么生效的呢?这就涉及擦除的另一项任务 : 插入强制转型 。
当你写 String s = list.get(0); 时:
- 编译期检查放进去的一定是
String。 - 擦除后,
list.get(0)实际返回的是Object。 - 编译器自动在字节码中插入向下转型 :
String s = (String) list.get(0);
所以泛型其实是一种 语法糖 ,它将“繁琐的手动强转”和“运行时的风险”提前转移给了编译器去自动处理。
五、反射获取泛型
既然泛型被擦除了,为什么还可以通过反射获取到泛型呢?这是因为虽然字段(Field) 和方法参数(Method) 上的泛型被擦除了,但源码中的泛型声明元数据被保留在了类的 Signature属性(字节码额外区域) 中。你可以通过反射的 getGenericParameterTypes() 和 getGenericReturnType() 获取这些元数据。这就是为什么 Gson 和 Jackson 能够通过 TypeToken 或 ParameterizedTypeReference 解析出 List<String> 中的 String 类型——它们利用了字节码中残留的签名信息,而非运行时实际持有的类型。
五、泛型通配符
Java泛型通配符(Wildcard)用 ? 表示,它的诞生是为了解决泛型中一个很“反直觉”的问题: List<String> 并不是 List<Object> 的子类型 。假设有一个方法 void print(List<Object> list),你无法传入 List<String>,因为编译器认为它们不兼容。为了给API增加灵活性 ,让方法能接受多种泛型类型的集合,通配符应运而生。
一、无界通配符(?)
表示类型未知,不做任何限制。
- 语法 :
List<?> - 读写权限 :只能读,且读出来都是
Object;不能写(除了写入null)。 - 使用场景 :方法只依赖于
Object类的方法(如toString、hashCode),或者不关心具体类型。
public class Main {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
printList(list);
}
public static void printList(List<?> list) {
// 可以遍历,因为不管是什么类型,取出来总是 Object 的子类
for (Object obj : list) {
System.out.println(obj);
}
// 注意:不能往里面 add 元素!
// list.add("hello"); // 编译报错!因为编译器不知道具体类型,无法保证类型安全
list.add(null); // 只有 null 是特例,可以加
}
}
二、上界通配符(? extends T)
表示类型必须是 T 或 T 的子类 (Upper Bound)。
- 语法 :
List<? extends Number>表示可以是List<Integer>、List<Double>或List<Number>。 - 读写权限 :只能读,读出来是
T类型;不能写(除了null)。 - 原因 :你只知道它是
Number的子类,但具体是Integer还是Double不确定。如果允许写入,写一个Integer进一个List<Double>就会出大问题,所以编译器 强制禁止写入 。
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
System.out.println(sumOfList(list));
}
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
// 可以安全读取,因为无论子类是什么,都能自动转型为 Number
for (Number num : list) {
sum += num.doubleValue();
}
// list.add(123); // 编译报错!不能写入具体元素
return sum;
}
}
三、下界通配符(? super T)
表示类型必须是 T 或 T 的父类 (Lower Bound)。
- 语法 :
List<? super Integer>表示可以是List<Integer>、List<Number>或List<Object>。 - 读写权限 :可以写入(
T及其子类),但读出来全是Object。 - 原因 :你只知道它是
Integer的某种父类。往里面放Integer是绝对安全的(因为父类容器一定能装下子类)。但读的时候,编译器不知道具体是Number还是Object,所以只能返回最顶层的Object。
public static void addIntegers(List<? super Integer> list) {
// 可以安全地写入 Integer
list.add(1);
list.add(2);
// list.add(3.5); // 编译报错!不能写入 Double,因为 Double 不是 Integer 的父类
// 读取时,只能当做 Object
Object obj = list.get(0);
}
四、黄金法则PECS
PECS 是 Producer Extends, Consumer Super 的缩写。这是 Joshua Bloch(Effective Java作者)提出的最佳实践:
- 如果集合是“生产者”(Producer) :即你从中读取数据来用,用
? extends T。 - 如果集合是“消费者”(Consumer) :即你往里面写入数据,用
? super T。 - 如果既要读又要写 :不要用通配符,直接用具体的类型参数
<T>。
//JDK源码中利用PECS的标准写法:Collections.copy()
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
五、注意事项
通配符只能出现在方法参数、局部变量和返回值中,不能在类声明和new中使用。
public class MyBox<?> {}(编译报错,声明类只能用<T>)List<?> list = new ArrayList<?>();(编译报错,不能new通配符,右边必须写具体类型,如new ArrayList<String>())

浙公网安备 33010602011771号