Java 泛型

Java 在 JDK5 中引入了 泛型 这一特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

泛型的必要性

让我们想象一个场景,我们想在 Java 中创建一个列表来存储Integer。

我们可能会尝试编写以下代码:

List list = new LinkedList();
list.add(new Integer(1)); 
Integer i = list.iterator().next();

上面的代码看似能满足我们的业务需要,实际上编译器是不认的。编译器会迷惑最后一行,它不知道返回什么数据类型。

为了要让编译器知道我们需要什么样的数据,我们需要做强制类型转换:

Integer i = (Integer) list.iterator.next();

此时,没有任何人能保证列表的返回类型是 Integer,list 列表可以包含任何对象,虽然通过阅读代码上下文,我们能确定此刻的类型转换是正确的,但是如果开发者在程序的编写过程中没有注意到类型转换的问题,那么错误只能等到运行时才会暴露出来。

让我们修改前面代码片段的第一行:

List<Integer> list = new LinkedList<>();

通过添加包含该类型的菱形运算符 <>,我们将该列表的范围缩小为仅整数类型。也就是说,我们指定了列表中保存的类型,编译器可以在编译时强制执行该类型。

在大型应用程序中,这可以显着增加稳健性并使程序更易于阅读。

泛型

Oracle 建议使用大写字母来表示泛型类型,并选择更具描述性的字母来表示正式类型。

标记符 说明 示例
E Element 一般用来表示集合中的元素
T Type 一般用来表示 Java 类
K Key 一般表示Map 的 key
V Value 一般表示Map 的 Value
N Number 一般表示数值
? 未知 一般表示不确定的 Java 类型

泛型方法

我们用一个方法声明编写泛型方法,我们可以用不同类型的参数调用它们。编译器将确保我们使用的任何类型的正确性。

这些是泛型方法的一些特点:

  • 泛型方法在方法声明的返回类型之前有一个类型参数(包围类型的菱形运算符)。
  • 类型参数可以是有界的(我们将在本文后面解释边界)。
  • 泛型方法可以在方法签名中有不同的类型参数,用逗号分隔。
  • 泛型方法的方法体就像普通方法一样。

下面的示例展示,如何将数组转换为列表:

public <T> List<T> fromArrayToList(T[] a) {   
    return Arrays.stream(a).collect(Collectors.toList());
}

方法签名中的 <T> 意味着该方法将处理泛型类型 T。即使方法返回 void,这也是必需的。

如果一个方法可以处理不止一种泛型类型,我们必须将所有泛型类型添加到方法签名中。

public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
    return Arrays.stream(a)
      .map(mapperFunction)
      .collect(Collectors.toList());
}

上面的方法中传递一个函数 Function,该函数能够将 T 类型元素的数组转换为 G 类型元素的列表。

有界泛型 Bounded Generics

类型参数是有界的。Bounded 表示“受限”,我们可以限制方法接受的类型。

例如,我们可以指定一个方法接受一个类型及其所有子类(上限)或一个类型及其所有超类(下限)。

要声明一个上限类型,我们在类型之后使用关键字extends,然后是我们要使用的上限:

public <T extends Number> List<T> fromArrayToList(T[] a) {
    ...
}

一个类型也可以有多个上限:

<T extends Number & Comparable>

如果由 T 继承的类型之一是类(例如Number),我们必须将它放在边界列表的首位。否则,将导致编译时错误。

类型擦除

将泛型添加到 Java 中是为了确保类型安全,并且为了确保泛型不会在运行时产生开销,编译器在编译时会对泛型进行擦除。

类型擦除删除所有类型参数并用它们的边界替换它们,如果类型参数是无界的,则用Object替换它们。这样编译后的字节码只包含普通的类、接口和方法,保证不会产生新的类型。

public <T> List<T> genericMethod(List<T> list) {
    return list.stream().collect(Collectors.toList());
}

上面这种无界的泛型,会被解析成下面的代码:

public List<Object> withErasure(List<Object> list) {
    return list.stream().collect(Collectors.toList());
}

上面的代码,对于编码器来说,相当于是:

public List withErasure(List list) {
    return list.stream().collect(Collectors.toList());
}

如果类型是有界的,则该类型将在编译时被边界替换:

public <T extends Animal> void genericMethod(T t) {
    ...
}

上面这种有界的泛型,会被解析成下面的代码:

public void genericMethod(Animal t) {
    ...
}

泛型和原始数据类型

Java 中泛型的一个限制是类型参数不能是原始类型。

也许你期待下面的代码能正确运行,实际上它根本无法编译。

List<int> list = new ArrayList<>();
list.add(22);

为了理解为什么原始数据类型不起作用,让我们记住泛型是一种编译时特性,这意味着类型参数被删除,所有泛型类型都实现为Object类型。

List<Integer> list = new ArrayList<>();
list.add(12);

上面的代码会被编译器认为 add() 方法是如下签名:

boolean add(E e);

也就是:

boolean add(Object e);

因此,类型参数必须可转换为Object。由于原始类型不是继承自 Object,我们不能将它们用作类型参数。

然而,Java 为原始基本类型提供了装箱类型,以及自动装箱和拆箱来处理它们:

List<Integer> list = new ArrayList<>();
list.add(315);
int first = list.get(0);

上面的代码相当于:

List list = new ArrayList<>();
list.add(Integer.valueOf(111));
int first = ((Integer) list.get(0)).intValue();

Java 泛型是对 Java 语言的强大补充,因为它使程序员的工作更轻松且不易出错。

泛型在编译时强制执行类型的正确性,最重要的是,这一切不会对我们的应用程序造成任何额外开销。

转载请注明出处:码谱记录 » Java 泛型
标签: