在 Java 编程中,大家或许都遭遇过令人头疼的ClassCastException
,尤其是在处理如Integer
、String
等不同类型对象时。这个异常通常是由于将对象强制转换为错误的数据类型所导致的。不过,Java 中的泛型可以帮助我们解决这一问题。
为什么我们需要泛型?
让我们从一个简单的例子开始。我们首先将不同类型的对象添加到一个ArrayList
中。然后打印它们的值。
java
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0);
System.out.println("String: " + str);
这里,我们向ArrayList
添加了一个String
对象。由于代码是自己编写,我们清楚元素类型,但编译器并不知晓。所以从列表获取值时得到的是Object
类型,必须进行显式强制转换。
java
list.add(123);
String number = (String) list.get(1);
System.out.println("Number: " + number);
如果我们向这个列表中添加一个Integer
并尝试获取该值,我们将得到一个ClassCastException
,因为Integer
对象不能被强制转换为String
。 而使用泛型,就能解决上述两个问题。使用菱形运算符明确指定列表中保存的对象类型,可实现编译时检查,无需显式强制转换。
java
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 无需显式强制转换
System.out.println("String: " + str);
list.add(123); // 抛出编译时错误
类型参数命名约定
在前面示例中,List<String>
的使用限制了列表可保存的对象类型。再看Box
类处理不同类型数据的示例:
java
public class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.setValue("Hello, world!");
System.out.println(stringBox.getValue());
Box<Integer> integerBox = new Box<>();
integerBox.setValue(123);
System.out.println(integerBox.getValue());
}
}
注意Box<T>
类的声明,这里T
是类型参数,表示Box
类可处理该类型的任意对象。在main
方法中创建Box<String>
和Box<Integer>
实例,确保了类型安全。
根据官方文档,类型参数名称通常为单个大写字母。常见的类型参数名称有:
E
- 元素(广泛用于 Java 集合框架)K
- 键N
- 数字T
- 类型V
- 值S
、U
、V
等 - 第二、第三、第四种类型
让我们看看如何编写一个泛型方法:
java
public static <T> void printArray(T[] inputArr) {
for (T element : inputArr) {
System.out.print(element + " ");
}
System.out.println();
}
这里,我们接受任何类型的数组并打印其元素。请注意,你需要在方法返回类型之前的尖括号<>
中指定泛型类型参数T
。方法体遍历我们作为参数传递的任何类型T
的数组,并打印每个元素。
java
public static void main(String[] args) {
// 创建不同类型的数组(Integer、Double和Character)
Integer[] intArr = {1, 2, 3, 4, 5};
Double[] doubleArr = {1.1, 2.2, 3.3, 4.4, 5.5};
Character[] charArr = {'H', 'E', 'L', 'L', 'O'};
System.out.println("Integer数组包含:");
printArray(intArr); // 传递一个Integer数组
System.out.println("Double数组包含:");
printArray(doubleArr); // 传递一个Double数组
System.out.println("Character数组包含:");
printArray(charArr); // 传递一个Character数组
}
我们可以通过传递不同类型的数组(Integer
、Double
、Character
)来调用这个泛型方法,你会看到你的程序将打印出这些数组的每个元素。
泛型的限制
在泛型中,我们使用边界来限制泛型类、接口或方法可以接受的类型。有两种类型:
1. 上界
这用于将泛型类型限制为上限。要定义上界,你使用extends
关键字。通过指定上界,你确保类、接口或方法接受指定的类型及其所有子类。 语法如下:<T extends SuperClass>
。例如,修改Box
类:
java
class Box<T extends Number> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
在这个例子中,T
可以是任何扩展Number
的类型,如Integer
、Double
或Float
。
2. 下界
这用于将泛型类型限制为下限。要定义下界,你使用super
关键字。通过指定下界,你确保类、接口或方法接受指定的类型及其所有超类。 语法如下:<T super SubClass>
。以下是使用下界的示例:
java
public static void printList(List<? super Integer> list) {
for (Object element : list) {
System.out.print(element + " ");
}
System.out.println();
}
下界<? super Integer>
的使用确保你可以将指定的类型及其所有超类(在这种情况下是Integer
、Number
或Object
的列表)传递给printList
方法。
什么是通配符?
你在上一个示例中看到的?
被称为通配符。你可以使用它们来引用未知类型。你可以使用带有上界的通配符,在这种情况下它看起来像这样:<? extends Number>
。它也可以与下界一起使用,如<? super Integer>
。
类型擦除
我们在类、接口或方法中使用的泛型类型仅在编译时可用,并且在运行时会被删除。这样做是为了确保向后兼容性,因为旧版本的Java(Java 1.5之前)不支持它。 编译器利用泛型类型信息确保类型安全。类型擦除过程如下:
-
对于有界泛型类型,编译器会将其擦除为它的上界类型。例如,
class Box<T extends Number>
,T
会被擦除为Number
。 -
对于无界泛型类型(如
class Box<T>
),T
会被擦除为Object
。所以在运行时,实际上并不能获取到泛型参数的具体类型信息。
java
import java.util.ArrayList;
import java.util.List;
class GenericExample<T> {
private List<T> list = new ArrayList<>();
public void add(T element) {
list.add(element);
}
public T get(int index) {
return list.get(index);
}
}
当编译器编译这段代码时,T
会被擦除。对于add
方法,实际上变成了类似public void add(Object element)
(如果T
是无界的)。对于get
方法,返回值类型也被擦除为Object
,不过编译器会在需要的时候插入强制类型转换。
结论
本文深入探讨了 Java 中的泛型概念及其使用方法,并给出了多个基本示例。理解和运用泛型能增强程序类型安全性,消除显式强制转换需求,使代码更具重用性和可维护性。希望通过本文的介绍,大家能在 Java 编程中更好地运用泛型,提升代码质量。