前言

本章主要内容摘录自 Java 核心技术 卷1 基础知识,主要是泛型的一些基础知识。

正文

泛型类

泛型类(generic class)就是具有一个或多个类型变量的类。下面给出一个简单的泛型类例子:

  1. public class Pair<T> {
  2. private T first;
  3. private T second;
  4. public Pair() {
  5. first = null;
  6. second = null;
  7. }
  8. public Pair(T first, T second) {
  9. this.first = first;
  10. this.second = second;
  11. }
  12. public T getFirst() {
  13. return first;
  14. }
  15. public void setFirst(T first) {
  16. this.first = first;
  17. }
  18. public T getSecond() {
  19. return second;
  20. }
  21. public void setSecond(T second) {
  22. this.second = second;
  23. }
  24. }

Pair 类引入了一个类型变量 T,用尖括号(<>)括起来,并放在类名的后面。

泛型类可以有多个类型变量。例如,可以定义 Pair 类,其中第一个域和第二个域使用不同的类型:public class Pair<T, U> {...}

泛型类中定义的类型变量可以用来指定方法的返回类型以及域和局部变量的类型。

:::info 类型变量使用大写形式,且比较短。一般,使用变量 E 表示集合的元素类型,K 和 V 分别表示表的关键字与值的类型。T(需要时还可以用临近的字母 U 和 S)表示“任意类型”。 :::

泛型类的实例化比较简单,使用具体的类型替换类型变量就可以实现。例如:Pair<String> pair = new Pair<>();

泛型方法

泛型方法就是定义一个带有类型参数的简单方法。下面给出一个简单的泛型方法例子:

  1. public class ArrayAlg {
  2. public static <T> T getMiddle(T... a) {
  3. return a[a.length / 2];
  4. }
  5. }

注意:类型变量放在修饰符(这里是 public static)的后面,返回类型的前面。

泛型方法可以定义在普通类中,也可以定义在泛型类中。

当调用一个泛型方法时,在方法名前的尖括号中放人具体的类型:String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");,但是,一般我们在调用泛型方法的时候不需要显示的指定类型参数,编译器可以根据上下文推断出所调用的方法使用的真实类型,比如:当我们传入 getMiddle 方法的是一批字符串数据,则编译器会使用实参的类型 String[] 与泛型类型 T[] 进行匹配并推断出 T 一定是 String。也就是说,可以这样调用:String middle = ArrayAlg.getMiddle("John", "Q.", "Public");

大多数情况下,编译器都能够推断出真实类型。但是,当我们传入到 getMiddle 方法中的参数存在类型不一致的情况时,编译器也会提示错误。看一看下面这个例子:

  1. double middle = ArrayAlg.getMiddle(3.14, 1729, 0);

image.png
编译器将会自动打包参数为 1 个 Double 和 2 个 Integer 对象,而后寻找这些类的共同超类型。事实上,找到 2 个这样的超类型:Number 和 Comparable 接口,即表示可以将结果赋给两种类型。所以,这里建议将所有的参数都写成 double 值。

类型变量的限定

有时,类或方法需要对类型变量加以约束。比如下面这个例子,我们要计算数组中的最小元素:

  1. public class ArrayAlg {
  2. public static <T extends Comparable> T min(T[] a) { // almost correct
  3. if (a == null || a.length == 0) return null;
  4. T smallest = a[0];
  5. for (int i = 1; i < a.length; i++)
  6. if (smallest.compareTo(a[i]) > 0) smallest = a[i];
  7. return smallest;
  8. }
  9. }

image.png
但是,编译器提示了一个问题,变量 smallest 类型为 T,这意味着它可以是任何一个类的对象。怎么才能确信 T 所属的类有 compareTo 方法呢?

解决这个问题的方案是将 T 限制为实现了 Comparable 接口的类:public static <T **extends Comparable**> T min(T[] a) ...。在讲到通配符类型的时候会进一步扩展该方法,使其更加通用。

另外,一个类型变量可以有多个限定,例如:T extends Comparable & Serializable, U extends Comparable,限定类型用“&”分隔,而逗号用来分隔类型变量。

在 Java 的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。

泛型类型的继承规则

这里只介绍部分规则,还有一部分规则在介绍通配符类型的时候说明。泛型类型的继承规则用一幅图基本就可以描述清楚:
泛型一 - 图3
如上图所示,泛型类可以扩展或实现其他的泛型类。例如,ArrayList 类实现 List 接口。这意味着,一个 ArrayList 可以被转换为一个 List。但是,一个 ArrayList 不是一个 ArrayList 或 List,虽然 Manager 类继承了 Employee。

另外,永远可以将参数化类型转换为一个原始类型。例如,可以这样写:

  1. ArrayList<Manager> managers = new ArrayList<>();
  2. List raws = managers;
  3. raws.add("Manager");

如上所示,转换成原始类型之后,会失去泛型程序设计提供的附加安全性。raws 在添加新的元素的时候不会再检查元素的类型,当 raws 添加一个 “Manager” 字符串的时候,会抛出 ClassCastException 异常。

通配符类型

我们先来看泛型的一些概念:

  • ArrayList 中的 E 称为类型参数变量。
  • ArrayList 中的 Integer 称为实际类型参数。
  • 整个 ArrayList 称为泛型类型。
  • 整个 ArrayList 称为参数化的类型(ParameterizedType)。

通配符的作用:

  1. 通配符是用来解决泛型无法协变的问题的。
    1. 协变指的是如果 Child 是 Parent 的子类,那么 List 也应该是 List 的子类。但是,根据泛型类型的继承规则,我们知道泛型是不支持协变的。
  2. 泛型 T 是一个确定的类型,而通配符则更为灵活或者说是不确定。
  3. 通配符不是类型参数变量,或者说通配符和类型参数变量 T 不是一类东西。
    1. 你可以理解成泛型 T 就像是一个变量,等着你将来传一个具体的类型使用的,而通配符则是一种规定,规定你能传哪些参数。

通配符的使用方式:

  1. 通配符后面只允许有一个限定,例如Pair<? extends Employee>orPair<? super Manager>
  2. 通配符主要用于的变量声明以及形参列表,不能用来定义泛型类、泛型接口、泛型方法。
  3. 在实例化泛型类的时候也不可以使用通配符,例如new Pair<?>()

    通配符的子类型限定

通配符有两种类型的限定,一种是子类型限定:? extends Employee,另一种是超类型限定:? super Manager。接下来分别介绍这两种类型的使用场景。

指定一个子类型限定:? extends Employee,这个通配符限制为 Employee 的所有子类型。

假设我们要编写一个打印雇员对信息的方法:

  1. public static void printBuddies(Pair<Employee> p) {
  2. Employee first = p.getFirst();
  3. Employee second = p.getSecond();
  4. System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
  5. }

正如前面讲到的,不能将 Pair 传递给这个方法。解决的方法很简单,使用通配符类型:public static void printBuddies(Pair<? extends Employee> p)

这里补充泛型类型的继承规则,类型Pair<Manager>Pair<? extends Employee>的子类型:
泛型一 - 图4
使用通配符的子类型限定虽然解决了泛型协变的问题,但是这样也对泛型类的写操作产生限制,即只可以取值,不可以设值。

例如,如下代码,编译器会提示错误。

  1. Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
  2. Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK
  3. wildcardBuddies.setFirst(lowlyEmployee); // compile-time error

当我们使用类型Pair<? extends Employee>的时候,其方法似乎是这样的:

  1. ? extends Employee getFirst()
  2. void setFirst(? extends Employee e)

编译器只知道 setFirst 方法需要一个 Employee 的子类型,具体什么类型不知道,所以它拒绝传递任何特定的类型。而使用 getFirst 方法不存在这个问题,因为 getFirst 方法的返回值肯定可以赋给一个 Employee 的引用。

通配符的超类型限定

接着,我们介绍另外一种类型,指定一个超类型限定(super type bound):? super Managere,这个通配符限制为 Manager 的所有超类型。

案例:有一个经理的数组,想把奖金最高和最低的经理放在一个 Pair 对象中。在这里,Pair 是合理的, Pair 也是合理的,下面的方法将可以接受任何适当的 Pair:

  1. public static void minmaxBonus(Manager[] a, Pair<? super Manager> result) {
  2. if (a.length == 0) return;
  3. Manager min = a[0];
  4. Manager max = a[0];
  5. for (int i = 1; i < a.length; i++) {
  6. if (min.getBonus() > a[i].getBonus()) min = a[i];
  7. if (max.getBonus() < a[i].getBonus()) max = a[i];
  8. }
  9. result.setFirst(min);
  10. result.setSecond(max);
  11. }

minmaxBonus 方法的第二个参数没有写成Pair<Manager> result,而是使用通配符的方式,使得传参更为灵活。

根据上面的案例,补充泛型类型的继承规则,类型Pair<Employee>Pair<? super Manager>的子类型:
泛型一 - 图5
同样的,使用通配符的超类型限定也有缺陷,它对泛型类的读操作产生限制,即只可以设值,不可以取值(也可以取值,只是 getFirst 方法只能赋给一个 Object)。

例如,当我们使用类型Pair<? super Manager>的时候,假设其方法是这样的:

  1. void setFirst(? super Manager e)
  2. ? super Manager getFirst()

这不是真正的 Java 语法,但是可以看出编译器知道些什么。编译器无法知道 setFirst 方法中参数的具体类型,只知道类型限定为 Manager 的父类,所以调用这个方法的时候不能传 Manager 的父类,比如类型为 Employee 或 Object 的参数,只能传 Manager 类型的对象,或者其子类型对象。另外,如果调用 getFirst,不能保证返回对象的类型,只能把它赋给一个 Object。

下面介绍超类型限定的另一种应用,是对类型变量的限定中介绍的例子的补充。因为 Comparable 接口本身就是一个泛型类型。声明如下:

  1. public interface Comparable<T> {
  2. public int compareTo(T o);
  3. }

所以我们可以对 ArrayAIg 类的 min 方法做如下优化:

  1. public static <T extends Comparable<T>> T min(T[] a)......

这样,如果需要计算一个 String 数组的最小值时,T 就是 String 类型,而 String 是 Comparable 的子类型。但是,如果处理的是一个 LocalDate 数组时,会出现一个问题。LocalDate 实现了 ChronoLocalDate, 而 ChronoLocalDate 扩展了 Comparable。因此, LocalDate 实现的是 Comparable 而不是 Comparable

针对这种情况,通配符的超类型限定就派上用场了:

  1. public static <T extends Comparable<? super T>> T min(T[] a)......

现在 compareTo 方法写成:int compareTo(? super T)

有可能被声明为使用类型 T 的对象,也有可能使用 T 的超类型,这样无论如何,传递一个 T 类型的对象给 compareTo 方法都是安全的。

还有一种常见的用法,作为一个函数式接口的参数类型,例如,Collection 接口有一个方法:default boolean removeIf(Predicate<? super E> filter)

这个方法会删除所有满足给定谓词条件的元素。例如,如果你不喜欢有奇怪散列码的员工,就可以如下将他们删除:

  1. ArrayList<Employee> staff = ...;
  2. Predicate<Object> oddHashCode = obj -> obj.hashCode() % 2 != 0;
  3. staff.removeIf(oddHashCode);

你希望传入一个 Predicate,而不只是 Predicate。Super 通配符可以使这个愿望成真。

无限定通配符

无限定通配符的格式:?,不限制任何类型。例如,Pair<?>,假设类型 Pair<?> 有以下方法:

  1. ? getFirst()
  2. void setFirst(?)

getFirst 的返回值只能赋给一个 Object,setFirst 方法不能被调用,甚至不能用 Object 调用。

Pair<?> 和原始 Pair 类型的本质不同在于:可以用任意 Object 对象调用原始 Pair 类的 setObject 方法。

无限定通配符有什么用呢?它对于许多简单的操作非常有用。例如,下面这个方法将用来测试一个 pair 是否包含一个 null 引用,它不需要实际的类型。

  1. public static boolean hasNulls(Pair<?> p) {
  2. return p.getFirst() == null || p.getSecond() == null;
  3. }

参考

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/etmrkg 来源:殷建卫 - Java 开发笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。