- 浏览: 123550 次
-
文章分类
最新评论
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);
该代码使用泛型:
List<Integer>li=newArrayList<Integer>();
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<K,V>{
publicvoidput(Kkey,Vvalue);
publicVget(Kkey);
}
注意该接口的两个附加物:
类型参数K和V在类级别的规格说明,表示在声明一个Map类型的变量时指定的类型的占位符。
在get()、put()和其他方法的方法签名中使用的K和V。
为了赢得使用泛型的好处,必须在定义或实例化Map类型的变量时为K和V提供具体的值。以一种相对直观的方式做这件事:
Map<String,String>m=newHashMap<String,String>();
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<K,V>{
publicvoidput(Kkey,Vvalue);
publicVget(Kkey);
}
Map接口是由两个类型参数化的,这两个类型是键类型K和值类型V。(不使用泛型)将会接受或返回Object的方法现在在它们的方法签名中使用K或V,指示附加的类型约束位于Map的规格说明之下。
当声明或者实例化一个泛型的对象时,必须指定类型参数的值:
Map<String,String>map=newHashMap<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=newInteger[10];
Number[]numberArray=intArray;
上面的代码是有效的,因为一个Integer是一个Number,因而一个Integer数组是一个Number数组。但是对于泛型来说则不然。下面的代码是无效的:
List<Integer>intList=newArrayList<Integer>();
List<Number>numberList=intList;//invalid
最初,大多数Java程序员觉得这缺少协变很烦人,或者甚至是“坏的(broken)”,但是之所以这样有一个很好的原因。如果可以将List<Integer>赋给List<Number>,下面的代码就会违背泛型应该提供的类型安全:
List<Integer>intList=newArrayList<Integer>();
List<Number>numberList=intList;//invalid
numberList.add(newFloat(3.1415));
因为intList和numberList都是有别名的,如果允许的话,上面的代码就会让您将不是Integers的东西放进intList中。但是,正如下一屏将会看到的,您有一个更加灵活的方式来定义泛型。
类型通配符
假设您具有该方法:
voidprintList(Listl){
for(Objecto:l)
System.out.println(o);
}
上面的代码在JDK5.0上编译通过,但是如果试图用List<Integer>调用它,则会得到警告。出现警告是因为,您将泛型(List<Integer>)传递给一个只承诺将它当作List(所谓的原始类型)的方法,这将破坏使用泛型的类型安全。
如果试图编写像下面这样的方法,那么将会怎么样?
voidprintList(List<Object>l){
for(Objecto:l)
System.out.println(o);
}
它仍然不会通过编译,因为一个List<Integer>不是一个List<Object>(正如前一屏泛型不是协变的中所学的)。这才真正烦人——现在您的泛型版本还没有普通的非泛型版本有用!
解决方案是使用类型通配符:
voidprintList(List<?>l){
for(Objecto:l)
System.out.println(o);
}
上面代码中的问号是一个类型通配符。它读作“问号”。List<?>是任何泛型List的父类型,所以您完全可以将List<Object>、List<Integer>或List<List<List<Flutzpah>>>传递给printList()。
类型通配符的作用
前一屏类型通配符中引入了类型通配符,这让您可以声明List<?>类型的变量。您可以对这样的List做什么呢?非常方便,可以从中检索元素,但是不能添加元素。原因不是编译器知道哪些方法修改列表哪些方法不修改列表,而是(大多数)变化的方法比不变化的方法需要更多的类型信息。下面的代码则工作得很好:
List<Integer>li=newArrayList<Integer>();
li.add(newInteger(42));
List<?>lu=li;
System.out.println(lu.get(0));
为什么该代码能工作呢?对于lu,编译器一点都不知道List的类型参数的值。但是编译器比较聪明,它可以做一些类型推理。在本例中,它推断未知的类型参数必须扩展Object。(这个特定的推理没有太大的跳跃,但是编译器可以作出一些非常令人佩服的类型推理,后面就会看到(在底层细节一节中)。所以它让您调用List.get()并推断返回类型为Object。
另一方面,下面的代码不能工作:
List<Integer>li=newArrayList<Integer>();
li.add(newInteger(42));
List<?>lu=li;
lu.add(newInteger(43));//error
在本例中,对于lu,编译器不能对List的类型参数作出足够严密的推理,以确定将Integer传递给List.add()是类型安全的。所以编译器将不允许您这么做。
以免您仍然认为编译器知道哪些方法更改列表的内容哪些不更改列表内容,请注意下面的代码将能工作,因为它不依赖于编译器必须知道关于lu的类型参数的任何信息:
List<Integer>li=newArrayList<Integer>();
li.add(newInteger(42));
List<?>lu=li;
lu.clear();
泛型方法
(在类型参数一节中)您已经看到,通过在类的定义中添加一个形式类型参数列表,可以将类泛型化。方法也可以被泛型化,不管它们定义在其中的类是不是泛型化的。
泛型类在多个方法签名间实施类型约束。在List<V>中,类型参数V出现在get()、add()、contains()等方法的签名中。当创建一个Map<K,V>类型的变量时,您就在方法之间宣称一个类型约束。您传递给add()的值将与get()返回的值的类型相同。
类似地,之所以声明泛型方法,一般是因为您想要在该方法的多个参数之间宣称一个类型约束。例如,下面代码中的ifThenElse()方法,根据它的第一个参数的布尔值,它将返回第二个或第三个参数:
public<T>TifThenElse(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<VextendsNumber>{...}
编译器允许您创建Matrix<Integer>或Matrix<Float>类型的变量,但是如果您试图定义Matrix<String>类型的变量,则会出现错误。类型参数V被判断为由Number限制。在没有类型限制时,假设类型参数由Object限制。这就是为什么前一屏泛型方法中的例子,允许List.get()在List<?>上调用时返回Object,即使编译器不知道类型参数V的类型。
三一个简单的泛型类
编写基本的容器类
此时,您可以开始编写简单的泛型类了。到目前为止,泛型类最常见的用例是容器类(比如集合框架)或者值持有者类(比如WeakReference或ThreadLocal)。我们来编写一个类,它类似于List,充当一个容器。其中,我们使用泛型来表示这样一个约束,即Lhist的所有元素将具有相同类型。为了实现起来简单,Lhist使用一个固定大小的数组来保存值,并且不接受null值。
Lhist类将具有一个类型参数V(该参数是Lhist中的值的类型),并将具有以下方法:
publicclassLhist<V>{
publicLhist(intcapacity){...}
publicintsize(){...}
publicvoidadd(Vvalue){...}
publicvoidremove(Vvalue){...}
publicVget(intindex){...}
}
要实例化Lhist,只要在声明时指定类型参数和想要的容量:
Lhist<String>stringList=newLhist<String>(10);
实现构造函数
在实现Lhist类时,您将会遇到的第一个拦路石是实现构造函数。您可能会像下面这样实现它:
publicclassLhist<V>{
privateV[]array;
publicLhist(intcapacity){
array=newV[capacity];//illegal
}
}
这似乎是分配后备数组最自然的一种方式,但是不幸的是,您不能这样做。具体原因很复杂,当学习到底层细节一节中的“擦除”主题时,您就会明白。分配后备数组的实现方式很古怪且违反直觉。下面是构造函数的一种可能的实现(该实现使用集合类所采用的方法):
publicclassLhist<V>{
privateV[]array;
publicLhist(intcapacity){
array=(V[])newObject[capacity];
}
}
另外,也可以使用反射来实例化数组。但是这样做需要给构造函数传递一个附加的参数——一个类常量,比如Foo.class。后面在Class<T>一节中将讨论类常量。
实现方法
实现Lhist的方法要容易得多。下面是Lhist类的完整实现:
publicclassLhist<V>{
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<size;i++){
if(array[i].equals(value))
++removalCount;
elseif(removalCount>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<Integer>li=newLhist<Integer>(30);
编译器知道,li.get()返回的任何值都将是Integer类型,并且它还强制传递给li.add()或li.remove()的任何东西都是Integer。除了实现构造函数的方式很古怪之外,您不需要做任何十分特殊的事情以使Lhist是一个泛型类。
四Java类库中的泛型
集合类
到目前为止,Java类库中泛型支持存在最多的地方就是集合框架。就像容器类是C++语言中模板的主要动机一样(参阅附录A:与C++模板的比较)(尽管它们随后用于很多别的用途),改善集合类的类型安全是Java语言中泛型的主要动机。集合类也充当如何使用泛型的模型,因为它们演示了泛型的几乎所有的标准技巧和方言。
所有的标准集合接口都是泛型化的——Collection<V>、List<V>、Set<V>和Map<K,V>。类似地,集合接口的实现都是用相同类型参数泛型化的,所以HashMap<K,V>实现Map<K,V>等。
集合类也使用泛型的许多“技巧”和方言,比如上限通配符和下限通配符。例如,在接口Collection<V>中,addAll方法是像下面这样定义的:
interfaceCollection<V>{
booleanaddAll(Collection<?extendsV>);
}
该定义组合了通配符类型参数和有限制类型参数,允许您将Collection<Integer>的内容添加到Collection<Number>。
如果类库将addAll()定义为接受Collection<V>,您就不能将Collection<Integer>的内容添加到Collection<Number>。不是限制addAll()的参数是一个与您将要添加到的集合包含相同类型的集合,而有可能建立一个更合理的约束,即传递给addAll()的集合的元素适合于添加到您的集合。有限制类型允许您这样做,并且使用有限制通配符使您不需要使用另一个不会用在其他任何地方的占位符名称。
应该可以将addAll()的类型参数定义为Collection<V>。但是,这不但没什么用,而且还会改变Collection接口的语义,因为泛型版本的语义将会不同于非泛型版本的语义。这阐述了泛型化一个现有的类要比定义一个新的泛型类难得多,因为您必须注意不要更改类的语义或者破坏现有的非泛型代码。
作为泛型化一个类(如果不小心的话)如何会更改其语义的一个更加微妙的例子,注意Collection.removeAll()的参数的类型是Collection<?>,而不是Collection<?extendsV>。这是因为传递混合类型的集合给removeAll()是可接受的,并且更加限制地定义removeAll将会更改方法的语义和有用性。
其他容器类
除了集合类之外,Java类库中还有几个其他的类也充当值的容器。这些类包括WeakReference、SoftReference和ThreadLocal。它们都已经在其包含的值的类型上泛型化了,所以WeakReference<T>是对T类型的对象的弱引用,ThreadLocal<T>则是到T类型的线程局部变量的句柄。
泛型不止用于容器
泛型最常见最直观的使用是容器类,比如集合类或引用类(比如WeakReference<T>)。Collection<V>中类型参数的含义很明显——“一个所有值都是V类型的集合”。类似地,ThreadLocal<T>也有一个明显的解释——“一个其类型是T的线程局部变量”。但是,泛型规格说明中没有指定容积。
像Comparable<T>或Class<T>这样的类中类型参数的含义更加微妙。有时,就像Class<T>中一样,类型变量主要是帮助编译器进行类型推理。有时,就像隐含的Enum<EextendsEnum<E>>中一样,类型变量只是在类层次结构上加一个约束。
Comparable<T>
Comparable接口已经泛型化了,所以实现Comparable的对象声明它可以与什么类型进行比较。(通常,这是对象本身的类型,但是有时也可能是父类。)
publicinterfaceComparable<T>{
publicbooleancompareTo(Tother);
}
所以Comparable接口包含一个类型参数T,该参数是一个实现Comparable的类可以与之比较的对象的类型。这意味着如果定义一个实现Comparable的类,比如String,就必须不仅声明类支持比较,还要声明它可与什么比较(通常是与它本身比较):
publicclassStringimplementsComparable<String>{...}
现在来考虑一个二元max()方法的实现。您想要接受两个相同类型的参数,二者都是Comparable,并且相互之间是Comparable。幸运的是,如果使用泛型方法和有限制类型参数的话,这相当直观:
publicstatic<TextendsComparable<T>>Tmax(Tt1,Tt2){
if(t1.compareTo(t2)>0)
returnt1;
else
returnt2;
}
在本例中,您定义了一个泛型方法,在类型T上泛型化,您约束该类型扩展(实现)Comparable<T>。两个参数都必须是T类型,这表示它们是相同类型,支持比较,并且相互可比较。容易!
更好的是,编译器将使用类型推理来确定当调用max()时T的值表示什么意思。所以根本不用指定T,下面的调用就能工作:
Strings=max("moo","bark");
编译器将计算出T的预定值是String,因此它将进行编译和类型检查。但是如果您试图用不实现Comparable<X>的类X的参数调用max(),那么编译器将不允许这样做。
Class<T>
类Class已经泛型化了,但是很多人一开始都感觉其泛型化的方式很混乱。Class<T>中类型参数T的含义是什么?事实证明它是所引用的类接口。怎么会是这样的呢?那是一个循环推理?如果不是的话,为什么这样定义它?
在以前的JDK中,Class.newInstance()方法的定义返回Object,您很可能要将该返回类型强制转换为另一种类型:
classClass{
ObjectnewInstance();
}
但是使用泛型,您定义Class.newInstance()方法具有一个更加特定的返回类型:
classClass<T>{
TnewInstance();
}
如何创建一个Class<T>类型的实例?就像使用非泛型代码一样,有两种方式:调用方法Class.forName()或者使用类常量X.class。Class.forName()被定义为返回Class<?>。另一方面,类常量X.class被定义为具有类型Class<X>,所以String.class是Class<String>类型的。
让Foo.class是Class<Foo>类型的有什么好处?大的好处是,通过类型推理的魔力,可以提高使用反射的代码的类型安全。另外,还不需要将Foo.class.newInstance()强制类型转换为Foo。
考虑一个方法,它从数据库检索一组对象,并返回JavaBeans对象的一个集合。您通过反射来实例化和初始化创建的对象,但是这并不意味着类型安全必须完全被抛至脑后。考虑下面这个方法:
publicstatic<T>List<T>getRecords(Class<T>c,Selectors){
//UseSelectortoselectrows
List<T>list=newArrayList<T>();
for(/*iterateoverresults*/){
Trow=c.newInstance();
//usereflectiontosetfieldsfromresult
list.add(row);
}
returnlist;
}
可以像下面这样简单地调用该方法:
List<FooRecord>l=getRecords(FooRecord.class,fooSelector);
编译器将会根据FooRecord.class是Class<FooRecord>类型的这一事实,推断getRecords()的返回类型。您使用类常量来构造新的实例并提供编译器在类型检查中要用到的类型信息。
用Class<T>替换T[]
Collection接口包含一个方法,用于将集合的内容复制到一个调用者指定类型的数组中:
publicObject[]toArray(Object[]prototypeArray){...}
toArray(Object[])的语义是,如果传递的数组足够大,就会使用它来保存结果,否则,就会使用反射分配一个相同类型的新数组。一般来说,单独传递一个数组作为参数来提供想要的返回类型是一个小技巧,但是在引入泛型之前,这是与方法交流类型信息最方便的方式。
有了泛型,就可以用一种更加直观的方式来做这件事。不像上面这样定义toArray(),泛型toArray()可能看起来像下面这样:
public<T>T[]toArray(Class<T>returnType)
调用这样一个toArray()方法很简单:
FooBar[]fba=something.toArray(FooBar.class);
Collection接口还没有改变为使用该技术,因为这会破坏许多现有的集合实现。但是如果使用泛型从新构建Collection,则当然会使用该方言来指定它想要返回值是哪种类型。
Enum<E>
JDK5.0中Java语言另一个增加的特性是枚举。当您使用enum关键字声明一个枚举时,编译器就会在内部为您生成一个类,用于扩展Enum并为枚举的每个值声明静态实例。所以如果您说:
publicenumSuit{HEART,DIAMOND,CLUB,SPADE};
编译器就会在内部生成一个叫做Suit的类,该类扩展java.lang.Enum<Suit>并具有叫做HEART、DIAMOND、CLUB和SPADE的常量(publicstaticfinal)成员,每个成员都是Suit类。
与Class一样,Enum也是一个泛型类。但是与Class不同,它的签名稍微更复杂一些:
classEnum<EextendsEnum<E>>{...}
这究竟是什么意思?这难道不会导致无限递归?
我们逐步来分析。类型参数E用于Enum的各种方法中,比如compareTo()或getDeclaringClass()。为了这些方法的类型安全,Enum类必须在枚举的类上泛型化。
所以extendsEnum<E>部分如何理解?该部分又具有两个部分。第一部分指出,作为Enum的类型参数的类本身必须是Enum的子类型,所以您不能声明一个类X扩展Enum<Integer>。第二部分指出,任何扩展Enum的类必须传递它本身作为类型参数。您不能声明X扩展Enum<Y>,即使Y扩展Enum。
总之,Enum是一个参数化的类型,只可以为它的子类型实例化,并且这些子类型然后将根据子类型来继承方法。幸运的是,在Enum情况下,编译器为您做这些工作,一切都很好。
与非泛型代码相互操作
数百万行现有代码使用已经泛型化的Java类库中的类,比如集合框架、Class和ThreadLocal。JDK5.0中的改进不要破坏所有这些代码是很重要的,所以编译器允许您在不指定其类型参数的情况下使用泛型类。
当然,以“旧方式”做事没有新方式安全,因为忽略了编译器准备提供的类型安全。如果您试图将List<String>传递给一个接受List的方法,它将能够工作,但是编译器将会发出一个可能丧失类型安全的警告,即所谓的“uncheckedconversion(不检查转换)”警告。
没有类型参数的泛型,比如声明为List类型而不是List<Something>类型的变量,叫做原始类型。原始类型与参数化类型的任何实例化是赋值兼容的,但是这样的赋值会生成unchecked-conversion警告。
为了消除一些unchecked-conversion警告,假设您不准备泛型化所有的代码,您可以使用通配符类型参数。使用List<?>而不使用List。List是原始类型;List<?>是具有未知类型参数的泛型。编译器将以不同的方式对待它们,并很可能发出更少的警告。
无论在哪种情况下,编译器在生成字节码时都会生成强制类型转换,所以生成的字节码在每种情况下都不会比没有泛型时更不安全。如果您设法通过使用原始类型或类文件来破坏类型安全,就会得到与不使用泛型时得到的相同的ClassCastException或ArrayStoreException。
已检查集合
作为从原始集合类型迁移到泛型集合类型的帮助,集合框架添加了一些新的集合包装器,以便为一些类型安全bug提供早期警告。就像Collections.unmodifiableSet()工厂方法用一个不允许任何修改的Set包装一个现有Set一样,Collections.checkedSet()(以及checkedList()和checkedMap())工厂方法创建一个包装器(或者视图)类,以防止您将错误类型的变量放在集合中。
checkedXxx()方法都接受一个类常量作为参数,所以它们可以(在运行时)检查这些修改是允许的。典型的实现可能像下面这样:
publicclassCollections{
publicstatic<E>Collection<E>checkedCollection(Collection<E>c,Class<E>type){
returnnewCheckedCollection<E>(c,type);
}
privatestaticclassCheckedCollection<E>implementsCollection<E>{
privatefinalCollection<E>c;
privatefinalClass<E>type;
CheckedCollection(Collection<E>c,Class<E>type){
this.c=c;
this.type=type;
}
publicbooleanadd(Eo){
if(!type.isInstance(o))
thrownewClassCastException();
else
returnc.add(o);
}
}
}
由C++的模板函数引入自定义泛型,如下函数的结构很相似,仅类型不同: C++用模板函数解决,只写一个通用方法,它可以适应各种类型,示意代码如下: Tadd(Tx,Ty){return(T)(x+y);} 泛型是提供给Javac编译器使用的。可以限定集合中输入的类型,让编译器挡住原始程序的非法输入,编译器编译带类型说明的集合时会去掉“类型”信息,使程序运行效率不受影响,对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样,由于编译生成的字节码会去掉泛型的类型信息,只要能跳过编译器,就可以往某个泛型集合中加入其它类型的数据,例如,用反射得到集合,再调用其add方法即可。 |
|
ArrayList<E>类定义和ArrayList<Integer>类引用中涉及如下术语:
整个称为ArrayList<E>泛型类型,ArrayList<E>中的E称为类型变量或类型参数,整个ArrayList<Integer>称为参数化的类型,ArrayList<Integer>中的Integer称为类型参数的实例或实际类型参数,ArrayList<Integer>中的<>念着typeof,ArrayList称为原始类型。
参数化类型与原始类型的兼容性:
参数化类型可以引用一个原始类型的对象,编译报告警告,例如,
Collection<String>c=newVector();
原始类型可以引用一个参数化类型的对象,编译报告警告,例如,
Collectionc=newVector<String>();
参数化类型不考虑类型参数的继承关系:
Vector<String>v=newVector<Object>(),错误
Vector<Object>v=newVector<String>(),也错误。
泛型中的?通配符
问题:
定义一个方法,该方法用于打印出任意参数化类型的集合中的所有数据,该方法如何定义呢?
错误方式:
publicstaticvoidprintCollection(Collection<Object>cols){
for(Objectobj:cols){
System.out.println(obj);
}
cols.add(“string”);//没错
cols=newHashSet<Date>();//会报告错误
}
正确方式:
publicstaticvoidprintCollection(Collection<?>cols){
for(Objectobj:cols){
System.out.println(obj);
}
cols.add(“String”);//错误,因为它不知自己未来匹配就一定是String
cols.size();//没错,此方法与类型参数没有关系
}
总结:使用?通配符可以引用其他各种参数化的类型,?通配符定义的变量主要用于引用,可以调用与参数化无关的方法,不能调用与参数化有关的方法。
泛型中的?通配符的扩展
限定通配符的上边界:
正确:Vector<?extendsNumber>x=newVector<Integer>();
错误:Vector<?extendsNumber>x=newVector<String>();
限定通配符的下边界:
正确:Vector<?superInteger>x=newVector<Number>();
错误:Vector<?superInteger>x=newVector<Byte>();
提示:限定通配符总是包括自己。
自定义类泛型
publicclassTestgeneric2{
publicstaticvoidmain(String[]args){
MyPoint<Integer>p=newMyPoint<Integer>();
p.setX(2);
p.setY(3);
System.out.println(p.getX());
System.out.println(p.getY());
}
}
classMyPoint<TextendsNumber>{
Tx;
Ty;
publicTgetX(){
returnx;
}
publicvoidsetX(Tx){
this.x=x;
}
publicTgetY(){
returny;
}
publicvoidsetY(Ty){
this.y=y;
}
}
自定义方法泛型:
publicstatic<SextendsNumber>voidswap(S[]array,inti,intj){
Stemp=array[i];
array[i]=array[j];
array[j]=temp;
}
调用:
swap(newInteger[]{1,2,3},0,1);
Java中的泛型类型(或者泛型)类似于C++中的模板,但是这种相似性仅限于表面,Java语言中的泛型基本上完全是在编译器中实现,属于编译器执行类型检查和类型诊断,然后生成普通的非泛型的字节码,这种实现技术称为擦出。编译器使用泛型类型信息保证类型安全,然后在生成字节码之前将其清除,这是因为扩展虚拟机指令集来支持泛型被认为是无法接受的,这会为Java厂商升级其JVM造成难以逾越的障碍,所以,java的泛型采用了可以完全在编译器中实现的擦出方法。
注意:只有引用类型才能作为泛型方法的实际参数。
在泛型中可以同时有多个类型参数,在定义它们的尖括号中有逗号分开。
定义泛型类型:
如果类的实例对象中的多处都要用到同一个泛型参数,即这些地方引用的泛型类型要保持同一个实际类型时,这时候就要采用泛型类型的方式进行定义,也就是类级别的泛型,语法格式如下:
publicclass<E>GenericDao{
privateTfield1;
publicvoidsave(Tobj){}
publicTgetById(intid){}
}
类级别的泛型是根据引用该类名时指定的类型信息来参数化类型变量的。例如,如下两种方式:
GenericDao<String>dao=nulll
newgenericDao<String>;
importjava.util.*;
classGenericDAO<T>{
//Create
publicvoidadd(Tt){
}
//Retrieve
publicTfindById(intid){
returnnull;
}
publicTfindByName(Stringname){
returnnull;
}
publicTfindByNamePassword(Stringname){
returnnull;
}
//Update
publicvoidupdate(Tt){
}
//Delete
publicvoiddelete(Tt){
}
//根据编号删除
publicvoiddelete(intid){
}
//根据条件查询
publicSet<T>findByConditions(Stringquery){
returnnull;
}
//泛型方法
public<S>Stest(){
returnnull;
}
}
相关推荐
### Java泛型编程指南知识点详解 #### 一、绪论:理解Java泛型的重要性与背景 **1.1 泛型的基本概念** 泛型是一种在编程语言中支持编写类型安全的通用函数或类的能力。在Java中引入泛型的主要目的是为了提供更...
Java泛型的用法及T.class的获取过程解析 Java泛型是Java编程语言中的一种重要特性,它允许开发者在编写代码时指定类型参数,从而提高代码的灵活性和可读性。本文将详细介绍Java泛型的用法 及T.class的获取过程解析...
Java 泛型详解 Java 泛型是 Java SE 5.0 中引入的一项特征,它允许程序员在编译时检查类型安全,从而减少了 runtime 错误的可能性。泛型的主要优点是可以Reusable Code,让程序员编写更加灵活和可维护的代码。 ...
Java泛型是Java编程语言中的一个强大特性,它允许我们在定义类、接口和方法时指定类型参数,从而实现代码的重用和类型安全。在Java泛型应用实例中,我们可以看到泛型如何帮助我们提高代码的灵活性和效率,减少运行时...
Java泛型机制详解 Java泛型是Java语言中的一种机制,用于在编译期检查类型安全。Java泛型的出现解决了Java早期版本中类型安全检查的缺陷。Java泛型的好处是可以在编译期检查类型安全,避免了运行时的...
综上所述,虽然Java泛型在编译后会进行类型擦除,但通过上述技巧,我们仍然能够在运行时获得关于泛型类实例化类型的一些信息。在实际开发中,这些方法可以帮助我们编写更加灵活和安全的代码。在示例文件`GenericRTTI...
Java泛型是Java编程语言中的一个关键特性,它在2004年随着Java SE 5.0的发布而引入,极大地增强了代码的类型安全性和重用性。本篇文章将深入探讨Java泛型的发展历程、核心概念以及其在实际开发中的应用。 1. **发展...
Java泛型是Java编程语言中的一个关键特性,它在2004年随着JDK 5.0的发布被引入。这个特性极大地提高了代码的类型安全性和可读性,减少了在运行时出现ClassCastException的可能性。SUN公司的Java泛型编程文档,包括...
下面我们将详细探讨Java泛型接口的相关知识点。 1. **泛型接口的定义** 泛型接口的定义方式与普通接口类似,只是在接口名之后添加了尖括号`<T>`,其中`T`是一个类型参数,代表某种未知的数据类型。例如: ```java...
下面我们将深入探讨Java泛型方法的概念、语法以及使用示例。 **一、泛型方法概念** 泛型方法是一种具有类型参数的方法,这些类型参数可以在方法声明时指定,并在方法体内部使用。与类的泛型类似,它们提供了编译时...
Java 泛型是一种强大的工具,它允许我们在编程时指定变量的类型,提供了编译时的类型安全。然而,Java 的泛型在运行时是被擦除的,这意味着在运行时刻,所有的泛型类型信息都会丢失,无法直接用来创建对象或进行类型...
Java泛型是Java编程语言中的一个强大特性,它允许在定义类、接口和方法时使用类型参数,从而实现参数化类型。这使得代码更加安全、可读性更强,并且能够减少类型转换的必要。在“java泛型的内部原理及更深应用”这个...
这是一个使用JAVA实现的泛型编程,分为两部分,第一部分创建泛型类,并实例化泛型对象,得出相加结果。 第二部分用户自行输入0--4,选择要进行的加减乘除运算或退出,再输入要进行运算的两个数,并返回运算结果及...
### Java泛型总结 #### 一、Java泛型概述 Java泛型是在JDK5之后引入的一个特性,它提供了一种类型安全的机制,用于指定集合或其他数据结构中的元素类型。通过使用泛型,程序员可以在编译阶段检测类型错误,避免了...
"Java 泛型学习" Java 泛型是 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的...
Java 泛型使用详细分析 Java 泛型是 Java 语言中的一种类型系统特性,允许开发者在编译期检查类型安全,以避免在运行时出现类型相关的错误。在本文中,我们将详细介绍 Java 泛型的使用方法和实现原理。 一、泛型的...
Java泛型是Java编程语言中的一个关键特性,它在2004年随着Java SE 5.0的发布而引入,极大地增强了代码的类型安全性和重用性。本篇文章将深入探讨Java泛型的发展历程、核心概念以及其在实际开发中的应用。 1. **发展...
本文将深入探讨Java泛型类型擦除的概念,并介绍在类型擦除后,为了保持泛型的安全性和便利性,Java设计者所采取的一些补偿机制。 1. **类型擦除**: - 在编译期间,所有的泛型类型信息都会被替换为它们的实际类型...