The Basics of Java Generics – Java泛型的基础知识

最后修改: 2016年 12月 9日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

JDK 5.0 introduced Java Generics with the aim of reducing bugs and adding an extra layer of abstraction over types.

JDK 5.0引入了Java Generics,目的是减少错误,并在类型上增加一个额外的抽象层。

This tutorial is a quick intro to Generics in Java, the goal behind them and how they can improve the quality of our code.

本教程是对Java中的泛型的快速介绍,其背后的目标以及它们如何提高我们的代码质量。

2. The Need for Generics

2.对仿制药的需求

Let’s imagine a scenario where we want to create a list in Java to store Integer.

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

We might try to write the following:

我们可以尝试写出以下内容。

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

Surprisingly, the compiler will complain about the last line. It doesn’t know what data type is returned.

令人惊讶的是,编译器会对最后一行进行投诉。它不知道返回的是什么数据类型。

The compiler will require an explicit casting:

编译器将要求明确的铸造。

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

There is no contract that could guarantee that the return type of the list is an Integer. The defined list could hold any object. We only know that we are retrieving a list by inspecting the context. When looking at types, it can only guarantee that it is an Object and therefore requires an explicit cast to ensure that the type is safe.

没有任何契约可以保证列表的返回类型是Integer。定义的列表可以容纳任何对象。我们只能通过检查上下文知道我们正在检索一个 list。在查看类型时,它只能保证是一个Object,因此需要显式的转换来保证类型的安全。

This cast can be annoying — we know that the data type in this list is an Integer. The cast is also cluttering our code. It can cause type-related runtime errors if a programmer makes a mistake with the explicit casting.

这种转换可能很烦人–我们知道这个列表中的数据类型是一个Integer。这种转换也使我们的代码变得混乱。如果程序员在显式转换中犯了错误,它可能会导致与类型相关的运行时错误。

It would be much easier if programmers could express their intention to use specific types and the compiler ensured the correctness of such types. This is the core idea behind generics.

如果程序员能够表达他们使用特定类型的意图,并且编译器能够确保这些类型的正确性,那就容易多了。这就是泛型背后的核心思想。

Let’s modify the first line of the previous code snippet:

让我们修改一下前面代码片断的第一行。

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

By adding the diamond operator <> containing the type, we narrow the specialization of this list to only Integer type. In other words, we specify the type held inside the list. The compiler can enforce the type at compile time.

通过添加包含类型的钻石运算符<>,我们将这个列表的专业化缩小到只有Integer类型。换句话说,我们指定了列表内持有的类型。编译器可以在编译时强制执行该类型。

In small programs, this might seem like a trivial addition. But in larger programs, this can add significant robustness and makes the program easier to read.

在小程序中,这可能看起来是一个微不足道的补充。但在大型程序中,这可以增加显著的稳健性,并使程序更容易阅读。

3. Generic Methods

3.通用方法

We write generic methods with a single method declaration, and we can call them with arguments of different types. The compiler will ensure the correctness of whichever type we use.

我们用一个方法声明来编写通用方法,我们可以用不同类型的参数来调用它们。无论我们使用哪种类型,编译器都会保证其正确性。

These are some properties of generic methods:

这些是通用方法的一些属性。

  • Generic methods have a type parameter (the diamond operator enclosing the type) before the return type of the method declaration.
  • Type parameters can be bounded (we explain bounds later in this article).
  • Generic methods can have different type parameters separated by commas in the method signature.
  • Method body for a generic method is just like a normal method.

Here’s an example of defining a generic method to convert an array to a list:

下面是一个定义通用方法的例子,将一个数组转换为一个列表。

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

The <T> in the method signature implies that the method will be dealing with generic type T. This is needed even if the method is returning void.

方法签名中的<T>意味着该方法将处理通用类型T。即使该方法返回无效,也需要这样做。

As mentioned, the method can deal with more than one generic type. Where this is the case, we must add all generic types to the method signature.

如前所述,该方法可以处理一个以上的通用类型。在这种情况下,我们必须将所有的泛型添加到方法签名中。

Here is how we would modify the above method to deal with type T and type G:

下面是我们如何修改上述方法以处理类型T和类型G

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

We’re passing a function that converts an array with the elements of type T to list with elements of type G.

我们要传递一个函数,将一个元素类型为T的数组转换为元素类型为G的列表。

An example would be to convert Integer to its String representation:

一个例子是将Integer转换为其String表示。

@Test
public void givenArrayOfIntegers_thanListOfStringReturnedOK() {
    Integer[] intArray = {1, 2, 3, 4, 5};
    List<String> stringList
      = Generics.fromArrayToList(intArray, Object::toString);
 
    assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
}

Note that Oracle recommendation is to use an uppercase letter to represent a generic type and to choose a more descriptive letter to represent formal types. In Java Collections, we use T for type, K for key and V for value.

请注意,Oracle的建议是使用大写字母来表示通用类型,并选择一个更具描述性的字母来表示正式类型。在Java集合中,我们用T表示类型,K表示key,V表示value。

3.1. Bounded Generics

3.1.有边界的泛型

Remember that type parameters can be bounded. Bounded means “restricted,” and we can restrict the types that a method accepts.

请记住,类型参数是可以被约束的。边界的意思是 “限制”,我们可以限制一个方法所接受的类型。

For example, we can specify that a method accepts a type and all its subclasses (upper bound) or a type and all its superclasses (lower bound).

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

To declare an upper-bounded type, we use the keyword extends after the type, followed by the upper bound that we want to use:

为了声明一个有上界的类型,我们在类型后面使用关键字extends,然后是我们要使用的上界。

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

We use the keyword extends here to mean that the type T extends the upper bound in case of a class or implements an upper bound in case of an interface.

我们在这里使用关键词extends来表示类型T在类的情况下扩展了上界,在接口的情况下实现了上界。

3.2. Multiple Bounds

3.2.多重界线

A type can also have multiple upper bounds:

一个类型也可以有多个上界。

<T extends Number & Comparable>

If one of the types that are extended by T is a class (e.g. Number), we have to put it first in the list of bounds. Otherwise, it will cause a compile-time error.

如果被T扩展的类型之一是一个类(例如Number),我们必须把它放在边界列表的第一位。否则,会引起编译时的错误。

4. Using Wildcards With Generics

4.在泛型中使用通配符

Wildcards are represented by the question mark ? in Java, and we use them to refer to an unknown type. Wildcards are particularly useful with generics and can be used as a parameter type.

通配符在Java中用问号?表示,我们用它来指代一个未知的类型。通配符在泛型中特别有用,可以作为一个参数类型使用。

But first, there is an important note to consider. We know that Object is the supertype of all Java classes. However, a collection of Object is not the supertype of any collection.

但首先,有一个重要的注意事项需要考虑。我们知道Object是所有Java类的超类型。然而,Object的集合不是任何集合的超类型。

For example, a List<Object> is not the supertype of List<String>, and assigning a variable of type List<Object> to a variable of type List<String> will cause a compiler error. This is to prevent possible conflicts that can happen if we add heterogeneous types to the same collection.

例如,List<Object>不是List<String>的超类型,将List<Object>类型的变量赋给List<String>类型的变量会引起编译器错误。这是为了防止我们在同一集合中添加异质类型时可能发生的冲突。

The same rule applies to any collection of a type and its subtypes.

同样的规则适用于一个类型的任何集合及其子类型。

Consider this example:

考虑一下这个例子。

public static void paintAllBuildings(List<Building> buildings) {
    buildings.forEach(Building::paint);
}

If we imagine a subtype of Building, such as a House, we can’t use this method with a list of House, even though House is a subtype of Building.

如果我们想象一个Building的子类型,例如House,我们不能用House的列表来使用这个方法,尽管HouseBuilding的一个子类型。

If we need to use this method with type Building and all its subtypes, the bounded wildcard can do the magic:

如果我们需要对Building类型和它的所有子类型使用这个方法,有界通配符可以发挥其作用。

public static void paintAllBuildings(List<? extends Building> buildings) {
    ...
}

Now this method will work with type Building and all its subtypes. This is called an upper-bounded wildcard, where type Building is the upper bound.

现在这个方法将对Building类型和它的所有子类型起作用。这被称为上界通配符,其中类型Building是上界。

We can also specify wildcards with a lower bound, where the unknown type has to be a supertype of the specified type. Lower bounds can be specified using the super keyword followed by the specific type. For example, <? super T> means unknown type that is a superclass of T (= T and all its parents).

我们还可以指定带有下限的通配符,其中未知的类型必须是指定类型的超类型。下限可以用super关键字来指定,后面是具体的类型。例如,<? super T>意味着未知类型是T的超类(=T和它的所有父类)。

5. Type Erasure

5.类型擦除

Generics were added to Java to ensure type safety. And to ensure that generics won’t cause overhead at runtime, the compiler applies a process called type erasure on generics at compile time.

泛型被添加到Java中以确保类型安全。为了确保泛型在运行时不会造成开销,编译器在编译时对泛型应用了一个叫做类型清除的过程。

Type erasure removes all type parameters and replaces them with their bounds or with Object if the type parameter is unbounded. This way, the bytecode after compilation contains only normal classes, interfaces and methods, ensuring that no new types are produced. Proper casting is applied as well to the Object type at compile time.

类型擦除会删除所有的类型参数,并用它们的边界代替,如果类型参数是无边界的,则用Object代替。这样,编译后的字节码只包含正常的类、接口和方法,确保不产生新的类型。在编译时,适当的铸造也被应用于Object类型。

This is an example of type erasure:

这是一个类型擦除的例子。

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

With type erasure, the unbounded type T is replaced with Object:

通过类型擦除,无界的类型T被替换为Object

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

// which in practice results in
public List withErasure(List list) {
    return list.stream().collect(Collectors.toList());
}

If the type is bounded, the type will be replaced by the bound at compile time:

如果类型是有界的,那么在编译时类型将被界所取代。

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

and would change after compilation:

并会在编译后改变。

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

6. Generics and Primitive Data Types

6.泛型和原始数据类型

One restriction of generics in Java is that the type parameter cannot be a primitive type.

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

For example, the following doesn’t compile:

例如,下面的内容无法编译。

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

To understand why primitive data types don’t work, let’s remember that generics are a compile-time feature, meaning the type parameter is erased and all generic types are implemented as type Object.

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

Let’s look at the add method of a list:

让我们看一下列表的add方法。

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

The signature of the add method is:

add方法的签名是。

boolean add(E e);

and will be compiled to:

并将其编译为。

boolean add(Object e);

Therefore, type parameters must be convertible to Object. Since primitive types don’t extend Object, we can’t use them as type parameters.

因此,类型参数必须可以转换为Object由于原始类型不扩展Object,我们不能将它们用作类型参数。

However, Java provides boxed types for primitives, along with autoboxing and unboxing to unwrap them:

然而,Java为基元提供了盒式类型,同时还有自动盒式和解盒式来解开它们

Integer a = 17;
int b = a;

So, if we want to create a list that can hold integers, we can use this wrapper:

因此,如果我们想创建一个可以容纳整数的列表,我们可以使用这个包装器。

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

The compiled code will be the equivalent of the following:

编译后的代码将相当于以下内容。

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

Future versions of Java might allow primitive data types for generics. Project Valhalla aims at improving the way generics are handled. The idea is to implement generics specialization as described in JEP 218.

未来的Java版本可能允许泛型的原始数据类型。项目Valhalla旨在改进处理泛型的方式。其想法是实现JEP 218中描述的泛型专用化。

7. Conclusion

7.结论

Java Generics is a powerful addition to the Java language because it makes the programmer’s job easier and less error-prone. Generics enforce type correctness at compile time and, most importantly, enable implementing generic algorithms without causing any extra overhead to our applications.

Java泛型是对Java语言的一个强有力的补充,因为它使程序员的工作更容易、更少出错。泛型在编译时强制执行类型的正确性,最重要的是,它能够实现泛型算法,而不会给我们的应用程序带来任何额外的开销。

The source code that accompanies the article is available over on GitHub.

伴随着这篇文章的源代码可以在GitHub上找到over