引言
你是否遇到过这样的场景?写了一个"万能"容器类,用Object
接收所有类型数据,结果取出时总担心ClassCastException
;或者为Integer
、String
、Double
分别写了几乎一样的工具类,代码重复到怀疑人生?Java泛型(Generics)正是为解决这些痛点而生——它像一把"类型安全锁",在编译期就圈定数据类型,又像一块"代码橡皮泥",用一套模板适配所有类型。今天我们就从0到1拆解泛型,带你彻底玩转这个Java的"类型魔法"。
一、泛型的核心:给类型加个"万能模板"
泛型的本质是类型参数化,即把类型(如String
、Integer
)当作参数传入,让类、方法、接口在定义时不绑定具体类型,使用时再指定。它解决了两大问题:
- 类型安全:编译期检查类型匹配,避免运行时
ClassCastException
; - 代码复用:一套逻辑适配所有类型,告别"复制-粘贴改类型"的低效操作。
举个简单例子:用非泛型容器存储String
时,数据会被当作Object
保存,取出时需要强制转型,一旦手滑存了Integer
,运行时就会崩溃:
// 非泛型的"万能"容器(危险警告!)
class NonGenericBox {
private Object data;
public void set(Object data) { this.data = data; }
public Object get() { return data; }
}
// 使用时可能翻车
NonGenericBox box = new NonGenericBox();
box.set("Hello"); // 存String
String str = (String) box.get(); // 需要强制转型(暂时安全)
box.set(123); // 手滑存了Integer
String wrongStr = (String) box.get(); // 运行时崩溃:ClassCastException!
而泛型容器会在编译期"锁死"类型,存错类型直接报错,取出时无需转型:
// 泛型容器(安全又省心)
class GenericBox<T> { // T是类型参数,代表"任意类型"
private T data;
public void set(T data) { this.data = data; }
public T get() { return data; } // 直接返回T类型,无需转型
}
// 使用时指定类型
GenericBox<String> stringBox = new GenericBox<>();
stringBox.set("Hello"); // 只能存String
String str = stringBox.get(); // 直接得到String,无需转型
stringBox.set(123); // 编译报错!不能存Integer到String类型的Box
二、泛型的三大形态:类、方法、接口
泛型可以作用于类、方法、接口,分别解决不同场景的需求。
1. 泛型类:最常用的"类型模板"
泛型类在类名后加<T>
(T
是Type的缩写,也可以用E
、K
、V
等约定符号),T
在类内部代表具体类型。
示例:定义一个支持任意类型的栈(Stack)
// 泛型类:支持任意类型的栈
class GenericStack<E> { // E通常代表Element(元素)
private Object[] elements; // 内部用Object数组存储(类型擦除后解释)
private int size = 0;
private static final int DEFAULT_CAPACITY = 10;
public GenericStack() {
elements = new Object[DEFAULT_CAPACITY];
}
// 入栈:只能添加E类型元素
public void push(E element) {
// 扩容逻辑(省略)
elements[size++] = element;
}
// 出栈:返回E类型元素
@SuppressWarnings("unchecked") // 抑制类型转换警告(类型擦除导致)
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
E element = (E) elements[--size]; // 取出时转型为E
elements[size] = null; // 帮助GC
return element;
}
}
// 使用:创建String类型的栈
GenericStack<String> stringStack = new GenericStack<>();
stringStack.push("Java");
stringStack.push("泛型");
String top = stringStack.pop(); // top = "泛型"(类型安全)
// 使用:创建Integer类型的栈
GenericStack<Integer> intStack = new GenericStack<>();
intStack.push(1024);
intStack.push(2048);
Integer num = intStack.pop(); // num = 2048(无需转型)
2. 泛型方法:独立于类的"类型魔法"
泛型方法是指在方法声明中定义类型参数的方法,它不依赖类是否是泛型类。适用于仅某个方法需要泛型支持的场景。
示例:定义一个打印任意类型数组的工具方法
class ArrayPrinter {
// 泛型方法:<T>声明类型参数,T是数组元素类型
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strArray = {"A", "B", "C", "D"};
Double[] doubleArray = {1.0, 2.0, 3.0};
printArray(intArray); // 输出:1 2 3 4 5
printArray(strArray); // 输出:A B C D
printArray(doubleArray); // 输出:1.0 2.0 3.0
}
}
3. 泛型接口:定义通用行为规范
泛型接口在接口名后加<T>
,实现类可以选择指定具体类型,或保持泛型。
示例:定义一个"生产者"接口,生产指定类型的对象
// 泛型接口:生产T类型的对象
interface Producer<T> {
T produce();
}
// 实现类1:指定具体类型(String)
class StringProducer implements Producer<String> {
@Override
public String produce() {
return "New String";
}
}
// 实现类2:保持泛型(由子类决定类型)
class NumberProducer<T extends Number> implements Producer<T> { // 限制T为Number子类
private T value;
public NumberProducer(T value) { this.value = value; }
@Override
public T produce() {
return value;
}
}
// 使用示例
Producer<String> strProducer = new StringProducer();
String str = strProducer.produce(); // str = "New String"
Producer<Integer> intProducer = new NumberProducer<>(123);
Integer num = intProducer.produce(); // num = 123
三、类型擦除:泛型的"运行时隐身术"
Java泛型是编译期机制,运行时泛型类型信息会被"擦除"(Type Erasure),即T
会被替换为原始类型(通常是Object
,若有限定则替换为上限类型)。这是Java泛型与C++模板的核心区别。
示例:验证类型擦除
import java.util.ArrayList;
public class TypeErasureDemo {
public static void main(String[] args) {
ArrayList<String> stringList = new ArrayList<>();
ArrayList<Integer> intList = new ArrayList<>();
// 编译期类型不同,但运行时类型相同
System.out.println(stringList.getClass() == intList.getClass()); // 输出:true
}
}
运行结果为true
,因为ArrayList<String>
和ArrayList<Integer>
在运行时都被擦除为ArrayList
,类型信息被抹除。
类型擦除的影响:
- 无法在运行时通过
instanceof
判断泛型类型(如list instanceof ArrayList<String>
会编译报错); - 泛型类不能创建具体类型的数组(如
new T[10]
会编译报错,需用(T[]) new Object[10]
并抑制警告); - 静态方法/变量不能使用类的泛型参数(静态成员属于类,泛型参数属于实例)。
四、通配符:泛型的"灵活适配器"
当需要处理不确定具体类型的泛型对象时,通配符?
可以让代码更灵活。常用通配符有三种:
通配符 | 含义 | 使用场景 |
---|---|---|
? (无界通配符) | 任意类型 | 仅读取数据,不关心具体类型 |
? extends T (上界通配符) | 类型是T或其子类(上限为T) | 读取数据(生产者),保证类型安全 |
? super T (下界通配符) | 类型是T或其父类(下限为T) | 写入数据(消费者),保证类型安全 |
示例1:无界通配符?
——通用数据访问
// 打印任意类型的ArrayList
public static void printList(ArrayList<?> list) {
for (Object element : list) { // 只能用Object接收元素
System.out.print(element + " ");
}
System.out.println();
}
// 使用
ArrayList<String> strList = new ArrayList<>(List.of("A", "B", "C"));
ArrayList<Integer> intList = new ArrayList<>(List.of(1, 2, 3));
printList(strList); // 输出:A B C
printList(intList); // 输出:1 2 3
示例2:上界通配符? extends T
——安全读取"生产者"
假设需要计算一个数字列表的总和(数字包括Integer
、Double
等Number
子类):
// 计算Number子类列表的总和(上界通配符)
public static double sumOfList(ArrayList<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue(); // 所有Number子类都有doubleValue()方法
}
return sum;
}
// 使用
ArrayList<Integer> intList = new ArrayList<>(List.of(1, 2, 3));
System.out.println(sumOfList(intList)); // 输出:6.0
ArrayList<Double> doubleList = new ArrayList<>(List.of(1.5, 2.5, 3.5));
System.out.println(sumOfList(doubleList)); // 输出:7.5
示例3:下界通配符? super T
——安全写入"消费者"
假设需要将一个Integer
列表的元素添加到另一个父类列表(如Number
或Object
列表):
// 将Integer列表的元素添加到父类列表(下界通配符)
public static void addIntegers(ArrayList<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// 使用
ArrayList<Number> numberList = new ArrayList<>();
addIntegers(numberList); // 可以添加,因为Number是Integer的父类
System.out.println(numberList); // 输出:[1, 2, 3]
ArrayList<Object> objectList = new ArrayList<>();
addIntegers(objectList); // 可以添加,因为Object是Number的父类
System.out.println(objectList); // 输出:[1, 2, 3]
五、泛型的终极价值:代码复用+类型安全的双赢
回到开头的痛点,泛型通过编译期类型检查和运行时类型擦除的巧妙设计,既保证了代码的复用性(一套逻辑处理所有类型),又避免了运行时类型错误(编译期提前拦截)。
比如ArrayList
的泛型实现,让我们无需为String
、Integer
等类型重复编写容器类;再比如Collections.sort()
方法的泛型设计,让它能排序任意实现了Comparable
接口的对象。
泛型是Java中最强大的类型工具之一,但也有一些边界(如类型擦除的限制)。你在实际开发中遇到过哪些泛型的"坑"?或者有没有用泛型优化过复杂代码?欢迎在评论区分享你的经验,一起解锁更多泛型的玩法!