天天看点

Java泛型和通配符那点事

泛型(generic type 或者generics)是对 java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。 

可以在集合框架(collection framework)中看到泛型的动机。例如,map类允许您向一个map添加任意类的对象,即使最常见的情况是在给定映射(map)中保存某个特定类型(比如string)的对象。 

因为map.get()被定义为返回object,所以一般必须将map.get()的结果强制类型转换为期望的类型,如下面的代码所示: 

map m = new hashmap(); 

m.put("key", "blarg"); 

string s = (string) m.get("key"); 

要让程序通过编译,必须将get()的结果强制类型转换为string,并且希望结果真的是一个string。但是有可能某人已经在该映射中保存了不是string的东西,这样的话,上面的代码将会抛出classcastexception。 

理想情况下,您可能会得出这样一个观点,即m是一个map,它将string键映射到string值。这可以让您消除代码中的强制类型转换,同时获得一个附加的类型检查层,该检查层可以防止有人将错误类型的键或值保存在集合中。这就是泛型所做的工作。 

泛型的好处 

java 语言中引入泛型是一个较大的功能增强。不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了。这带来了很多好处: 

· 类型安全。泛型的主要目标是提高 java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。 

java 程序中的一种流行技术是定义这样的集合,即它的元素或键是公共类型的,比如“string列表”或者“string到string的映射”。通过在变量声明中捕获这一附加的类型信息,泛型允许编译器实施这些附加的类型约束。类型错误现在就可以在编译时被捕获了,而不是在运行时当作classcastexception展示出来。将类型检查从运行时挪到编译时有助于您更容易找到错误,并可提高程序的可靠性。 

· 消除强制类型转换。泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。 

尽管减少强制类型转换可以降低使用泛型类的代码的罗嗦程度,但是声明泛型变量会带来相应的罗嗦。比较下面两个代码例子。 

该代码不使用泛型: 

list li = new arraylist(); 

li.put(new integer(3)); 

integer i = (integer) li.get(0); 

该代码使用泛型: 

list<integer> li = new arraylist<integer>(); 

integer i = li.get(0); 

在简单的程序中使用一次泛型变量不会降低罗嗦程度。但是对于多次使用泛型变量的大型程序来说,则可以累积起来降低罗嗦程度。 

· 潜在的性能收益。泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的jvm 的优化带来可能。 

由于泛型的实现方式,支持泛型(几乎)不需要jvm 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。 

泛型用法的例子 

泛型的许多最佳例子都来自集合框架,因为泛型让您在保存在集合中的元素上指定类型约束。考虑这个使用map类的例子,其中涉及一定程度的优化,即map.get()返回的结果将确实是一个string: 

如果有人已经在映射中放置了不是string的其他东西,上面的代码将会抛出classcastexception。泛型允许您表达这样的类型约束,即m是一个将string键映射到string值的map。这可以消除代码中的强制类型转换,同时获得一个附加的类型检查层,这个检查层可以防止有人将错误类型的键或值保存在集合中。 

下面的代码示例展示了 jdk 5.0 中集合框架中的map接口的定义的一部分: 

public interface map<k, v> { 

public void put(k key, v value); 

public v get(k key); 

注意该接口的两个附加物: 

* 类型参数 k 和 v 在类级别的规格说明,表示在声明一个 map 类型的变量时指定的类型的占位符。 

* 在 get()、put() 和其他方法的方法签名中使用的 k 和 v。 

为了赢得使用泛型的好处,必须在定义或实例化map类型的变量时为k和v提供具体的值。以一种相对直观的方式做这件事: 

map<string, string> m = new hashmap<string, string>(); 

string s = m.get("key"); 

当使用map的泛型化版本时,您不再需要将map.get()的结果强制类型转换为string,因为编译器知道get()将返回一个string。 

在使用泛型的版本中并没有减少键盘录入;实际上,比使用强制类型转换的版本需要做更多键入。使用泛型只是带来了附加的类型安全。因为编译器知道关于您将放进map中的键和值的类型的更多信息,所以类型检查从执行时挪到了编译时,这会提高可靠性并加快开发速度。 

向后兼容 

在 java 语言中引入泛型的一个重要目标就是维护向后兼容。尽管 jdk 5.0 的标准类库中的许多类,比如集合框架,都已经泛型化了,但是使用集合类(比如hashmap和arraylist)的现有代码将继续不加修改地在 jdk 5.0 中工作。当然,没有利用泛型的现有代码将不会赢得泛型的类型安全好处。 

类型参数 

在定义泛型类或声明泛型类的变量时,使用尖括号来指定形式类型参数。形式类型参数与实际类型参数之间的关系类似于形式方法参数与实际方法参数之间的关系,只是类型参数表示类型,而不是表示值。 

泛型类中的类型参数几乎可以用于任何可以使用类名的地方。例如,下面是java.util.map接口的定义的摘录: 

map接口是由两个类型参数化的,这两个类型是键类型k和值类型v。(不使用泛型)将会接受或返回object的方法现在在它们的方法签名中使用k或v,指示附加的类型约束位于map的规格说明之下。 

当声明或者实例化一个泛型的对象时,必须指定类型参数的值: 

map<string, string> map = new hashmap<string, string>(); 

注意,在本例中,必须指定两次类型参数。一次是在声明变量map的类型时,另一次是在选择hashmap类的参数化以便可以实例化正确类型的一个实例时。 

编译器在遇到一个map<string, string>类型的变量时,知道k和v现在被绑定为string,因此它知道在这样的变量上调用map.get()将会得到string类型。 

除了异常类型、枚举或匿名内部类以外,任何类都可以具有类型参数。 

命名类型参数 

推荐的命名约定是使用大写的单个字母名称作为类型参数。这与c++ 约定有所不同(参阅附录 a:与 c++ 模板的比较),并反映了大多数泛型类将具有少量类型参数的假定。对于常见的泛型模式,推荐的名称是: 

* k —— 键,比如映射的键。 

* v —— 值,比如 list 和 set 的内容,或者 map 中的值。 

* e —— 异常类。 

* t —— 泛型。 

泛型不是协变的 

关于泛型的混淆,一个常见的来源就是假设它们像数组一样是协变的。其实它们不是协变的。list<object>不是list<string>的父类型。 

如果 a 扩展 b,那么 a 的数组也是 b 的数组,并且完全可以在需要b[]的地方使用a[]: 

integer[] intarray = new integer[10]; 

number[] numberarray = intarray; 

上面的代码是有效的,因为一个integer是一个number,因而一个integer数组是一个number数组。但是对于泛型来说则不然。下面的代码是无效的: 

list<integer> intlist = new arraylist<integer>(); 

list<number> numberlist = intlist; // invalid 

最初,大多数 java 程序员觉得这缺少协变很烦人,或者甚至是“坏的(broken)”,但是之所以这样有一个很好的原因。如果可以将list<integer>赋给list<number>,下面的代码就会违背泛型应该提供的类型安全: 

numberlist.add(new float(3.1415)); 

因为intlist和numberlist都是有别名的,如果允许的话,上面的代码就会让您将不是integers的东西放进intlist中。但是,正如下一屏将会看到的,您有一个更加灵活的方式来定义泛型。 

package com.ibm.course.generics; 

import java.util.arraylist; 

import java.util.list; 

public class genericsexample { 

public static void main(string[] args) { 

integer[] integer = new integer[5]; 

number[] number = integer; 

system.out.println(number[0]);// null 

number[0] = new float(7.65); 

system.out.println(number[0]); 

system.out.println(integer[0]); 

list<integer> list = new arraylist<integer>(); 

// type mismatch: cannot convert from list<integer> to list<number> 

// list<number> listobj = list; 

list<number> listobj = list;导致编译错误:type mismatch: cannot convert from list<integer> to list<number> 

而system.out.println(number[0]);和system.out.println(integer[0]);导致运行时异常: 

exception in thread "main" java.lang.arraystoreexception: java.lang.float 

at com.ibm.course.generics.genericsexample.main(genericsexample.java:15) 

类型通配符 

假设您具有该方法: 

void printlist(list l) { 

for (object o : l) 

system.out.println(o); 

上面的代码在 jdk 5.0 上编译通过,但是如果试图用list<integer>调用它,则会得到警告。出现警告是因为,您将泛型(list<integer>)传递给一个只承诺将它当作list(所谓的原始类型)的方法,这将破坏使用泛型的类型安全。 

如果试图编写像下面这样的方法,那么将会怎么样? 

void printlist(list<object> l) { 

它仍然不会通过编译,因为一个list<integer>不是一个list<object>(正如前一屏泛型不是协变的 中所学的)。这才真正烦人——现在您的泛型版本还没有普通的非泛型版本有用! 

解决方案是使用类型通配符: 

void printlist(list<?> l) { 

上面代码中的问号是一个类型通配符。它读作“问号”。list<?>是任何泛型list的父类型,所以您完全可以将list<object>、list<integer>或list<list<list<flutzpah>>>传递给printlist()。 

public class genericexample { 

list<integer> integer = new arraylist<integer>(); 

integer.add(new integer(0)); 

integer.add(new integer(1)); 

list<string> str = new arraylist<string>(); 

str.add(new string("hello")); 

str.add(new string("world")); 

list<?> li=integer; 

li=str; 

printlist(integer); 

printlist(str); 

public static void printlist(list<?> l) { 

for (object o : l) { 

上面的例子程序没有警告也没有编译错误。 

类型通配符的作用 

前一屏类型通配符 中引入了类型通配符,这让您可以声明list<?>类型的变量。您可以对这样的list做什么呢?非常方便,可以从中检索元素,但是不能添加元素(可以添加null)。原因不是编译器知道哪些方法修改列表哪些方法不修改列表,而是(大多数)变化的方法比不变化的方法需要更多的类型信息。下面的代码则工作得很好: 

li.add(new integer(42)); 

list<?> lu = li; 

system.out.println(lu.get(0)); 

为什么该代码能工作呢?对于lu,编译器一点都不知道list的类型参数的值。但是编译器比较聪明,它可以做一些类型推理。在本例中,它推断未知的类型参数必须扩展object。(这个特定的推理没有太大的跳跃,但是编译器可以作出一些非常令人佩服的类型推理,后面就会看到(在底层细节 一节中)。所以它让您调用list.get()并推断返回类型为object。 

另一方面,下面的代码不能工作: 

lu.add(new integer(43)); // error 

在本例中,对于lu,编译器不能对list的类型参数作出足够严密的推理,以确定将integer传递给list.add()是类型安全的。所以编译器将不允许您这么做。 

以免您仍然认为编译器知道哪些方法更改列表的内容哪些不更改列表内容,请注意下面的代码将能工作,因为它不依赖于编译器必须知道关于lu的类型参数的任何信息: 

lu.clear(); 

泛型方法 

(在类型参数 一节中)您已经看到,通过在类的定义中添加一个形式类型参数列表,可以将类泛型化。方法也可以被泛型化,不管它们定义在其中的类是不是泛型化的。 

泛型类在多个方法签名间实施类型约束。在list<v>中,类型参数v出现在get()、add()、contains()等方法的签名中。当创建一个map<k, v>类型的变量时,您就在方法之间宣称一个类型约束。您传递给add()的值将与get()返回的值的类型相同。 

类似地,之所以声明泛型方法,一般是因为您想要在该方法的多个参数之间宣称一个类型约束。例如,下面代码中的ifthenelse()方法,根据它的第一个参数的布尔值,它将返回第二个或第三个参数: 

public <t> t ifthenelse(boolean b, t first, t second) { 

return b ? first : second; 

注意,您可以调用ifthenelse(),而不用显式地告诉编译器,您想要t的什么值。编译器不必显式地被告知 t 将具有什么值;它只知道这些值都必须相同。编译器允许您调用下面的代码,因为编译器可以使用类型推理来推断出,替代t的string满足所有的类型约束: 

string s = ifthenelse(b, "a", "b"); 

类似地,您可以调用: 

integer i = ifthenelse(b, new integer(1), new integer(2)); 

但是,编译器不允许下面的代码,因为没有类型会满足所需的类型约束: 

string s = ifthenelse(b, "pi", new float(3.14)); 

为什么您选择使用泛型方法,而不是将类型t添加到类定义呢?(至少)有两种情况应该这样做: 

* 当泛型方法是静态的时,这种情况下不能使用类类型参数。 

* 当 t 上的类型约束对于方法真正是局部的时,这意味着没有在相同类的另一个 方法签名中使用相同 类型 t 的约束。通过使得泛型方法的类型参数对于方法是局部的,可以简化封闭类型的签名。 

有限制类型 

在前一屏泛型方法 的例子中,类型参数v是无约束的或无限制的类型。有时在还没有完全指定类型参数时,需要对类型参数指定附加的约束。 

考虑例子matrix类,它使用类型参数v,该参数由number类来限制: 

public class matrix<v extends number> { ... } 

编译器允许您创建matrix<integer>或matrix<float>类型的变量,但是如果您试图定义matrix<string>类型的变量,则会出现错误。类型参数v被判断为由number限制。在没有类型限制时,假设类型参数由object限制。这就是为什么前一屏泛型方法 中的例子,允许list.get()在list<?>上调用时返回object,即使编译器不知道类型参数v的类型。