第8章 泛型程序设计
致使 Java SE 5.0 中增加泛型机制的主要原因是为了满足在 1999 年制定的最早的 Java 规范需求之一(JSR 14 )。专家组花费了 5 年左右的时间用来定义规范和测试实现。
至少在表面上看来, 泛型很像 C++ 中的模板。与 java—样,在 C++ 中, 模板也是最先被添加到语言中支持强类型集合的。但是, 多年之后人们发现模板还有其他的用武之地。学习完本章的内容可以发现 Java 中的泛型在程序中也有新的用途。
8.1 为什么要使用泛型程序设计
泛型程序设计(Generic programming) 意味着编写的代码可以被很多不同类型的对象所重用。
8.1.1 类型参数的好处
在 Java 中增加范型类之前, 泛型程序设计是用继承实现的。ArrayList 类只维护一个Object 引用的数组
这种方法有两个问题。当获取一个值时必须进行强制类型转换。
此外,这里没有错误检査。可以向数组列表中添加任何类的对象 。
泛型提供了一个更好的解决方案: 类型参数 (type parameters)。 ArrayList 类有一个类型参数用来指示元素的类型
注释: 前面已经提到,在 Java SE 7及以后的版本中, 构造函数中可以省略泛型类型:ArrayList\<String> files = new ArrayList\<>();
类型参数的魅力在于:使得程序具有更好的可读性和安全性
8.1.2 谁想成为泛型程序员
但是, 实现一个泛型类并没有那么容易。对于类型参数, 使用这段代码的程序员可能想要内置(plug in) 所有的类。 他们希望在没有过多的限制以及混乱的错误消息的状态下, 做所有的事情。因此, 一个泛型程序员的任务就是预测出所用类的未来可能有的所有用途。
Java 语言的设计者发明了一个具有独创性的新概念,通配符类型 ( wildcard type), 它解决了这个问题。通配符类型非常抽象,然而,它们能让库的构建者编写出尽可能灵活的方法。
8.2 定义简单泛型类
一个泛型类(generic class) 就是具有一个或多个类型变量的类。
类定义中的类型变量指定方法的返回类型以及域和局部变量的类型。
注释: 类型变量使用大写形式,且比较短, 这是很常见的。在 Java 库中, 使用变量 E 表示集合的元素类型, K 和 V 分别表示表的关键字与值的类型。 T ( 需要时还可以用临近的字母 U 和 S ) 表示“任意类型”。
用具体的类型替换类型变量就可以实例化泛型类型
换句话说,泛型类可看作普通类的工厂
C++ 注释: 从表面上看, Java 的泛型类类似于 C++ 的模板类。唯一明显的不同是 Java 没有专用的 template 关键字。但是, 在本章中读者将会看到, 这两种机制有着本质的区别。
8.3 泛型方法
这个方法是在普通类中定义的, 而不是在泛型类中定义的。然而,这是一个泛型方法,可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符(这里是 public static) 的后面,返回类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
几乎在大多数情况下,对于泛型方法的类型引用没有问题。 偶尔, 编译器也会提示错误, 此时需要解译错误报告。看一看下面这个示例:
double middle = ArrayAlg.getMiddle(3.14, 1729, 0);
错误消息会以晦涩的方式指出(不同的编译器给出的错误消息可能有所不同): 解释这句代码有两种方法,而且这两种方法都是合法的。简单地说, 编译器将会自动打包参数为 1 个Double 和 2 个 Integer 对象,而后寻找这些类的共同超类型。事实上;找到 2 个这样的超类型: Number 和 Comparable 接口,其本身也是一个泛型类型。在这种情况下,可以采取的补救措施是将所有的参数写为 double 值
提示: 如果想知道编译器对一个泛型方法调用最终推断出哪种类型, Peter von der Ahe 推荐了这样一个窍门: 有目的地引入一个错误, 并研究所产生的错误消息。
C++ 注释:在 C++ 中将类型参数放在方法名后面, 有可能会导致语法分析的歧义。例如,g(f<a,b>©) 可以理解为“用 f<a, b>© 的结果调用 g”,或者理解为“ 用两个布尔值 f<a 和 b>© 调用 g。”
8.4 类型变量的限定
有时,类或方法需要对类型变量加以约束。
可以通过对类型变量 T 设置限定(bound) 实现这一点:public static <T extends Coiparable> T min(T[] a) . . .
C++ 注释:在 C++ 中不能对模板参数的类型加以限制。 如果程序员用一个不适当的类型实例化一个模板,将会在模板代码中报告一个(通常是含糊不清的)错误消息。
读者或许会感到奇怪—在此为什么使用关键字 extends 而不是 implements ? 毕竟,Comparable 是一个接口。下面的记法
<T extends BoundingType>
表示 T 应该是绑定类型的子类型 (subtype)。 T 和绑定类型可以是类, 也可以是接口。选择关键字 extends 的原因是更接近子类的概念, 并且 Java 的设计者也不打算在语言中再添加一个新的关键字(如 sub)。
一个类型变量或通配符可以有多个限定, 例如:
T extends Comparable & Serializable
限定类型用“ &” 分隔,而逗号用来分隔类型变量。
在 Java 的继承中, 可以根据需要拥有多个接口超类型, 但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。
8.5 泛型代码和虚拟机
虚拟机没有泛型类型对象—所有对象都属于普通类。在泛型实现的早期版本中, 甚至能够将使用泛型的程序编译为在 1.0 虚拟机上运行的类文件! 这个向后兼容性在 Java 泛型开发的后期被放弃了。
8.5.1 类型擦除
无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型 ( raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除( erased) 类型变量 , 并替换为限定类型 (无限定的变量用 Object。)
C++ 注释:就这点而言, Java 泛型与 C++ 模板有很大的区别。C++ 中每个模板的实例化产生不同的类型,这一现象称为“ 模板代码膨账”。Java 不存在这个问题的困扰。
原始类型用第一个限定的类型变量来替换, 如果没有给定限定就用 Object 替换。
注释: 读者可能想要知道切换限定: class Interval<T extends Serializable & Comparable>会发生什么。 如果这样做, 原始类型用 Serializable 替换 T, 而编译器在必要时要向Comparable 插入强制类型转换。 为了提高效率, 应该将标签(tagging) 接口(即没有方法的接口)放在边界列表的末尾。
8.5.2 翻译泛型表达式
当程序调用泛型方法时, 如果擦除返回类型, 编译器插入强制类型转换。
8.5.3 翻译泛型方法
类型擦除也会出现在泛型方法中。
问题在于类型擦除与多态发生了冲突。要解决这个问题, 就需要编译器在 类中生成一个桥方法 (bridge method)
不能这样编写 Java 代码(在这里,具有相同参数类型的两个方法是不合法的)。它们都没有参数。但是, 在虚拟机中,用参数类型和返回类型确定一个方法。因此, 编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。
注释: 桥方法不仅用于泛型类型。 第 5 章已经讲过,在一个方法覆盖另一个方法时可以指定一个更严格的返回类型。
两个方法被说成具有协变的返回类型 (covariant return types。)
总之,需要记住有关 Java 泛型转换的事实:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都用它们的限定类型替换。
- 桥方法被合成来保持多态。
- 为保持类型安全性,必要时插人强制类型转换。
8.5.4 调用遗留代码
设计 Java 泛型类型时, 主要目标是允许泛型代码和遗留代码之间能够互操作。
8.6 约束与局限性
在下面几节中, 将阐述使用 Java 泛型时需要考虑的一些限制。大多数限制都是由类型擦除引起的。
8.6.1 不能用基本类型实例化类型参数
当然 ,其原因是类型擦除。擦除之后, Pair 类含有 Object 类型的域, 而 Object 不能存储 double 值。
8.6.2 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。 因此, 所有的类型查询只产生原始类型。
为提醒这一风险, 试图查询一个对象是否属于某个泛型类型时, 倘若使用 instanceof 会得到一个编译器错误, 如果使用强制类型转换会得到一个警告。
同样的道理, getClass 方法总是返回原始类型。
8.6.3 不能创建参数化类型的数组
不过对于泛型类型, 擦除会使这种机制无效。 以下赋值:
objarray[0] = new Pair< Employee>0;
能够通过数组存储检査, 不过仍会导致一个类型错误。 出于这个原因, 不允许创建参数
化类型的数组。
需要说明的是, 只是不允许创建这些数组, 而声明类型为 Pair<String>[] 的变量仍是合法的。不过不能用 new Pair<String>[10] 初始化这个变量。
注释: 可以声明通配类型的数组, 然后进行类型转换:
Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];
结果将是不安全的。 如果在 table[0] 中存储一个 Pair<Employee>
, 然后对 table[0].getFirst() 调用一个 String 方法, 会得到一个 ClassCastException 异常。
*提示:如果需要收集参数化类型对象, 只有一种安全而有效的方法: 使用 ArrayList:Arra yList<Pair<String>>
。
8.6.4 Varargs警告
上一节中已经了解到, Java 不支持泛型类型的数组。这一节中我们再来讨论一个相关的问题:向参数个数可变的方法传递一个泛型类型的实例。
为了调用这个方法, Java 虚拟机必须建立一个 Pair<String> 数组, 这就违反了前面的规则。不过,对于这种情况, 规则有所放松, 你只会得到一个警告,而不是错误。
可以采用两种方法来抑制这个警告。 一种方法是为包含 addAll 调用的方法增加注解 @SuppressWamings(“unchecked”。) 或者在 Java SE 7中, 还可以用@SafeVarargs 直接标注addAll 方法 。
*注释: 可以使用 @SafeVarargs标注来消除创建泛型数组的有关限制。这看起来很方便, 不过隐藏着危险。 *
8.6.5 不能实例化类型变量
不能使用像 new T(…), new T[…] 或 T.class 这样的表达式中的类型变量。
在 Java SE 8 之后,最好的解决办法是让调用者提供一个构造器表达式。
比较传统的解决方法是通过反射调用 Class.newlnstance 方法来构造泛型对象
注意,Class类本身是泛型。 例如, String.daSS 是一个 Class<String> 的实例(事实上,它是唯一的实例。)
8.6.6 不能构造泛型数组
就像不能实例化一个泛型实例一样, 也不能实例化数组。不过原因有所不同,毕竟数组会填充null 值,构造时看上去是安全的。不过, 数组本身也有类型,用来监控存储在虚拟机中的数组。这个类型会被擦除。
如果数组仅仅作为一个类的私有实例域, 就可以将这个数组声明为 Object[],并且在获取元素时进行类型转换。
在这种情况下, 最好让用户提供一个数组构造器表达式
比较老式的方法是利用反射, 调用 Array.newlnstance
8.6.7 泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量。
8.6.8 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象。实际上, 甚至泛型类扩展 Throwable 都是不合法的。
catch 子句中不能使用类型变量。
不过, 在异常规范中使用类型变量是允许的。
8.6.9 可以消除对受查异常的检查
Java 异常处理的一个基本原则是, 必须为所有受查异常提供一个处理器。不过可以利用泛型消除这个限制。 关键在于以下方法:
@SuppressWamings("unchecked")
public static <T extends Throwable〉void throwAs(Throwable e) throws T
{
throw (T) e;
}
通过使用泛型类、 擦除和 @SuppressWamings 注解, 就能消除 Java 类型系统的部分基本限制。
8.6.10 注意擦除后的冲突
当泛型类型被擦除时, 无法创建引发冲突的条件。
当然,补救的办法是重新命名引发错误的方法。
泛型规范说明还提到另外一个原则:“ 要想支持擦除的转换, 就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类, 而这两个接口是同一接口的不同参数化。”
8.7 泛型类型的继承规则
考虑一个类和一个子类, 如 Employee 和 Manager。 Pair<Manager> 是Pair<Employee> 的一个子类吗? 答案是“ 不是”, 或许人们会感到奇怪。
无论 S 与 T 有什么联系 , 通常, Pair<S> 与 Pair<T> 没有什么联系。
这一限制看起来过于严格, 但对于类型安全非常必要。
永远可以将参数化类型转换为一个原始类型。例如, Pair<Employee> 是原始类型 Pair 的一个子类型。在与遗留代码衔接时,这个转换非常必要。
转换成原始类型之后会产生类型错误吗? 很遗憾, 会!
最后, 泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。 例如, ArrayList<T> 类实现 List<T> 接口。
8.8 通配符类型
8.8.1 通配符概念
通配符类型中, 允许类型参数变化。 例如, 通配符类型
Pair<? extends Employee>
表示任何泛型 Pair 类型, 它的类型参数是 Employee 的子类, 如 Pair<Manager>, 但不是Pair<String>。
编译器只知道需要某个 Employee 的子类型,但不知道具体是什么类型。它拒绝传递任何特定的类型。毕竟?不能用来匹配
这就是引入有限定的通配符的关键之处。现在已经有办法区分安全的访问器方法和不安全的更改器方法了。
8.8.2 通配符的超类型限定
通配符限定与类型变量限定十分类似,但是,还有一个附加的能力, 即可以指定一个超
类型限定 (supertype bound), 如下所示:
? super Manager
带有超类型限定的通配符的行为与 8.8 节介绍的相反。可以为方法提供参数, 但不能使用返回值。
直观地讲,带有超类型限定的通配符可以向泛型对象写人,带有子类型限定的通配符可以从泛型对象读取。
对于初学者来说,<T extends Comparable<? super T>>这样的声明看起来有点吓人。很遗憾, 因为这一声明的意图在于帮助应用程序员排除调用参数上的不必要的限制。对泛型没有兴趣的应用程序员很可能很快就学会掩盖这些声明,想当然地认为库程序员做的都是正确的。如果是一名库程序员,一定要习惯于通配符,否则, 就会受到用户的责备,还要在代码中随意地添加强制类型转换直至代码可以编译。
注释:子类型限定的另一个常见的用法是作为一个函数式接口的参数类型。
8.8.3 无限定通配符
还可以使用无限定的通配符, 例如,Pair<?>。初看起来,这好像与原始的 Pair 类型一样。实际上, 有很大的不同。
8.8.4 通配符捕获
8.9 反射和泛型
8.9.1 泛型Class类
现在, Class 类是泛型的。 例如, String.class 实际上是一个:Class< String > 类的对象(事实上,是唯一的对象。)
类型参数十分有用, 这是因为它允许 Class<T> 方法的返回类型更加具有针对性 。
newlnstance 方法返回一个实例,这个实例所属的类由默认的构造器获得。它的返回类型目前被声明为 T, 其类型与 Class<T> 描述的类相同,这样就免除了类型转换。
【API】java.lang.Class<T> 1.0 :
T newInstance()
返回无参数构造器构造的一个新实例。T cast(Object obj)
如果 obj 为 null 或有可能转换成类型 T, 则返回 obj ; 否则拋出 BadCastException异常。T[ ] getEnumConstants( )
5.0
如果 T 是枚举类型, 则返回所有值组成的数组,否则返回 null。Class<? super T> getSuperclass( )
返回这个类的超类。如果 T 不是一个类或 Object 类, 则返回 null。Constructor<T> getConstructor(Class... parameterTypes)
1.1
Constructor<T> getDeclaredConstructor(Class... parameterTypes)
1.1
获得公有的构造器, 或带有给定参数类型的构造器。
【API】java.lang.reflect.Constructor<T> 1.1 :
T newlnstance(Object... parameters)
返回用指定参数构造的新实例。
8.9.2 使用Class<T>参数进行类型匹配
8.9.3 虚拟机中的泛型类型信息
Java 泛型的卓越特性之一是在虚拟机中泛型类型的擦除。令人感到奇怪的是, 擦除的类仍然保留一些泛型祖先的微弱记忆。
换句话说,需要重新构造实现者声明的泛型类以及方法中的所有内容。但是,不会知道对于特定的对象或方法调用, 如何解释类型参数。
为了表达泛型类型声明, 使用 java.lang.reflect 包中提供的接口 Type。这个接口包含下列子类型:
•Class 类,描述具体类型。
•TypeVariable 接口,描述类型变量(如 T extends Comparable<? super T>) 。
•WildcardType 接口, 描述通配符 (如?super T )。
•ParameterizedType 接口, 描述泛型类或接口类型(如 Comparable<? super T>)。
•GenericArrayType 接口, 描述泛型数组(如 T[ ]。
【API】java.lang.Class<T> 1.0
TypeVariable[ ] getTypeParameters( )
5.0
如果这个类型被声明为泛型类型, 则获得泛型类型变量,否则获得一个长度为 0 的数组。Type getGenericSuperclass( )
5.0
获得被声明为这一类型的超类的泛型类型; 如果这个类型是 Object 或不是一个类类型(class type), 则返回 null。Type[ ] getGenericInterfaces( )
5.0
获得被声明为这个类型的接口的泛型类型(以声明的次序,) 否则, 如果这个类型没有实现接口,返回长度为 0 的数组。
【API】java.lang.reflect.Method 1.1 :
TypeVariable[] getTypeParameters( )
5.0
如果这个方法被声明为泛型方法, 则获得泛型类型变量,否则返回长度为 0 的数组。Type getGenericReturnType( )
5.0
获得这个方法被声明的泛型返回类型。Type[ ] getGenericParameterTypes( )
5.0
获得这个方法被声明的泛型参数类型。 如果这个方法没有参数, 返回长度为 0 的数组。
【API】java.lang.reflect.TypeVariable 5.0 :
String getName( )
获得类型变量的名字。Type[ ] getBounds( )
获得类型变量的子类限定,否则, 如果该变量无限定, 则返回长度为 0 的数组。
【API】 java.Iang.reflect.WildcardType 5.0
Type[ ] getUpperBounds( )
获得这个类型变量的子类 ( extends) 限定,否则, 如果没有子类限定,则返回长度为0 的数组。Type[ ] getLowerBounds( )
获得这个类型变量的超类(super) 限定,否则, 如果没有超类限定, 则返回长度为 0的数组。
【API】 java.Iang.reflect.ParameterizedType 5.0 :
Type getRawType( )
获得这个参数化类型的原始类型。Type[ ] getActualTypeArguments( )
获得这个参数化类型声明时所使用的类型参数。Type getOwnerType( )
如果是内部类型, 则返回其外部类型, 如果是一个顶级类型, 则返回 null。
【API】java.Iang.reflect.GenericAnrayType 5.0 :
Type getGenericComponentType( )
获得声明该数组类型的泛型组件类型。