深入理解java泛型详解
什么是泛型?
泛型(Generictype或者generics)是对Java语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。
可以在集合框架(Collectionframework)中看到泛型的动机。例如,Map类允许您向一个Map添加任意类的对象,即使最常见的情况是在给定映射(map)中保存某个特定类型(比如String)的对象。
因为Map.get()被定义为返回Object,所以一般必须将Map.get()的结果强制类型转换为期望的类型,如下面的代码所示:
Mapm=newHashMap(); m.put("key","blarg"); Strings=(String)m.get("key");
要让程序通过编译,必须将get()的结果强制类型转换为String,并且希望结果真的是一个String。但是有可能某人已经在该映射中保存了不是String的东西,这样的话,上面的代码将会抛出ClassCastException。
理想情况下,您可能会得出这样一个观点,即m是一个Map,它将String键映射到String值。这可以让您消除代码中的强制类型转换,同时获得一个附加的类型检查层,该检查层可以防止有人将错误类型的键或值保存在集合中。这就是泛型所做的工作。
泛型的好处
Java语言中引入泛型是一个较大的功能增强。不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了。这带来了很多好处:
类型安全。泛型的主要目标是提高Java程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
Java程序中的一种流行技术是定义这样的集合,即它的元素或键是公共类型的,比如“String列表”或者“String到String的映射”。通过在变量声明中捕获这一附加的类型信息,泛型允许编译器实施这些附加的类型约束。类型错误现在就可以在编译时被捕获了,而不是在运行时当作ClassCastException展示出来。将类型检查从运行时挪到编译时有助于您更容易找到错误,并可提高程序的可靠性。
消除强制类型转换。泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
尽管减少强制类型转换可以降低使用泛型类的代码的罗嗦程度,但是声明泛型变量会带来相应的罗嗦。比较下面两个代码例子。
该代码不使用泛型:
Listli=newArrayList(); li.put(newInteger(3)); Integeri=(Integer)li.get(0);
该代码使用泛型:
Listli=newArrayList (); li.put(newInteger(3)); Integeri=li.get(0);
在简单的程序中使用一次泛型变量不会降低罗嗦程度。但是对于多次使用泛型变量的大型程序来说,则可以累积起来降低罗嗦程度。
潜在的性能收益。泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的JVM的优化带来可能。
由于泛型的实现方式,支持泛型(几乎)不需要JVM或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。
泛型用法的例子
泛型的许多最佳例子都来自集合框架,因为泛型让您在保存在集合中的元素上指定类型约束。考虑这个使用Map类的例子,其中涉及一定程度的优化,即Map.get()返回的结果将确实是一个String:
Mapm=newHashMap(); m.put("key","blarg"); Strings=(String)m.get("key");
如果有人已经在映射中放置了不是String的其他东西,上面的代码将会抛出ClassCastException。泛型允许您表达这样的类型约束,即m是一个将String键映射到String值的Map。这可以消除代码中的强制类型转换,同时获得一个附加的类型检查层,这个检查层可以防止有人将错误类型的键或值保存在集合中。
下面的代码示例展示了JDK5.0中集合框架中的Map接口的定义的一部分:
publicinterfaceMap{ publicvoidput(Kkey,Vvalue); publicVget(Kkey); }
注意该接口的两个附加物:
类型参数K和V在类级别的规格说明,表示在声明一个Map类型的变量时指定的类型的占位符。
在get()、put()和其他方法的方法签名中使用的K和V。
为了赢得使用泛型的好处,必须在定义或实例化Map类型的变量时为K和V提供具体的值。以一种相对直观的方式做这件事:
Mapm=newHashMap (); m.put("key","blarg"); Strings=m.get("key");
当使用Map的泛型化版本时,您不再需要将Map.get()的结果强制类型转换为String,因为编译器知道get()将返回一个String。
在使用泛型的版本中并没有减少键盘录入;实际上,比使用强制类型转换的版本需要做更多键入。使用泛型只是带来了附加的类型安全。因为编译器知道关于您将放进Map中的键和值的类型的更多信息,所以类型检查从执行时挪到了编译时,这会提高可靠性并加快开发速度。
向后兼容
在Java语言中引入泛型的一个重要目标就是维护向后兼容。尽管JDK5.0的标准类库中的许多类,比如集合框架,都已经泛型化了,但是使用集合类(比如HashMap和ArrayList)的现有代码将继续不加修改地在JDK5.0中工作。当然,没有利用泛型的现有代码将不会赢得泛型的类型安全好处。
二泛型基础
类型参数
在定义泛型类或声明泛型类的变量时,使用尖括号来指定形式类型参数。形式类型参数与实际类型参数之间的关系类似于形式方法参数与实际方法参数之间的关系,只是类型参数表示类型,而不是表示值。
泛型类中的类型参数几乎可以用于任何可以使用类名的地方。例如,下面是java.util.Map接口的定义的摘录:
publicinterfaceMap{ publicvoidput(Kkey,Vvalue); publicVget(Kkey); }
Map接口是由两个类型参数化的,这两个类型是键类型K和值类型V。(不使用泛型)将会接受或返回Object的方法现在在它们的方法签名中使用K或V,指示附加的类型约束位于Map的规格说明之下。
当声明或者实例化一个泛型的对象时,必须指定类型参数的值:
Map
注意,在本例中,必须指定两次类型参数。一次是在声明变量map的类型时,另一次是在选择HashMap类的参数化以便可以实例化正确类型的一个实例时。
编译器在遇到一个Map
除了异常类型、枚举或匿名内部类以外,任何类都可以具有类型参数。
命名类型参数
推荐的命名约定是使用大写的单个字母名称作为类型参数。这与C++约定有所不同(参阅附录A:与C++模板的比较),并反映了大多数泛型类将具有少量类型参数的假定。对于常见的泛型模式,推荐的名称是:
K——键,比如映射的键。
V——值,比如List和Set的内容,或者Map中的值。
E——异常类。
T——泛型。
泛型不是协变的
关于泛型的混淆,一个常见的来源就是假设它们像数组一样是协变的。其实它们不是协变的。List
如果A扩展B,那么A的数组也是B的数组,并且完全可以在需要B[]的地方使用A[]:
Integer[]intArray=newInteger[10];
Number[]numberArray=intArray;
上面的代码是有效的,因为一个Integer是一个Number,因而一个Integer数组是一个Number数组。但是对于泛型来说则不然。下面的代码是无效的:
List
List
最初,大多数Java程序员觉得这缺少协变很烦人,或者甚至是“坏的(broken)”,但是之所以这样有一个很好的原因。如果可以将List
ListintList=newArrayList (); List numberList=intList;//invalid numberList.add(newFloat(3.1415));
因为intList和numberList都是有别名的,如果允许的话,上面的代码就会让您将不是Integers的东西放进intList中。但是,正如下一屏将会看到的,您有一个更加灵活的方式来定义泛型。
类型通配符
假设您具有该方法:
voidprintList(Listl){ for(Objecto:l) System.out.println(o); }
上面的代码在JDK5.0上编译通过,但是如果试图用List
如果试图编写像下面这样的方法,那么将会怎么样?
voidprintList(List
它仍然不会通过编译,因为一个List
解决方案是使用类型通配符:
voidprintList(List>l){ for(Objecto:l) System.out.println(o); }
上面代码中的问号是一个类型通配符。它读作“问号”。List>是任何泛型List的父类型,所以您完全可以将List或List>>传递给printList()。
类型通配符的作用
前一屏类型通配符中引入了类型通配符,这让您可以声明List>类型的变量。您可以对这样的List做什么呢?非常方便,可以从中检索元素,但是不能添加元素。原因不是编译器知道哪些方法修改列表哪些方法不修改列表,而是(大多数)变化的方法比不变化的方法需要更多的类型信息。下面的代码则工作得很好:
Listli=newArrayList (); li.add(newInteger(42)); List>lu=li; System.out.println(lu.get(0));
为什么该代码能工作呢?对于lu,编译器一点都不知道List的类型参数的值。但是编译器比较聪明,它可以做一些类型推理。在本例中,它推断未知的类型参数必须扩展Object。(这个特定的推理没有太大的跳跃,但是编译器可以作出一些非常令人佩服的类型推理,后面就会看到(在底层细节一节中)。所以它让您调用List.get()并推断返回类型为Object。
另一方面,下面的代码不能工作:
Listli=newArrayList (); li.add(newInteger(42)); List>lu=li; lu.add(newInteger(43));//error
在本例中,对于lu,编译器不能对List的类型参数作出足够严密的推理,以确定将Integer传递给List.add()是类型安全的。所以编译器将不允许您这么做。
以免您仍然认为编译器知道哪些方法更改列表的内容哪些不更改列表内容,请注意下面的代码将能工作,因为它不依赖于编译器必须知道关于lu的类型参数的任何信息:
Listli=newArrayList (); li.add(newInteger(42)); List>lu=li; lu.clear();
泛型方法
(在类型参数一节中)您已经看到,通过在类的定义中添加一个形式类型参数列表,可以将类泛型化。方法也可以被泛型化,不管它们定义在其中的类是不是泛型化的。
泛型类在多个方法签名间实施类型约束。在List
类似地,之所以声明泛型方法,一般是因为您想要在该方法的多个参数之间宣称一个类型约束。例如,下面代码中的ifThenElse()方法,根据它的第一个参数的布尔值,它将返回第二个或第三个参数:
publicTifThenElse(booleanb,Tfirst,Tsecond){ returnb?first:second; }
注意,您可以调用ifThenElse(),而不用显式地告诉编译器,您想要T的什么值。编译器不必显式地被告知T将具有什么值;它只知道这些值都必须相同。编译器允许您调用下面的代码,因为编译器可以使用类型推理来推断出,替代T的String满足所有的类型约束:
Strings=ifThenElse(b,"a","b");
类似地,您可以调用:
Integeri=ifThenElse(b,newInteger(1),newInteger(2));
但是,编译器不允许下面的代码,因为没有类型会满足所需的类型约束:
Strings=ifThenElse(b,"pi",newFloat(3.14));
为什么您选择使用泛型方法,而不是将类型T添加到类定义呢?(至少)有两种情况应该这样做:
当泛型方法是静态的时,这种情况下不能使用类类型参数。
当T上的类型约束对于方法真正是局部的时,这意味着没有在相同类的另一个方法签名中使用相同类型T的约束。通过使得泛型方法的类型参数对于方法是局部的,可以简化封闭类型的签名。
有限制类型
在前一屏泛型方法的例子中,类型参数V是无约束的或无限制的类型。有时在还没有完全指定类型参数时,需要对类型参数指定附加的约束。
考虑例子Matrix类,它使用类型参数V,该参数由Number类来限制:
publicclassMatrix
编译器允许您创建Matrix
三一个简单的泛型类
编写基本的容器类
此时,您可以开始编写简单的泛型类了。到目前为止,泛型类最常见的用例是容器类(比如集合框架)或者值持有者类(比如WeakReference或ThreadLocal)。我们来编写一个类,它类似于List,充当一个容器。其中,我们使用泛型来表示这样一个约束,即Lhist的所有元素将具有相同类型。为了实现起来简单,Lhist使用一个固定大小的数组来保存值,并且不接受null值。
Lhist类将具有一个类型参数V(该参数是Lhist中的值的类型),并将具有以下方法:
publicclassLhist{ publicLhist(intcapacity){...} publicintsize(){...} publicvoidadd(Vvalue){...} publicvoidremove(Vvalue){...} publicVget(intindex){...} }
要实例化Lhist,只要在声明时指定类型参数和想要的容量:
Lhist
实现构造函数
在实现Lhist类时,您将会遇到的第一个拦路石是实现构造函数。您可能会像下面这样实现它:
publicclassLhist{ privateV[]array; publicLhist(intcapacity){ array=newV[capacity];//illegal } }
这似乎是分配后备数组最自然的一种方式,但是不幸的是,您不能这样做。具体原因很复杂,当学习到底层细节一节中的“擦除”主题时,您就会明白。分配后备数组的实现方式很古怪且违反直觉。下面是构造函数的一种可能的实现(该实现使用集合类所采用的方法):
publicclassLhist{ privateV[]array; publicLhist(intcapacity){ array=(V[])newObject[capacity]; } }
另外,也可以使用反射来实例化数组。但是这样做需要给构造函数传递一个附加的参数——一个类常量,比如Foo.class。后面在Class
实现方法
实现Lhist的方法要容易得多。下面是Lhist类的完整实现:
publicclassLhist{ privateV[]array; privateintsize; publicLhist(intcapacity){ array=(V[])newObject[capacity]; } publicvoidadd(Vvalue){ if(size==array.length) thrownewIndexOutOfBoundsException(Integer.toString(size)); elseif(value==null) thrownewNullPointerException(); array[size++]=value; } publicvoidremove(Vvalue){ intremovalCount=0; for(inti=0;i 0){ array[i-removalCount]=array[i]; array[i]=null; } } size-=removalCount; } publicintsize(){returnsize;} publicVget(inti){ if(i>=size) thrownewIndexOutOfBoundsException(Integer.toString(i)); returnarray[i]; } }
注意,您在将会接受或返回V的方法中使用了形式类型参数V,但是您一点也不知道V具有什么样的方法或域,因为这些对泛型代码是不可知的。
使用Lhist类
使用Lhist类很容易。要定义一个整数Lhist,只需要在声明和构造函数中为类型参数提供一个实际值即可:
Lhist
编译器知道,li.get()返回的任何值都将是Integer类型,并且它还强制传递给li.add()或li.remove()的任何东西都是Integer。除了实现构造函数的方式很古怪之外,您不需要做任何十分特殊的事情以使Lhist是一个泛型类。
四Java类库中的泛型
集合类
到目前为止,Java类库中泛型支持存在最多的地方就是集合框架。就像容器类是C++语言中模板的主要动机一样(参阅附录A:与C++模板的比较)(尽管它们随后用于很多别的用途),改善集合类的类型安全是Java语言中泛型的主要动机。集合类也充当如何使用泛型的模型,因为它们演示了泛型的几乎所有的标准技巧和方言。
所有的标准集合接口都是泛型化的——Collection
集合类也使用泛型的许多“技巧”和方言,比如上限通配符和下限通配符。例如,在接口Collection
interfaceCollection{ booleanaddAll(Collection); }
该定义组合了通配符类型参数和有限制类型参数,允许您将Collection
如果类库将addAll()定义为接受Collection
应该可以将addAll()的类型参数定义为Collection
作为泛型化一个类(如果不小心的话)如何会更改其语义的一个更加微妙的例子,注意Collection.removeAll()的参数的类型是Collection>,而不是Collection。这是因为传递混合类型的集合给removeAll()是可接受的,并且更加限制地定义removeAll将会更改方法的语义和有用性。
其他容器类
除了集合类之外,Java类库中还有几个其他的类也充当值的容器。这些类包括WeakReference、SoftReference和ThreadLocal。它们都已经在其包含的值的类型上泛型化了,所以WeakReference
泛型不止用于容器
泛型最常见最直观的使用是容器类,比如集合类或引用类(比如WeakReference
像Comparable
Comparable
Comparable接口已经泛型化了,所以实现Comparable的对象声明它可以与什么类型进行比较。(通常,这是对象本身的类型,但是有时也可能是父类。)
publicinterfaceComparable{ publicbooleancompareTo(Tother); }
所以Comparable接口包含一个类型参数T,该参数是一个实现Comparable的类可以与之比较的对象的类型。这意味着如果定义一个实现Comparable的类,比如String,就必须不仅声明类支持比较,还要声明它可与什么比较(通常是与它本身比较):
publicclassStringimplementsComparable
现在来考虑一个二元max()方法的实现。您想要接受两个相同类型的参数,二者都是Comparable,并且相互之间是Comparable。幸运的是,如果使用泛型方法和有限制类型参数的话,这相当直观:
publicstatic>Tmax(Tt1,Tt2){ if(t1.compareTo(t2)>0) returnt1; else returnt2; }
在本例中,您定义了一个泛型方法,在类型T上泛型化,您约束该类型扩展(实现)Comparable
更好的是,编译器将使用类型推理来确定当调用max()时T的值表示什么意思。所以根本不用指定T,下面的调用就能工作:
Strings=max("moo","bark");
编译器将计算出T的预定值是String,因此它将进行编译和类型检查。但是如果您试图用不实现Comparable
Class
类Class已经泛型化了,但是很多人一开始都感觉其泛型化的方式很混乱。Class
在以前的JDK中,Class.newInstance()方法的定义返回Object,您很可能要将该返回类型强制转换为另一种类型:
classClass{ ObjectnewInstance(); }
但是使用泛型,您定义Class.newInstance()方法具有一个更加特定的返回类型:
classClass{ TnewInstance(); }
如何创建一个Class
让Foo.class是Class
考虑一个方法,它从数据库检索一组对象,并返回JavaBeans对象的一个集合。您通过反射来实例化和初始化创建的对象,但是这并不意味着类型安全必须完全被抛至脑后。考虑下面这个方法:
publicstaticList getRecords(Class c,Selectors){ //UseSelectortoselectrows List list=newArrayList (); for(/*iterateoverresults*/){ Trow=c.newInstance(); //usereflectiontosetfieldsfromresult list.add(row); } returnlist; }
可以像下面这样简单地调用该方法:
List
编译器将会根据FooRecord.class是Class
用Class
Collection接口包含一个方法,用于将集合的内容复制到一个调用者指定类型的数组中:
publicObject[]toArray(Object[]prototypeArray){...}
toArray(Object[])的语义是,如果传递的数组足够大,就会使用它来保存结果,否则,就会使用反射分配一个相同类型的新数组。一般来说,单独传递一个数组作为参数来提供想要的返回类型是一个小技巧,但是在引入泛型之前,这是与方法交流类型信息最方便的方式。
有了泛型,就可以用一种更加直观的方式来做这件事。不像上面这样定义toArray(),泛型toArray()可能看起来像下面这样:
public
调用这样一个toArray()方法很简单:
FooBar[]fba=something.toArray(FooBar.class);
Collection接口还没有改变为使用该技术,因为这会破坏许多现有的集合实现。但是如果使用泛型从新构建Collection,则当然会使用该方言来指定它想要返回值是哪种类型。
Enum
JDK5.0中Java语言另一个增加的特性是枚举。当您使用enum关键字声明一个枚举时,编译器就会在内部为您生成一个类,用于扩展Enum并为枚举的每个值声明静态实例。所以如果您说:
publicenumSuit{HEART,DIAMOND,CLUB,SPADE};
编译器就会在内部生成一个叫做Suit的类,该类扩展java.lang.Enum
与Class一样,Enum也是一个泛型类。但是与Class不同,它的签名稍微更复杂一些:
classEnum
这究竟是什么意思?这难道不会导致无限递归?
我们逐步来分析。类型参数E用于Enum的各种方法中,比如compareTo()或getDeclaringClass()。为了这些方法的类型安全,Enum类必须在枚举的类上泛型化。
所以extendsEnum
总之,Enum是一个参数化的类型,只可以为它的子类型实例化,并且这些子类型然后将根据子类型来继承方法。幸运的是,在Enum情况下,编译器为您做这些工作,一切都很好。
与非泛型代码相互操作
数百万行现有代码使用已经泛型化的Java类库中的类,比如集合框架、Class和ThreadLocal。JDK5.0中的改进不要破坏所有这些代码是很重要的,所以编译器允许您在不指定其类型参数的情况下使用泛型类。
当然,以“旧方式”做事没有新方式安全,因为忽略了编译器准备提供的类型安全。如果您试图将List
没有类型参数的泛型,比如声明为List类型而不是List
为了消除一些unchecked-conversion警告,假设您不准备泛型化所有的代码,您可以使用通配符类型参数。使用List>而不使用List。List是原始类型;List>是具有未知类型参数的泛型。编译器将以不同的方式对待它们,并很可能发出更少的警告。
无论在哪种情况下,编译器在生成字节码时都会生成强制类型转换,所以生成的字节码在每种情况下都不会比没有泛型时更不安全。如果您设法通过使用原始类型或类文件来破坏类型安全,就会得到与不使用泛型时得到的相同的ClassCastException或ArrayStoreException。
已检查集合
作为从原始集合类型迁移到泛型集合类型的帮助,集合框架添加了一些新的集合包装器,以便为一些类型安全bug提供早期警告。就像Collections.unmodifiableSet()工厂方法用一个不允许任何修改的Set包装一个现有Set一样,Collections.checkedSet()(以及checkedList()和checkedMap())工厂方法创建一个包装器(或者视图)类,以防止您将错误类型的变量放在集合中。
checkedXxx()方法都接受一个类常量作为参数,所以它们可以(在运行时)检查这些修改是允许的。典型的实现可能像下面这样:
publicclassCollections{
publicstatic
returnnewCheckedCollection
}
privatestaticclassCheckedCollectionimplementsCollection { privatefinalCollection c; privatefinalClass type; CheckedCollection(Collection c,Class type){ this.c=c; this.type=type; } publicbooleanadd(Eo){ if(!type.isInstance(o)) thrownewClassCastException(); else returnc.add(o); } } }
希望本篇文章可以给各位朋友带来帮助