泛型类型(Generic Type)

泛型类型是参数化类型的泛型类或接口。下面是一个 Box 类例子来说明这个概念。

一个简单的 Box 类

  1. public class Box {
  2. private Object object;
  3. public void set(Object object) {
  4. this.object = object;
  5. }
  6. public Object get() {
  7. return object;
  8. }
  9. }

由于它的方法接受或返回一个 Object,你可以自由地传入任何你想要的类型,只要它不是原始的类型之一。在编译时,没有办法验证如何使用这个类。代码的一部分可以设置 Integer 并期望得到 Integer ,而代码的另一部分可能会由于错误地传递一个String ,而导致运行错误。

一个泛型版本的 Box 类

  1. // 泛型类定义语法
  2. class name<T1, T2, ..., Tn> { /* ... */ }

类型参数部分用 <> 包裹,制定了类型参数或称为类型变量(type parameters or type variables) T1, T2, …, 直到 Tn.

  1. public class Box<T> {
  2. // T stands for "Type"
  3. private T t;
  4. public void set(T t) {
  5. this.t = t;
  6. }
  7. public T get() {
  8. return t;
  9. }
  10. }

主要,所有的 Object 被 T 代替了。类型变量可以是非基本类型的的任意类型,任意的类、接口、数组或其他类型变量。

这个技术同样适用于泛型接口的创建。

类型参数命名规范

按照惯例,类型参数名称是单个大写字母,用来区别普通的类或接口名称。

常用的类型参数名称如下:

  1. E - Element (由 Java 集合框架广泛使用)
  2. K - Key
  3. N - Number
  4. T - Type
  5. V - Value
  6. S,U,V 等. - 第二种、第三种、第四种类型

调用和实例化一个泛型

从代码中引用泛型 Box 类,则必须执行一个泛型调用(generic type invocation),用具体的值,比如 Integer 取代 T :

  1. Box<Integer> integerBox;

泛型调用与普通的方法调用类似,所不同的是传递参数是类型参数(type argument ),本例就是传递 Integer 到 Box 类

Type Parameter 和 Type Argument 区别 编码时,提供 type argument 的一个原因是为了创建 参数化类型。因此,Foo<T> 中的 T 是一个 type parameter, 而 Foo<String> 中的 String 是一个 type argument

与其他变量声明类似,代码实际上没有创建一个新的 Box 对象。它只是声明integerBox 在读到 Box<Integer> 时,保存一个“Integer 的 Box”的引用。

泛型的调用通常被称为一个参数化类型(parameterized type)。

实例化类,使用 new 关键字:

  1. Box<Integer> integerBox = new Box<Integer>();

菱形(Diamond)

Java SE 7 开始泛型可以使用空的类型参数集<>,只要编译器能够确定,或推断,该类型参数所需的类型参数。这对尖括号<>,被非正式地称为“菱形(diamond)”。例如:

  1. Box<Integer> integerBox = new Box<>();

多类型参数

下面是一个泛型 Pair 接口和一个泛型 OrderedPair :

  1. public interface Pair<K, V> {
  2. public K getKey();
  3. public V getValue();
  4. }
  5. public class OrderedPair<K, V> implements Pair<K, V> {
  6. private K key;
  7. private V value;
  8. public OrderedPair(K key, V value) {
  9. this.key = key;
  10. this.value = value;
  11. }
  12. public K getKey() { return key; }
  13. public V getValue() { return value; }
  14. }
  15. // 创建两个 OrderedPair 实例
  16. OrderedPair<String, Integer> p1;
  17. OrderedPair<String, String> p2;
  18. p1 = new OrderedPair<String, Integer>("Even", 8);
  19. p2 = new OrderedPair<String, String>("hello", "world");
  20. // 使用菱形(diamond)来简化代码
  21. p1 = new OrderedPair<>("Even", 8);
  22. p2 = new OrderedPair<>("hello", "world");

参数化类型

您也可以用 参数化类型(例如,List<String>的)来替换类型参数(即 K 或 V )。例如,使用OrderedPair<K,V>例如:

  1. OrderedPair<String, Box<Integer>> p ;
  2. p = new OrderedPair<>("primes", new Box<Integer>(...));

原生类型(Raw Types)

原生类型是没有类型参数(type arguments)的泛型类和泛型接口,如泛型 Box 类;

  1. public class Box<T> {
  2. public void set(T t) { /* ... */ }
  3. // ...
  4. }

要创建参数化类型的Box<T>,需要为形式类型参数T提供实际的类型参数:

  1. Box<Integer> intBox = new Box<>();

如果想省略实际的类型参数,则需要创建一个Box<T>的原生类型:

  1. Box rawBox = new Box();

因此,Box是泛型Box<T>的原生类型。但是,非泛型的类或接口类型不是原始类型。

JDK为了保证向后兼容,允许将参数化类型分配给其原始类型:

  1. Box<String> stringBox = new Box<>();
  2. Box rawBox = stringBox; // OK

但如果将原始类型与参数化类型进行管理,则会得到告警:

  1. Box rawBox = new Box(); // rawBox is a raw type of Box<T>
  2. Box<Integer> intBox = rawBox; // warning: unchecked conversion

如果使用原始类型调用相应泛型类型中定义的泛型方法,也会收到警告:

  1. Box<String> stringBox = new Box<>();
  2. Box rawBox = stringBox;
  3. rawBox.set(8); // warning: unchecked invocation to set(T)

警告显示原始类型绕过泛型类型检查,将不安全代码的捕获推迟到运行时。因此,开发人员应该避免使用原始类型。

泛型方法(Generic Method)

泛型方法是引入其自己的类型参数的方法。这类似于声明泛型类型,但类型参数的范围仅限于声明它的方法。允许使用静态和非静态泛型方法,以及泛型类构造函数。

泛型方法的语法包括一个类型参数列表,在尖括号内,它出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。

下面例子中,Util类包含一个泛型方法compare,用于比较两个Pair对象:

  1. public class Util {
  2. public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
  3. return p1.getKey().equals(p2.getKey()) &&
  4. p1.getValue().equals(p2.getValue());
  5. }
  6. }
  7. public class Pair<K, V> {
  8. private K key;
  9. private V value;
  10. public Pair(K key, V value) {
  11. this.key = key;
  12. this.value = value;
  13. }
  14. public void setKey(K key) { this.key = key; }
  15. public void setValue(V value) { this.value = value; }
  16. public K getKey() { return key; }
  17. public V getValue() { return value; }
  18. }

以下是方法的调用:

  1. Pair<Integer, String> p1 = new Pair<>(1, "apple");
  2. Pair<Integer, String> p2 = new Pair<>(2, "pear");
  3. boolean same = Util.<Integer, String>compare(p1, p2);

其中,compare方法的类型通常可以省略,因为编译器将推断所需的类型:

  1. Pair<Integer, String> p1 = new Pair<>(1, "apple");
  2. Pair<Integer, String> p2 = new Pair<>(2, "pear");
  3. boolean same = Util.compare(p1, p2);

泛型的继承和子类型

在Java中,只要类型兼容,就可以将一种类型的对象分配给另一种类型的对象。例如,可以将Integer分配给Object,因为Object是Integer的超类之一:

  1. Object someObject = new Object();
  2. Integer someInteger = new Integer(10);
  3. someObject = someInteger; // OK

在面向对象的术语中,这种关系被称为“is-a”。 由于Integer是一种Object,因此允许赋值。但是Integer同时也是一种Number,所以下面的代码也是有效的:

  1. public void someMethod(Number n) { /* ... */ }
  2. someMethod(new Integer(10)); // OK
  3. someMethod(new Double(10.1)); // OK

在泛型中也是如此。 可以执行泛型类型调用,将Number作为其类型参数传递,如果参数与Number兼容,则允许任何后续的add调用:

  1. Box<Number> box = new Box<Number>();
  2. box.add(new Integer(10)); // OK
  3. box.add(new Double(10.1)); // OK

现在考虑下面的方法:

  1. public void boxTest(Box<Number> n) { /* ... */ }

通过查看其签名,可以看到上述方法接受一个类型为Box<Number>的参数。
也许你可能会想当然的认为这个方法也能接收Box<Integer>Box<Double>吧?
答案是否定的,因为Box<Integer>Box<Double>并不是Box<Number>的子类型。
在使用泛型编程时,这是一个常见的误解,虽然Integer和Double是Number的子类型。

下图展示了泛型和子类型的之间的关系:

image.png

泛型类及子类

可以通过扩展或实现泛型类或接口来对其进行子类型化。一个类或接口的类型参数与另一个类或参数的类型参数之间的关系由extends和implements子句确定。

以Collections类为例,ArrayList<E>实现了List<E>,而List<E>扩展了Collection<E>。所以ArrayList<String>List<String>的子类型,同时它也是Collection<String>的子类型。只要不改变类型参数,就会在类型之间保留子类型关系。下图展示了这些类的层次关系:

image.png

现在假设我们想要定义我们自己的列表接口PayloadList,它将泛型类型P的可选值与每个元素相关联。它的声明可能如下:

  1. interface PayloadList<E,P> extends List<E> {
  2. void setPayload(int index, P val);
  3. ...
  4. }

以下是PayloadList参数化的List<String>的子类型:

  • PayloadList
  • PayloadList
  • PayloadList

这些类的关系图如下:

image.png