Java中的泛型(Generics)是指在定义类、接口或方法时,使用类型参数化的方式,允许在编译时指定类型,从而提供代码的重用性和类型安全性。泛型使得代码更加灵活和通用,同时能够在编译时进行类型检查,避免了许多运行时错误。
1. 泛型的基本概念
泛型主要用于类、接口和方法中,它的核心思想是通过类型参数化来定义数据类型。也就是说,你可以在声明时使用“占位符”来表示类型,等到实际使用时再指定具体的类型。
举个简单例子,假设你有一个类 Box
,你希望这个类能够存储不同类型的对象,但是不想为每种类型写一个不同的类。使用泛型后,你可以定义一个通用的Box
类,能够存储任意类型的对象。
// 定义一个泛型类
public class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
在上面的代码中,T
就是一个类型参数,它表示Box
可以存储任何类型的对象。T
可以在具体使用时指定,比如 Box<Integer>
表示存储Integer
类型,Box<String>
表示存储String
类型。
2. 泛型类
一个泛型类是使用泛型类型参数定义的类。例如,Box<T>
类定义了一个类型参数T
,这个参数可以在实例化对象时被替换成具体类型。
2.1 泛型类的定义
// 定义一个泛型类
public class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
2.2 创建泛型类的对象
你可以通过指定具体类型来创建泛型类的实例:
Box<Integer> integerBox = new Box<>();
integerBox.setValue(10); // 设置为Integer类型
Integer value = integerBox.getValue(); // 获取为Integer类型
Box<String> stringBox = new Box<>();
stringBox.setValue("Hello World"); // 设置为String类型
String str = stringBox.getValue(); // 获取为String类型
3. 泛型方法
泛型不仅可以用于类和接口,还可以用于方法。使用泛型方法,你可以在方法中指定类型参数,使方法能够处理不同类型的对象。
3.1 泛型方法的定义
泛型方法的定义与普通方法略有不同。在方法的返回类型前面加上类型参数。
// 定义一个泛型方法
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
在上面的代码中,<T>
指定了类型参数T
,T[]
表示该方法可以接收任何类型的数组。
3.2 调用泛型方法
调用泛型方法时,编译器会自动推断出类型参数,或者你也可以显式地指定类型。
Integer[] intArray = {1, 2, 3, 4};
printArray(intArray); // 编译器会推断出T为Integer
String[] strArray = {"Hello", "World"};
printArray(strArray); // 编译器会推断出T为String
你也可以显式地指定类型:
printArray(intArray); // <Integer>可以省略,编译器会自动推断
printArray(strArray); // <String>可以省略,编译器会自动推断
4. 泛型的类型限制(Bounded Types)
泛型不仅可以使用任意类型,还可以限制类型参数的范围。这通过使用上限通配符(extends
)来实现。你可以指定某个类型必须是某个类或接口的子类。
4.1 上限通配符
上限通配符用于限制类型参数,表示该类型参数必须是某个特定类的子类,或者实现了某个接口。
例如,假设你希望限制T
只能是Number
类或其子类的类型:
public static <T extends Number> void printNumber(T num) {
System.out.println(num);
}
在这个方法中,T
的类型被限制为Number
类或它的子类。你只能传入Number
、Integer
、Double
等类型。
4.2 使用上限通配符的示例
public class BoundedTypes {
public static <T extends Number> void printNumber(T num) {
System.out.println(num);
}
public static void main(String[] args) {
printNumber(10); // Integer 类型
printNumber(3.14); // Double 类型
}
}
4.3 下限通配符
下限通配符(super
)允许你指定一个类型的父类。例如:
public static void addNumbers(List<? super Integer> list) {
list.add(10); // 可以添加 Integer 或其子类
}
在这个方法中,list
可以是Integer
、Number
,或者Object
类型,但不能是Double
或String
类型等。
5. 通配符(Wildcard)
通配符(?
)用于表示任意类型,它可以与上限或下限配合使用,以提供更加灵活的类型绑定。
5.1 上限通配符(? extends T
)
表示类型是T
或T
的子类型。
public static void printNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num);
}
}
此方法可以接受List<Number>
、List<Integer>
、List<Double>
等类型的列表。
5.2 下限通配符(? super T
)
表示类型是T
或T
的父类型。
public static void addNumbers(List<? super Integer> list) {
list.add(10); // 可以添加 Integer 或其父类(如 Number)
}
此方法可以接受List<Object>
、List<Number>
、List<Integer>
等类型的列表,但无法接受List<Double>
等类型。
5.3 无通配符(?
)
通配符表示“任意类型”,通常用于获取数据而不是修改数据。
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
6. 泛型的类型擦除(Type Erasure)
Java泛型是通过类型擦除(Type Erasure)机制实现的。泛型在编译时会被替换成原始类型(raw type)。这意味着泛型类型参数在编译后不再存在,所有的类型参数都会被替换为Object
(或指定的上限类型)。这使得泛型代码在运行时的性能不会受到影响。
例如,泛型类Box<T>
,在编译后会被转换成:
public class Box {
private Object value;
public void setValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
虽然在编译时,Box
是一个泛型类,但在运行时,它实际上是一个普通的类,只不过编译器会进行类型检查来确保类型安全。
7. 泛型的优缺点
优点:
- 类型安全:泛型使得编译器能够检查类型,在编译时就能发现类型错误。
- 代码重用性:可以编写通用的代码,不需要为每个类型单独编写类或方法。
- 提高可读性:泛型方法和类明确了类型的意图,代码更加简洁、清晰。
缺点:
- 类型擦除:在运行时,泛型信息被擦除,因此无法直接使用泛型类型信息进行反射等操作。
- 无法创建泛型数组:Java不支持创建泛型数组。例如,
new T[10]
是不合法的,因为编译后,T
会被擦除为Object
,导致类型不明确。
8. 总结
- 泛型允许类、接口、方法使用类型参数化,提供了强类型检查和代码重用性。
- 泛型支持上限和下限通配符,允许更加灵活的类型控制。
- 类型擦除是Java泛型实现的机制,使得泛型在运行时不再保持类型信息,但依然能提供类型安全。
- 使用泛型可以提高代码的可读性、可维护性,并减少类型转换错误。