为什么要使用泛型程序设计
泛型程序设计意味着编写的代码可以对多种不同类型的对象重用。
谁想成为泛型程序员
使用像 ArrayList的泛型类很容易。大多数Java程序员都使用 ArrayList
但是,实现一个泛型类并没有那么容易。对于类型参数,使用这段代码的程序员可能想要内置( plug in)所有的类。他们希望在没有过多的限制以及混乱的错误消息的状态下,做所有的事情。因此,一个泛型程序员的任务就是预测出所用类的未来可能有的所有用途。
这一任务难到什么程度呢?下面是标准类库的设计者们肯定产生争议的一个典型问题。 ArrayList类有一个方法addAll用来添加另一个集合的全部元素。程序员可能想要将 ArrayList
泛型程序设计划分为3个能力级别。基本级别是,仅仅使用泛型类——典型的是像 ArrayList这样的集合——不必考虑它们的工作方式与原因。大多数应用程序员将会停留在这一级别上,直到出现了什么问题。当把不同的泛型类混合在一起时,或是在与对类型参数一无所知的遗留的代码进行衔接时,可能会看到含混不清的错误消息。如果这样的话,就需要学习Java泛型来系统地解决这些问题,而不要胡乱地猜测。当然,最终可能想要实现自己的泛型类与泛型方法。
应用程序员很可能不喜欢编写太多的泛型代码。JDK开发人员已经做出了很大的努力,为所有的集合类提供了类型参数。凭经验来说,那些原本涉及许多来自通用类型(如 Object或 Comparable接口)的强制类型转换的代码一定会因使用类型参数而受益。
定义简单泛型
泛型类就是有一个或多个类型变量的类。
注释:类型变量使用大写形式,且比较短,这是很常见的。在ava库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型。T(需要时还可以用临近的字母U和S)表示“任意类型”。
泛型方法
定义一个带有类型参数的简单方法。
1 | class ArrayAlg |
这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法,可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符(这里是 public static)的后面,返回类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:
1 | String middle ArrayAlg. <String> ("John", "Q", "Public"); |
在这种情况(实际也是大多数情况)下,方法调用中可以省略
1 | String middle ArrayAlg. getMiddle("John", "Q.", "Public"); |
几乎在大多数情况下,对于泛型方法的类型引用没有问题。偶尔,编译器也会提示错误,此时需要解译错误报告。看一看下面这个示例:
1 | double middle= ArrayAlg.getMiddle(3.14,1729,0); |
错误消息会以晦涩的方式指出(不同的编译器给出的错误消息可能有所不同):解释这句代码有两种方法,而且这两种方法都是合法的。简单地说,编译器将会自动打包参数为1个 Double和2个 Integer对象,而后寻找这些类的共同超类型。事实上;找到2个这样的超类型: Number和 Comparable接口,其本身也是一个泛型类型。在这种情况下,可以采取的补救措施是将所有的参数都写为double值。
类型限定的变量
1 | 有时,类或方法需要对类型变量加以约束。下面是一个典型的例子。我们要计算数组中的最小元素: |
但是,这里有一个问题。请看一下min方法的代码内部。变量 smallest类型为T,这意味着它可以是任何一个类的对象。怎么才能确信T所属的类有 compareTo方法呢?
解决这个问题的方案是将T限制为实现了 Comparable接口(只含一个方法compareTo的标准接口)的类。可以通过对类型变量T设置限定( bound)实现这一点:
1 | public static <T extends Comparable> T min(T[] a)... |
实际上 Comparable接口本身就是一个泛型类型。目前,我们忽略其复杂性以及编译器产生的警告。
现在,泛型的min方法只能被实现了 Comparable接口的类(如String、 LocalDate等)的数组调用。由于 Rectangle类没有实现 Comparable接口,所以调用min将会产生一个编译错误。
读者或许会感到奇怪——在此为什么使用关键字 extends而不是 implements?毕竞, Comparable是一个接口。下面的记法
1 | <T extends Boundinglype> |
表示T应该是绑定类型的子类型(subtype)。T和绑定类型可以是类,也可以是接口。选择关键字 extends的原因是更接近子类的概念,并且Java的设计者也不打算在语言中再添加一个新的关键字(如sub)。
一个类型变量或通配符可以有多个限定,例如:
1 | T extends Comparable & Serializable |
限定类型用“&”分隔,而逗号用来分隔类型变量。
在java的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。