深入理解Java泛型:从基础到通配符的完整指南
目录
前言
在Java编程中,我们经常需要编写可以处理多种数据类型的代码。在泛型出现之前,我们通常使用Object类型来实现"通用"编程,但这种方式存在明显的缺陷:类型不安全和需要显式强制类型转换。
想象一下,你正在开发一个容器类,需要存储各种类型的数据。在没有泛型的情况下,你可能会这样写:
public class Container {
private Object item;
public void setItem(Object item) {
this.item = item;
}
public Object getItem() {
return item;
}
}
// 使用时
Container container = new Container();
container.setItem("Hello"); // 存储字符串
String str = (String) container.getItem(); // 需要强制转换
这种方法的问题在于:
编译时无法检测类型错误
运行时强制转换可能导致
ClassCastException代码可读性和安全性较差
Java泛型(Generics)在JDK 1.5中引入,解决了这些问题。泛型的核心思想是:参数化类型,即在定义类、接口或方法时,使用一个或多个类型形参,在使用时指定具体的类型实参。
一、泛型的基本概念与语法
1.1 泛型类的定义
泛型类在类名后面使用尖括号<>声明类型参数:
public class Box<T> {
private T content;
public Box(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
}
这里,T是一个类型形参(Type Parameter),它只是一个占位符,表示某种类型。按照约定,类型形参通常使用单个大写字母表示:
T- Type(类型)
E- Element(元素)
K- Key(键)
V- Value(值)
N- Number(数字)
1.2 泛型类的使用
使用泛型类时,需要指定具体的类型实参:
// 指定类型实参为String
Box<String> stringBox = new Box<>("Hello");
String content = stringBox.getContent(); // 无需强制转换
// 指定类型实参为Integer
Box<Integer> integerBox = new Box<>(42);
int value = integerBox.getContent(); // 自动拆箱
1.3 类型推导
Java编译器可以根据上下文推导类型实参,因此可以省略构造器中的类型实参:
// 完整写法
Box<String> box1 = new Box<String>("Hello");
// 简化写法(类型推导)
Box<String> box2 = new Box<>("Hello"); // 编译器推导出String
二、泛型的实现原理:类型擦除
2.1 什么是类型擦除?
Java泛型是通过类型擦除(Type Erasure)实现的。这意味着在编译期间,所有的泛型类型信息都会被擦除,替换为它们的原始类型(Raw Type)。
// 编译前
public class Box<T> {
private T content;
// ...
}
// 编译后(字节码层面)
public class Box {
private Object content;
// ...
}
可以通过查看字节码验证这一点:
javap -c Box.class
2.2 类型擦除带来的限制
由于类型擦除,泛型在使用上有一些限制:
1. 不能创建泛型数组
// 错误:不能创建泛型数组
T[] array = new T[10]; // 编译错误
// 正确的方式:使用Object数组并强制转换
T[] array = (T[]) new Object[10];
但是,强制转换的方式并不安全,因为运行时类型信息已被擦除:
public class GenericArray<T> {
private T[] array;
public GenericArray(int size) {
// 编译时通过,但运行时可能出现ClassCastException
array = (T[]) new Object[size];
}
public T[] getArray() {
return array; // 这里可能有问题
}
}
// 使用时
GenericArray<Integer> intArray = new GenericArray<>(10);
Integer[] array = intArray.getArray(); // 运行时ClassCastException
2. 通过反射创建泛型数组
如果需要创建确切的泛型类型数组,可以使用反射:
public class SafeGenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public SafeGenericArray(Class<T> clazz, int size) {
// 使用反射创建确切类型的数组
array = (T[]) Array.newInstance(clazz, size);
}
public T[] getArray() {
return array;
}
}
// 使用时
SafeGenericArray<Integer> intArray = new SafeGenericArray<>(Integer.class, 10);
Integer[] array = intArray.getArray(); // 安全
2.3 为什么选择类型擦除?
Java选择类型擦除的主要目的是向后兼容。泛型是在JDK 1.5中引入的,而Java已经有了大量的非泛型代码。通过类型擦除,可以确保泛型代码与非泛型代码的二进制兼容性。
三、泛型的上界(类型边界)
有时我们需要限制泛型类型参数的范围,这就是类型边界(Type Bounds)。
3.1 单一上界
public class NumberBox<T extends Number> {
private T number;
public NumberBox(T number) {
this.number = number;
}
public double doubleValue() {
return number.doubleValue(); // 可以调用Number的方法
}
}
// 可以这样用
NumberBox<Integer> intBox = new NumberBox<>(10);
NumberBox<Double> doubleBox = new NumberBox<>(3.14);
// NumberBox<String> stringBox = new NumberBox<>("Hello"); // 编译错误
3.2 多重上界
类型参数可以有多个上界,但类必须放在第一位,接口放在后面:
public class MultiBound<T extends Number & Comparable<T> & Serializable> {
private T value;
// ...
}
3.3 上界通配符
上界通配符使用? extends Type的语法,表示可以接受Type或其子类:
public void processNumbers(List<? extends Number> numbers) {
for (Number n : numbers) {
System.out.println(n.doubleValue());
}
}
// 可以接受各种Number子类的List
processNumbers(new ArrayList<Integer>());
processNumbers(new ArrayList<Double>());
重要特性:上界通配符的集合是只读的(确切地说,不能添加除null以外的元素):
List<? extends Number> numbers = new ArrayList<Integer>();
// numbers.add(new Integer(10)); // 编译错误
// numbers.add(new Double(3.14)); // 编译错误
numbers.add(null); // 只有null可以添加
这是因为编译器无法确定?的具体类型,所以为了类型安全,不允许添加元素。
四、泛型方法
除了泛型类,我们还可以定义泛型方法。泛型方法可以在普通类、泛型类或接口中定义。
4.1 泛型方法的定义
public class ArrayUtils {
// 静态泛型方法
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// 非静态泛型方法
public <E> E getFirstElement(List<E> list) {
if (list == null || list.isEmpty()) {
return null;
}
return list.get(0);
}
}
4.2 泛型方法的使用
// 使用类型推导
Integer[] numbers = {1, 2, 3, 4, 5};
ArrayUtils.swap(numbers, 0, 4); // 交换第一个和最后一个元素
// 显式指定类型参数
ArrayUtils.<Integer>swap(numbers, 1, 3);
// 使用泛型方法
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
String first = new ArrayUtils().getFirstElement(names);
4.3 泛型方法与泛型类的区别
-
作用域不同:泛型类的类型参数作用于整个类,而泛型方法的类型参数只作用于该方法
-
类型参数可以不同:泛型类中的泛型方法可以使用自己的类型参数,与类的类型参数不同
public class Pair<K, V> {
private K key;
private V value;
// 泛型方法,使用自己的类型参数T
public <T> void printType(T item) {
System.out.println(item.getClass().getName());
}
}
五、通配符及其应用
通配符?是泛型中一个强大的特性,它提供了更灵活的类型匹配。
5.1 无界通配符
无界通配符?表示可以匹配任何类型:
public void printList(List<?> list) {
for (Object elem : list) {
System.out.println(elem);
}
}
// 可以接受任何类型的List
printList(new ArrayList<String>());
printList(new ArrayList<Integer>());
printList(new ArrayList<Object>());
5.2 上界通配符
前面已经介绍过,上界通配符? extends Type用于生产者(Producer),即提供数据的场景。
5.3 下界通配符
下界通配符使用? super Type语法,表示可以接受Type或其父类:
public void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i); // 可以添加Integer及其子类
}
}
// 可以传入各种父类List
addNumbers(new ArrayList<Integer>());
addNumbers(new ArrayList<Number>());
addNumbers(new ArrayList<Object>());
重要特性:下界通配符的集合是可写的,但读取时只能作为Object类型:
List<? super Integer> list = new ArrayList<Number>();
list.add(10); // 可以添加Integer
list.add(20); // 可以添加Integer
// Integer num = list.get(0); // 编译错误,不能直接读取为Integer
Object obj = list.get(0); // 只能读取为Object
5.4 PECS原则
PECS(Producer Extends, Consumer Super)原则是使用通配符的重要指导原则:
生产者(Producer):如果你需要从一个泛型集合中读取数据(生产数据),使用
? extends T消费者(Consumer):如果你需要向一个泛型集合中写入数据(消费数据),使用
? super T既是生产者又是消费者:如果既要读又要写,不要使用通配符
示例:
// 生产者示例:从源集合复制到目标集合
public static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T item : src) {
dest.add(item);
}
}
// 使用
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Number> numList = new ArrayList<>();
copy(intList, numList); // 正确:Integer是Number的子类
六、泛型在实际开发中的应用
6.1 集合框架
Java集合框架是泛型最典型的应用:
// 类型安全的集合
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
// names.add(123); // 编译错误,类型安全
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 87);
int aliceScore = scores.get("Alice"); // 无需强制转换
6.2 设计模式中的泛型
工厂模式
public interface Factory<T> {
T create();
}
public class StringFactory implements Factory<String> {
@Override
public String create() {
return new String();
}
}
public class IntegerFactory implements Factory<Integer> {
@Override
public Integer create() {
return new Integer(0);
}
}
策略模式
public interface Comparator<T> {
int compare(T o1, T o2);
}
public class StringLengthComparator implements Comparator<String> {
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
6.3 工具类开发
public class CollectionUtils {
// 安全的空集合
public static <T> List<T> emptyList() {
return Collections.emptyList();
}
// 转换数组为列表
public static <T> List<T> asList(T... items) {
return Arrays.asList(items);
}
// 查找最大值
public static <T extends Comparable<? super T>> T max(Collection<T> coll) {
if (coll == null || coll.isEmpty()) {
return null;
}
Iterator<T> i = coll.iterator();
T candidate = i.next();
while (i.hasNext()) {
T next = i.next();
if (next.compareTo(candidate) > 0) {
candidate = next;
}
}
return candidate;
}
}
七、泛型的限制与注意事项
7.1 不能使用基本类型
泛型类型参数必须是引用类型,不能是基本类型:
// 错误
List<int> intList; // 编译错误
// 正确
List<Integer> integerList; // 使用包装类
7.2 不能创建泛型实例
由于类型擦除,不能在运行时创建泛型类型的实例:
public class Example<T> {
public void doSomething() {
// T obj = new T(); // 编译错误
// T[] array = new T[10]; // 编译错误
}
}
7.3 静态上下文中不能使用类型参数
静态成员不能使用类的类型参数:
public class Box<T> {
// private static T staticField; // 编译错误
// public static T staticMethod() { return null; } // 编译错误
// 但静态泛型方法可以使用自己的类型参数
public static <E> E staticGenericMethod(E param) {
return param;
}
}
7.4 不能使用instanceof检查泛型类型
由于类型擦除,不能在运行时检查泛型类型:
public <T> void checkType(Object obj) {
// if (obj instanceof T) { // 编译错误
// // ...
// }
// 但可以检查原始类型
if (obj instanceof Box) {
Box<?> box = (Box<?>) obj;
// ...
}
}
总结
泛型的优点
-
类型安全:编译时检查类型,减少运行时错误
-
代码复用:编写一次,适用于多种类型
-
消除强制转换:使代码更简洁、清晰
-
更好的API设计:提供更灵活、更安全的API
更多推荐

所有评论(0)