Java泛型的深度指南

一本全面介绍Java泛型的指南,涵盖泛型方法、泛型类、通配符、类型擦除、增强等内容。
On this page

Java泛型的深度指南

Java 泛型介绍

Java 5 引入了泛型,允许在不同类型之间进行类型安全的代码和算法的重用。这被称为Java 泛型。在泛型出现之前,我们必须使用原始类型,如 List 和 Map,这需要显式转换并容易出现运行时错误。

Java 泛型为类型添加了额外的抽象层,使我们能够编写灵活和可重用的代码,可以安全地在多个类型之间工作。在这个全面的指南中,我们将涵盖关于 Java 泛型的所有内容,包括泛型方法、泛型类、通配符、类型擦除等等。

Java 泛型的必要性

让我们首先了解为什么首先需要泛型。考虑下面使用原始 List 类型的代码:

1List list = new LinkedList();
2list.add(new Integer(1));
3
4Integer i = (Integer) list.iterator().next(); // explicit cast required

我们必须明确将由 list 返回的元素转换为 Integer,即使我们知道 list 包含的是整数。这是因为编译器只知道 list 在编译时是 Object 类型。

为了避免这种情况,我们可以使用泛型来指定元素类型,如下所示:

1List<Integer> list = new LinkedList<>();
2list.add(1);
3
4Integer i = list.iterator().next(); // no cast required

泛型 Java的 List让我们避免了转换并确保了编译时的类型安全性。这是一个简单的例子,用于说明其好处,但对于更大的程序,泛型 Java显著提高了代码重用性和可读性。

通用方法

我们可以通过在返回类型之前的尖括号内声明类型参数来定义适用于不同类型的通用方法:

1public static <T> void printList(List<T> list) {
2   for(T elem : list) {
3      System.out.println(elem);
4   }
5}

这个 printList 方法可以打印任何类型的列表,因为 T 代表元素类型。以下是一个示例调用:

1List<String> fruits = new ArrayList<>();
2fruits.add("apple");
3fruits.add("banana");
4
5printList(fruits); // prints the string elements

我们可以有多个类型参数,如<T, U>,并在方法签名中使用它们。编译器将强制执行传递的实际类型的一致性。

有界类型参数

类型参数可以通过使用 extends 和 super 关键字来限制只允许某些类型:

1public static <T extends Number> double sum(List<T> nums) {
2   double total = 0.0;
3
4   for(T num : nums) {
5      total += num.doubleValue();
6   }
7   return total;
8}

指定 T 只能是 Number 或 Number 的子类。这允许我们在方法内部调用 num.doubleValue()。

多个边界也是可能的,如<T extends A & B>。如果存在,则第一个边界必须是一个类。

使用通配符

通配符用?表示,当编写可以处理不同类型的方法时非常有用。考虑以下方法:

1public static void printBuildings(List<Building> buildings) {
2  // ...
3}

我们可以使用通配符将其泛化为适用于 Building 子类(如 House)的方法:

1public static void printBuildings(List<? extends Building> buildings) {
2 // ...
3}

? extends Building 表示 Building 的任何子类型。通配符提供了我们接受的类型的灵活性。上界通配符将类型限制为类型或其子类型。

我们还有下界通配符,如? super Building,它接受该类型或其超类型。

通用类

除了方法之外,我们还可以声明具有类型参数的整个类:

 1public class Box<T> {
 2
 3  private T contents;
 4
 5  public void set(T item) {
 6     this.contents = item;
 7  }
 8
 9  public T get() {
10     return contents;
11  }
12}

Box声明了一个将在类内部使用的类型 T。现在我们可以创建具有正确类型的实例,如 Box和 Box

1Box<Integer> intBox = new Box<>();
2intBox.set(5);
3
4System.out.println(intBox.get()); // prints 5

generictype java Box 类确保类型安全,并避免了显式转换的需要。

类型擦除

Java 中的泛型使用一种称为type erasure的技术,在运行时实现泛型行为。在编译时,所有类型参数都会被擦除,并替换为它们的边界类型,如果没有指定边界,则替换为 Object。

这确保与现有的 JVM 指令兼容,并避免引入新的运行时类型。下面是一个擦除的例子:

1// Before erasure
2public static <T> void method(T arg) {
3  // ...
4}
5
6// After erasure
7public static void method(Object arg) {
8  // ...
9}

T 被替换为 Object。同样,字段和方法中的任何泛型类型都会被擦除以保持二进制兼容性。

泛型和原始类型

generictype java的一个限制是类型参数不能是原始类型,如 int、double 等。

1List<int> nums; // compilation error

这是因为泛型在运行时使用类型擦除到 Object。由于原始类型不是对象,所以不允许它们作为类型参数。

我们可以使用原始包装类,如 Integer 和 Double:

1List<Integer> nums = new ArrayList<>();
2nums.add(42); // autoboxing converts 42 to Integer
3
4int num = nums.get(0); // unboxing extracts int value

自动装箱和拆箱有助于弥合这个差距。未来的 Java 版本可能会包括专门化和实例化,以允许原始类型参数。

Java 7 及以上版本中的泛型 Java 增强

Java 7 和 8 中对泛型进行了一些重大改进:

  • 钻石操作符用于构造函数类型推断
  • 更好的目标类型和泛型分析
  • 用于改进分析的类型注解
  • 引入了方法句柄进行泛型调用

例如,我们可以使用:

1Map<String, List<String>> myMap = new HashMap<>();

而不必在右边重复类型参数。总体上,泛型已经在新的 Java 版本中得到了改进和优化。

结论:Java 泛型

Java 泛型允许代码重用,并且可以以灵活的方式编写适用于多种类型的算法。它们在编译时提供类型安全性,而不会产生运行时的开销。泛型方法、通配符、边界和类型擦除等概念为泛型编程提供了强大的结构。

虽然 Java 泛型存在一些限制,比如缺乏基本类型支持,但在不同的版本中已经进行了改进。泛型显著提高了类型安全性,并减少了与原始类型和类型转换相关的错误。

要了解更多详细信息,请参阅Oracle 泛型教程