什么是泛型
Java 泛型(Generics)是自 JDK 5.0 版本引入的一项关键特性,它将参数化类型的概念引入到 Java 编程中。泛型的本质是为了创建可操作多种数据类型的类、接口和方法,同时提供编译时期的类型安全检查。这项特性显著提升了代码的可读性、健壮性和复用性。
核心优势
引入泛型主要是为了解决两个核心问题:类型安全和代码重用。
- 类型安全:在泛型出现之前,Java 的集合类(如
ArrayList)只能存储Object类型的对象。这意味着在从集合中取出对象时,必须进行强制类型转换,这不仅繁琐,而且容易在运行时引发ClassCastException异常。泛型在编译时期就对类型进行检查,从而避免了这类错误的发生。 - 消除强制类型转换:通过在声明集合时指定其可存储的元素类型,可以省去大量的强制类型转换代码,使代码更加简洁、清晰。
- 代码复用:泛型允许程序员编写通用的算法和数据结构,这些代码可以安全地应用于多种不同的数据类型,而无需为每种类型都编写重复的逻辑。
基本语法
泛型通过尖括号 <> 来声明类型参数,这些参数在类、接口或方法被使用时,会被具体的类型所替代。
1. 泛型类
泛型类是在实例化时接受类型参数的类。其定义语法如下:
class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
在这个例子中,T 是一个类型参数,可以在创建 Box 对象时指定为任何类类型。
Box<Integer> integerBox = new Box<>();
integerBox.set(10); // 正确
// integerBox.set("hello"); // 编译时错误
Box<String> stringBox = new Box<>();
stringBox.set("Hello World"); // 正确
2. 泛型接口
与泛型类类似,接口也可以通过泛型来定义。
interface Generator<T> {
T next();
}
实现该接口的类,可以选择指定具体的类型,也可以继续使用泛型。
class RandomIntegerGenerator implements Generator<Integer> {
private Random rand = new Random();
@Override
public Integer next() {
return rand.nextInt(100);
}
}
3. 泛型方法
泛型方法是在调用时才指明具体类型的方法,它可以在普通类中定义,也可以在泛型类中定义。 泛型方法的类型参数声明位于方法修饰符和返回类型之间。
public class Utils {
public static <T> void printArray(T[] inputArray) {
for (T element : inputArray) {
System.out.printf("%s ", element);
}
System.out.println();
}
}
调用泛型方法时,编译器通常会利用类型推断来确定 T 的具体类型。
Integer[] intArray = { 1, 2, 3, 4, 5 };
String[] stringArray = { "Hello", "World" };
Utils.printArray(intArray);
Utils.printArray(stringArray);
注意: 静态方法如果要使用泛型,必须被定义为泛型方法,因为它无法访问类上定义的泛型类型。
泛型通配符
通配符 ? 是 Java 泛型中一个强大而灵活的概念,用于表示未知的类型。
1. 无界通配符 <?>
无界通配符 <?> 表示可以是任何类型,它在逻辑上是所有 List<具体类型> 的父类。 当一个方法的参数类型是 List<?> 时,你不能向这个列表中添加任何元素(null 除外),因为编译器无法确定元素的具体类型是否匹配。但是,你可以安全地读取元素,因为你知道它们都是 Object 类型的。
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.print(elem + " ");
}
System.out.println();
}
2. 上界通配符 <? extends T>
上界通配符 <? extends T> 表示参数化的类型必须是 T 或 T 的子类。 这通常用于当你需要从一个数据结构中读取数据(作为生产者)的场景。
例如,一个接受 List<? extends Number> 的方法,可以处理 List<Integer>、List<Double> 等,因为 Integer 和 Double 都是 Number 的子类。你不能往这个列表中添加元素(null 除外),因为编译器无法确定列表的确切类型,但你可以安全地将元素读取为 Number 类型。
3. 下界通配符 <? super T>
下界通配符 <? super T> 表示参数化的类型必须是 T 或 T 的父类。 这通常用于当你需要向一个数据结构中写入数据(作为消费者)的场景。
例如,一个接受 List<? super Integer> 的方法,可以处理 List<Integer>、List<Number> 或 List<Object>。你可以安全地向这个列表中添加 Integer 或其子类型的对象,因为它们都可以向上转型为 T。但是,当你从中读取数据时,只能保证它们是 Object 类型的。
核心机制:类型擦除
Java 泛型是通过一种被称为“类型擦除”(Type Erasure)的机制来实现的。 这意味着泛型信息只存在于编译阶段,在生成的字节码中,所有的泛型类型参数都会被替换为其边界类型(对于无界的类型参数,则替换为 Object)。
例如,Box<T> 在编译后会变成 Box,其内部的 T 类型会被替换为 Object。编译器会在必要的地方自动插入类型转换代码,以保证类型安全。
为什么需要类型擦除?
类型擦除主要是为了向后兼容。在 JDK 5.0 引入泛型时,需要确保新的泛型代码能够与旧的、非泛型的代码库兼容。
类型擦除的影响:
- 无法获取运行时泛型类型:在运行时,
ArrayList<String>和ArrayList<Integer>的getClass()方法都将返回ArrayList.class,因为泛型信息已被擦除。 - 不能创建泛型数组:你不能直接创建像
new T[]这样的泛型数组,因为数组的类型在运行时必须是具体的。 - 不能实例化类型参数:不能创建类型参数的实例,例如
new T()。 - 静态上下文中不能使用类的类型参数:静态变量或方法不能引用其所在类的类型参数。