泛型(Generics)是Java 5中引入的一个特性,它提供了编译时类型安全检查的机制,允许在编码阶段指定集合的元素类型或者方法的参数类型。使用泛型可以增强程序的可读性和安全性,避免在运行时出现类型转换异常。
定义:泛型是通过类型参数来实现的,这些类型参数在类、接口或方法的定义中使用一对尖括号(<>)指定。
一、泛型类
泛型类是一种能够处理不同数据类型的类。它的定义方式是将类型参数放在类的名称后面,通常使用单个大写字母来表示类型参数(如:T
、E
、K
、V
等)。例如:
// 泛型类
public class Box<T> {
private T value; // 存储一个类型为T的值
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
// 使用泛型类
public class Main {
public static void main(String[] args) {
Box<Integer> intBox = new Box<>(); // 创建一个Integer类型的Box
intBox.setValue(10); // 设置值为10
System.out.println(intBox.getValue()); // 输出10
Box<String> strBox = new Box<>(); // 创建一个String类型的Box
strBox.setValue("Hello, Generics!");
System.out.println(strBox.getValue()); // 输出Hello, Generics!
}
}
在上面的例子中,Box<T>
是一个泛型类,它的成员变量和方法都使用了类型参数 T
,并且当我们创建 Box
实例时,指定了类型参数(如 Integer
或 String
)。这使得 Box
可以用于不同类型的数据。
二、泛型方法
泛型不仅可以用于类和接口,还可以用于方法。泛型方法允许在方法的签名中声明类型参数,而这个类型参数是与方法调用时所传递的类型相关的。其基本语法是将类型参数放在方法返回类型之前,泛型方法可以是静态的,也可以是非静态的。例如:
public class GenericMethodExample {
// 泛型方法
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 = {"Hello", "World", "Generics"};
printArray(intArray); // 输出:1 2 3 4 5
printArray(strArray); // 输出:Hello World Generics
}
}
三、泛型接口
与泛型类类似,接口也可以使用泛型。接口的泛型常用于设计需要处理不同数据类型的行为。例如,Java的 List
接口就是一个常见的泛型接口。
// 泛型接口
public interface Container<T> {
void add(T item);
T get(int index);
}
// 泛型接口的实现
public class StringContainer implements Container<String> {
private List<String> list = new ArrayList<>();
@Override
public void add(String item) {
list.add(item);
}
@Override
public String get(int index) {
return list.get(index);
}
}
public class Main {
public static void main(String[] args) {
Container<String> container = new StringContainer();
container.add("Hello");
container.add("Generics");
System.out.println(container.get(0)); // 输出Hello
System.out.println(container.get(1)); // 输出Generics
}
}
在这个例子中,Container<T>
是一个泛型接口,表示一个容器,可以存储任何类型的元素。StringContainer
是这个接口的一个实现,它专门处理 String
类型的数据。
四、泛型的通配符
泛型通配符用于表示未知类型。主要有三种形式:无限制通配符?
、有上限的通配符? extends Type
和有下限的通配符? super Type
。
- 无限制通配符
?
:表示任何类型。 - 有上限的通配符
? extends Type
:表示类型是Type
或其子类。 - 有下限的通配符
? super Type
:表示类型是Type
或其父类。
public class WildcardExample {
// 使用通配符?表示类型未知
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.print(obj + " ");
}
System.out.println();
}
// 使用上界通配符表示类型T及其子类
public static void printNumberList(List<? extends Number> list) {
for (Number num : list) {
System.out.print(num + " ");
}
System.out.println();
}
// 使用下界通配符表示类型T及其父类
public static void addIntegerList(List<? super Integer> list) {
list.add(10);
list.add(20);
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3, 4, 5);
List<Double> doubleList = List.of(3.14, 2.71, 1.62);
printList(intList); // 输出:1 2 3 4 5
printList(doubleList); // 输出:3.14 2.71 1.62
printNumberList(intList); // 输出:1 2 3 4 5
printNumberList(doubleList); // 输出:3.14 2.71 1.62
List<Number> numberList = new ArrayList<>();
addIntegerList(numberList); // 向numberList添加Integer对象
System.out.println(numberList); // 输出:[10, 20]
}
}
在上面的例子中,printList
使用了 ?
来表示接受任何类型的列表,printNumberList
使用了 ? extends Number
来限制只能传入 Number
或其子类的列表,而 addIntegerList
使用了 ? super Integer
来接受 Integer
或其父类的列表。
五、泛型擦除
在Java中,泛型擦除(Generic Type Erasure)是指编译器在编译时将泛型类型信息去除(擦除),并替换为原始类型或通配符。这是因为Java的泛型是编译时的特性,在运行时并不会保留泛型类型的信息,泛型的类型参数会被擦除为其边界类型(如果有的话),或者是 Object
类型。
这种擦除机制确保了泛型代码可以与没有泛型的代码兼容,也可以与老旧的类库互操作。然而,这也意味着在运行时,泛型信息不可用,无法通过反射等方式获取类型参数的实际类型。
1. 基本概念
假设我们定义了一个泛型类 Box<T>
,它存储类型为 T
的元素,编译后,所有泛型类型都会被擦除,替换为其原始类型或边界类型。例如:
// 泛型类
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
编译器会将这个类擦除为:
// 擦除后的类
public class Box {
private Object value;
public Box(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
在编译时,T
被擦除为 Object
类型(因为没有给 T
限定边界),这意味着 Box<T>
的实例在运行时其实是一个 Box
类型的对象,它的 value
可以是任何类型的对象。
细节:
- 类型参数擦除:所有泛型类型的类型参数都会在编译后被擦除成
Object
(如果没有指定边界)或者其边界类型(如果有的话)。 - 类型边界的处理:如果泛型类型参数指定了上界(例如
T extends Number
),则擦除后的类型会被替换为这个边界类型。
例如:
// 泛型类:限定T必须是Number的子类
public class Box<T extends Number> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
在编译后,Box<T extends Number>
会被擦除为:
// 擦除后的类
public class Box {
private Number value;
public Box(Number value) {
this.value = value;
}
public Number getValue() {
return value;
}
}
此时,T
被擦除为 Number
类型,而不是 Object
,因为我们给 T
指定了上界 extends Number
。
总结
泛型在Java中广泛用于提高代码的复用性、类型安全性和可维护性。它通过类型参数使得类、方法和接口能够处理不同类型的数据。在使用泛型时,通配符和上/下界通配符提供了更多的灵活性,使得代码更加通用和强大。