文章目录
一般的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义类型。如果要编写可以用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
在面向对象的编程语言中,多态算是一种泛化机制。例如,如果方法参数使用的是基类,那么这个方法可以适用的类和地方就会更多,这样就具备更好的灵活性。但是final类不能扩展,其他任何类都可以扩展。
Java中是单继承的,所以上述方法还是受到很大限制。如果方法的参数是一个接口,而不是一个类,这种限制就放松了许多。即只要实现了该接口的类都适合该方法,包括暂时还不存在的类。
可是有的时候即便使用了接口,对程序也还是太束缚。因为一旦指明了接口,那么该方法只能适用实现该接口的类。要想的目的是方法可以适用更多的类或接口,可以是已经出现的类或接口,也可以是暂时还没出现的类或接口。Java SE5的重大变化之一就是引入了泛型的概念。泛型实现了参数化类型的概念,使代码能够应用于多种类型。“泛型”这个语术的意思是:“适用许多许多的类型”。
泛型最初的目的是希望类或方法能够具备最广泛的表达能力。如何做到这一点呢?正是通过解耦类或方法与所使用的类型之间的约束。
简单泛型
有许多原因促成泛型的出现,而最引人注目的是一个原因就是为了创造泛型容器。
Java是单根继承,并且所有类都继承Object类,所以可以把所有类的对象都赋值给Object类型的引用,可以用Object类型的引用来“存储”对象。
有些情况下,确实希望容器能够同时持有不同类型对象。但是,通常而言,我们只会使用容器存储一种类型的对象。泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。
因此,与Object相比,我们更喜欢暂时不指定类型,而是稍后在决定其具体使用什么类型。要达到这一目的,需要使用类型参数,用尖括号括住,放在其类名的后面(近根其后)。
class Holder<T>{
private T t ;
public void setT( T t){
this.t = t ;
}
public T getT(){
return t ;
}
}
然后在使用这个类的时候,用具体的类型来替换此类型参数。当创建Holder对象时,必须指明想要持有什么类型的对象,将其置于尖括号内。然后只能在Holder中存入该类型(或其子类,因为多态和泛型不冲突)的对象。并且,从Holder中取出它持有的对象时,自动地就是正确的类型。
这就是Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮助你处理一切细节。
元组
元组:它是将一组对象直接打包存储于其中的一个单一对象。这个容器对象允许读取其中元素,但是不允许向其中存放新的对象。(这个概念也称为数据传递对象或信使。)
元组可以具有任意长度,同时,元组中的对象可以是任意类型不同类型。不过,我们希望能够为每一个对象指明其类型,并且从容器中取出来时,能够得到正确的类型。要求处理不同长度的问题,我们需要创建多个不同的原组。例:一个二维数组
public class TwoTuple<A,B>{
public final A a ;
public final B b ;
public TwoTuple(A a , B b){
this.a = a ;
this.b = b ;
}
public A getA(){
return a ;
}
public B getB(){
return b ;
}
}
字段a, b 权限虽然时public的,但它们同时被定义为final的,所以不能改变a,b的值。这样是安全的。
通过继承实现长度不同的原组。
public class ThreeTuple<A,B,C> exenteds TwoTuple<A,B>{
public final C c ;
public ThreeTuple(A a , B b , C c){
super(a,b);
this.c = c ;
}
}
public class FourTuple<A,B,C,D> exenteds TwoTuple<A,B,C>{
public final D d ;
public FourTuple(A a , B b , C c , D d){
super(a,b,c);
this.d = d ;
}
}
泛型接口
泛型也可以应用于接口。例如生成器(generator),这是一种专门负责创建对象的类,实际上,这是工厂方法设计模式的一种应用。不过生成器生成对象时,它不需要额外的参数,而工厂方法需要额外的参数。也就是说,生成器不需要额外的信息就知道如何创建新对象。
一般而言,一个生成器只定义一个方法,该方法用以生成新的对象。
public interface Generator<T>{
T next();
}
可见,定义泛型接口和定义泛型类一样。在接口名后近根尖括号<>,并将类型参数至于尖括号类。
泛型方法
可以在类中包含参数化方法,而这个方法所在的类可以是泛型,也可以不是泛型类,也就是说,是否拥有泛型方法和包含这个方法的类是否是泛型类没有关系。
泛型方法使得该方法能够独立于类而产生变化。以下是一个基本的指导原则:无论何时,只要你能做到,你就应该尽量使用泛型方法。即如果泛型方法能取代整个类,那么应该尽量只使用泛型方法,因为它可以使事情更加清楚。另外对于一个static的方法而言,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。
定义泛型方法,只需要将泛型参数列表置于返回值之前(仅前一步),例:
public class GenericMethods{
public <T> void f(T t){
}
}
这个类不是参数化的。尽管这个类和其内部类的方法可以被同时参数化,但是在这个例子中,只有方法f()拥有类型参数。
注意:当使用泛型类时,必须在创建对象的时候指定类型参数的值。而使用泛型方法时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数推断。
显示的类型说明
在泛型方法中,可以显示的指明类型,不过这种语法很少用。要显示的指明类型,必须要:
- 在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内;
public class Test{
GenericMethods gm = new GenericMethods();
public static void mian(String[] arg){
gm.<String>f(arg[0]);
}
}
- 如果在定义方法的类的内部,必须在点操作符之前使用this关键字;
public class Test{
public static void mian(String[] arg){
this.<String>f(arg[0]){
}
}
public static <T> void f(T t){
...
}
}
- 如果是使用static的方法,必须在点操作符之前加上类名。
public class Test{
public static <T> void f(T t){
...
}
}
public class TestTwo{
public static void mian(String[] arg){
Test.<String>f(arg[0]){
}
}
}
可变参数与泛型方法
泛型方法和可变参数列表能够很好的共存
public calss GenericVarags{
public static <T> void makelist(T ...args ){
}
}
擦除的神秘之处
我们在代码中可以声明ArrayList.class,但是不能声明ArrayList< String >.calss。看一个例子:
public class Test{
public static void mian(String[] arg){
Class cl1 = new ArrayList<Integer>.getClass() ;
Class cl2 = new ArrayList<String>.getClass() ;
System.out.println( cl1 == cl2 );
}
}
// log
true
输出的结果可能会令我们惊讶,结果既然为true。为什么?往ArrayList< Integer >不是只能添加Integer对象吗?往ArrayList< String >不是只能添加String对象吗?行为都不一样,Class对象为什么会相等呢?在看一个示例:
class Frod{
}
class Quark<Q>{
}
public class Test{
public static void mian(String[] arg){
Frod<Frod > q = new Quark<Frod>();
System.out.println(Arrays.toString(q.getClass().getTypeParameters()));
}
}
//log
[Q]
Class.getTypeParameters()返回一个TypeVariable对象数组,表示有泛型声明所声明的类型参数。字面意思理解好像是可以知道参数类型的信息。但从日志中看到,只是看到参数占位符的标识符,这些信息并没有什么用。因此,现实是:
在泛型代码内部,无法获得任何有关泛型参数类型的信息。因此,我们可以知道类型参数标示符和泛型类型边界这类的信,但是我们无法知道用来创建某个特定实例的实际的类型参数。
Java泛型是使用擦除来实现的,这意味着当使用泛型时,任何具体的类型信息都被擦除了,唯一知道的就是我们在使用一个对象。因此ArrayList< Integer >和 ArrayList< String >在运行时是相同的类型。这两种类型都被擦除到它们的“原生”类型,即ArrayList。
看一个C++例子:
template<class T> class Manipulator{
T obj ;
public :
Manipulator(T x ){
obj = x ;
}
void manipulate(){
obj.f();
}
}
class HasF{
public:
void f(){
cout << "HasF::f()">>end ;
}
}
int main(){
HasF hf ;
Manipulator<HasF> manipulator(hf);
manipulator.manipulate();
}
发现在manipulate函数中,它在obj上调用方法()。它怎么知道f()方法是为类型参数T而存在的呢?C++编译器会进行检查,当在Manipulator被实例化的这一刻,它看到HasF类有一个f()方法,如果没有,就会得到一个编译期错误,因此类型安全就得到了保障。
Java 的泛型是使用擦除实现的,所以Java中不能这样写。示例
class HasF{
public void f(){
cout << "HasF::f()">>end ;
}
}
class Manipulator<T>{
private T obj ;
public Manipulator(T obj){
this.obj = obj ;
}
// 不能这样写
public void manipulate(){
obj.f();
}
}
由于擦除的原因,Java编译器无法将manipulate()必须能够在obj上调用f()方法这一需求映射到HasF拥有f()这一事实上。如果真的需要调用f()方法,需要给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。例:
class Manipulator2<T extends HasF>{
private T obj ;
public Manipulator(T obj){
this.obj = obj ;
}
public void manipulate(){
obj.f();
}
}
边界< T extends HasF >声明T必须是类型HasF或从HasF导出的类型。在实例化Manipulator2实例时,如果情况确实如在,那么就可以安全的在obj上调用f(),因为HasF类或是HasF的导出类,都必有f()方法。如果类型参数不是类型HasF或从HasF导出的类型,那么会得到一个编译期错误,因此类型安全就得到了保障。
泛型类型参数将擦除到它的第一个边界(它可能会有多个边界)。编译器实际上会把类型参数替换为它的擦除。如上面,T擦除到了HasF,就好像在类中声明中用HasF替换了T一样。既然这样,那么泛型< T extends HasF >并没有给我们带来多大的好处,我们可以自己做简单的擦除就能实现上面的效果。例:
class Manipulator3{
private HasF obj ;
public Manipulator(T obj){
this.obj = obj ;
}
public void manipulate(){
obj.f();
}
}
所以,这里提出了重要一点:只有当我们希望使用的类型参数必某个具体类型(以及它的所有子类)更加“泛型”时——也就是说,当我们希望代码能够跨多个类工作时,使用泛型才有所帮助。因此,类型参数和它们在有用的泛型代码中的应用,通常比具体的类型更加复杂。
当Manipulator2中有一个返回泛型参数T的方法,那么我们做的简单擦除用HasF替换T就没有使用泛型那么复杂了,即使用泛型会更加有用。
擦除的问题
泛型类型只有在静态的类型检查期间才出现,在此之后,程序中所有泛型都将被擦除,替换为它们的非泛型上界。例如,List< T>这样的类型注解将被擦除为List,而普通的类型变量(T obj)在未指定边界的情况下将被擦除为Object。
擦除的核心动机是它使得泛化的客户端代码可以用非泛型的类库来使用,这被称为“迁移兼容性”。擦除的主要正当理由是从非泛化的代码到泛化的代码的转变过程,以及在不破环现有类库的情况喜爱,将泛型融入Java语言。擦除的代价是显住的。泛型不能用于显式地引用运行时类型的操作之中,例如转型instanceof(if(T instanceof String))操作和new( new T())表达式。因为所有关于参数的类型信息都丢失了,无论何时,当我们在编写泛型代码时,必须时刻提醒自己,我们只是看起来拥有有关类型信息而已。例:
class Foo<T>{
T var ;
}
Foo<Cat> f = new Foo<Cat>();
我们可能会认为 class Foo中的代码应该知道现在工作于Cat之上,而泛型语法也在强烈暗示:在整个类中的各个地方,类型T都在被替换。但事实并非如此,无论何时,在我们编写这个类的代码时,必须提醒自己,T只是一个Object。
边界处的动作
class FilledList<T>{
public List<T> creatList( T t , int size){
List<T> list = new ArrayList<T>();
for(int i = 0 ; i < size ; i++){
list.add(t);
}
return list ;
}
public void mian(String[] arg){
FilledList<String> f = new FilledList<String>();
List<String> lis = f.creatLis("He" , 3);
System.out.println(lis));
}
}
// log
[He,He,He]
即使编译器无法知道creatList()中T的任何信息,但是它仍旧可以在编译器确保放置到容器中的对象具有T类型,使其适合Arraylist。因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界。在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。边界就是发生动作的地方。
擦除的补偿
擦除丢失了在代码中执行某些操作的能力。任何在运行时需要知道确切类型信息的操作都将无法工作。如前面说的instanceof和new,因为其类型信息已经被擦除了。偶尔可以绕过这些编程,但是有时必须通过引入类型标签来对擦除进行补偿。这意味着你需要显示地传递你的C lass对象,以便你可以在类型表达式中使用它。
如instanceof,如果我们使用类型标签,就可以使用动态的instanceof,即isInstanceof()。例
class Building{
}
class House extends Building{
}
public class Test<T>{
Class<T> typeClass ;
public Test(Class<T> type){
typeClass = type ;
}
boolean f(Object o){
return typeClass.isInstanceof(o);
}
publid void mian(String[] arg){
Test<Building> t = new Test<Building>(Building.class);
System.out.println(t.f(new Building()));
System.out.println(t.f(new House()));
Test<House> t2 = new Test<Building>(House.class);
System.out.println(t2.f(new Building()));
System.out.println(t2.f(new House()));
}
}
// log
true
true
false
true
创建类型实例
我们不能直接 new T() ;(T是一个类型参数表示符)。失败的部分原因是擦除,还有一部分是不能确保T类有默认的构造方法。但是在C++中这种操作很自然、很直观、也很安全(它在编译器会受到检查)
Java中如何解决呢?可以使用类型标签,Class.newInstance()来创建这个类型的新对象。但是使用newInstance()有一个要求,类必须要有默认的构造方法。如果没有默认的构造方法,运行newInstance()就会得到一个运行时异常,在编译器并不知道,所以直接使用类型标签通过newInstance()来创建这个类型的新对象并不是很友好。
还有一种方式使用显示的工厂,并将限制其类型,使其只能接受实现这个工厂的类(即给类型参数添加边界)。示例:
interface FactoryI<T>{
T create();
}
class Foo2<T>{
private T x ;
public <F extends FactoryI<T>> T Foo2(F factory){ //只能接受实现这个工厂的类
x = factory.create();
}
}
//具体工厂类
calss IntegerFactory implements FactoryI<Integer>{
public Integer create(){
new Integer(0);
}
}
calss Widget {
puiblic static class Factory implements FactoryI<Widget>{
public Widget create(){
new Widget();
}
}
}
public class FactoryConstranint{
public static void mian(String[] arg){
new Foo2(new IntegerFactory());
new Foo2(new Widget.Factory());
}
}
这里只是传递了Class的一种变体。创建什么对象是由实现了工厂接口的具体工厂来负责完成的,即传这种显示的工厂对象来完成对象的创建。Class也是一种工厂对象,这只不过是内置的工厂对象,而这个内置的工厂对象使用newInstance()可能会产生运行时异常。而使用自己编写的显示的工厂对象,可以得到编译期的类型检查安全保障。
还有一种方式是使用模版方法设计模式,把create方法设置为模版方法(抽象的),由具体的子类来实现。示例:
abstract class GenericWithCreate<T>{
public T t ;
GenericWithCreate(
t = create();
)
abstract T create();
}
class Hot{
}
// 子类
calss Test extends GenericWithCreate<Hot>{
public Hot create(){
new Hot();
}
void f(){
System.out.pirntln(t.getClass().getSimpleName());
}
}
泛型数组
我们不能直接创建一个泛型数组,T[] ts = new T[];。一般的解决方法是在需要使用泛型数组的地方使用ArrayList来代替,如ArrayList< T> 。这样可以获得数组的行为,以及由泛型提供的编译期的类型安全。
但是如果我们确实希望创建一个泛型数组,那该怎么办呢?如ArrayList底层就是用数组实现的。
我们按照编译器认可的方式定义一个引用,如:
class Generic<T>{
}
public class Test{
public Generic<Integer>[] gi ;
}
这样定义是可以通过编译器编译的,但是我们确不能创建这个确切类型的数组,即不能创建
Generic类型的数组(gi = new Generic[])。想要成功创建这个泛型数组的唯一方式是,创建一个被擦除类型的新数组,然后对其转型。如:
gi = [Generic<Integer>]new Generic[4];
如果我们尝试获取gi的类型来看一下就会发现,gi的实际类型是Generic[],而不是Generic[]类型。我们可能会想,无论数组持有什么类型的对像,在底层它们都具有相同的结构,我们可以创建一个Object类型的数组,然是转型(gi = new Object[4])。这个在编译期我们会得到一个警告,我们可以用注解来压住这个警告。但是我们运行程序后得到一个类型转换错误的异常。因为数组会跟踪它们的实际类型,而这个类型是在数组创建的时候就要确定好的。即如果查创建Object数组,那它的实际类型就有Object[]。创建Generic类型数组,它的实际类型就是Generic[]类型的数组。
我们可以定义泛型数组引用,然后创建一个Object类型数组转型为泛型数组,但这会得到一个转型的警告,可以用注解压住这个警告。示例:
T[] ts ;
ts = [T[]]new Object();
这在编译器只会得到一个类型转换警告,用注解压住警告即可。那为什么在运行时并不会抛出类型转换错误呢?因为擦除,T[]数组在运行时类型就只能是Object[],所以在运行时不会抛出类型转换错误。如果我们将ts返回类某个非Object数字的引用,如:
public Integer[] getArray(){
return ts;
}
Integer[] igs = getArray();
这样会得到一个类型转换错误,因为igs是以Integer[]类型来接受数组的,但数组的实际类型是Object[],所以会抛出异常。就算在类型定义的是Object[]类型的数组,然后存放某泛型T类型的对象(会产生转型警告,用注解压住)。在获取数组中的元素转型为T,这样是安全的。但如果将整个数组Object[]转型为T[]类型,依然是不正确的,会抛出类型转换错误。因为数组的实际类型是Object[]类型的。所以没用办法推翻数组的实际类型就是Object[]的事实,因为数组会跟踪它们的实际类型,而这个类型是在数组创建的时候就要确定好的(这里创建的是Object[]类型的数组)。
那因该怎么做,在返回整个数组的时候类型是正确的确切类型呢?使用类型标签Class< T>。示例:
calss TestArray<T>{
private T[] tArray ;
@SuppressWarnings("unchecked")
public TestArray(Class<T> ct , int szie){
tArray = (T[])Array.newInstance(ct,size);
}
public void put(T t, int index){
tArray[index] = t ;
}
public T get( int index){
return tArray[index] ;
}
public T[] rep( ){
return tArray ;
}
public static void mian(String[] arg){
TestArray<Inetger> test = new TestArray(Inetger.class , 4);
Inetger[] is = test.rep();
}
}
边界
边界使得我们可以在泛型的参数类型上设置限制条件。尽管这使得我们可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是我们可以按照自己的边界类型来调用方法。
因为擦除移除了类型信息,所以可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。T t 被擦为Object ,所以t只能调用Object有的方法。如果能够将这个参数限制为某个类型或这个类型的子类,那么我们就可以用这些类型子集来调用方法。可以使用extends。示例:
// T可以是A或A的子类
class Testi<T extends A>{
}
// T需要即使A类型又是B类型(T实现了A,又实现了B)
class Testi<T extends A & B>{
}
// T需要即使A类型又是B类型又要是C类型(T继承了A,实现了B,又实现了C)
class Testi<T extends A & B & C>{
}
也可以使用即成
class Test<T extends A > extends B<T>{
}
class Test<T extends A & B> extends C<T>{
}
通配符
通配符在泛型中用问号(?)来表示。
我们先来看数组的一个特殊行为:可以把导出类型的数组赋值给基类型的数组引用。 示例:
class A {
}
class B extends A{
}
clase C extends B{
}
class D extends A{
}
public class ArrayTest{
public static void mian(String[] arg){
A[] a = new B[4] ; // B是导出类,A是基类,对于数组来时这是允许的。
a[0] = new B() ;
a[1] = new C() ;
try{
a[2] = new A() ;
}cach(Exception e){
System.out.printlne(e) ;
}
try{
a[3] = new D() ;
}cach(Exception e){
System.out.printlne(e) ;
}
}
}
发现在编译器完全没问题。但是运行这个程序时,发现会有异常信息打印出来。我们来分析一下这个程序。首先创建了一个B[]类型的数组(这个数组的实际类型,这个数组应该持有的对象类型)对象,然后赋值给A[]类型的数组的引用a。由于数组的实际类型是B[]类型,所以把B类型及其子类型的对象存入数组中是完全没有问题。由于数组对象的引用是A[]类型的,可以把A类型及其子类型放入数组中, 所以在编译期阶段是安全的。但在运行时,由于数组的实际类型是B[]的,所以在运行时把A和D类型的对象放入数组中就会出错,从而抛出异常java.lang.ArrayStoraExcption:A和java.lang.ArrayStoraExcption:D。所以很明显,数组对象可以保留有关它们包含(可持有)的对象类型的规则。
泛型的主要目的之一就是将这种错误检测移入到编译期。我们尝试使用泛型的容器来代替数组,示例:
ArrayList<A> aList = new ArrayList<B>();
但实际上这是不允许的。为什么?我们首次理解可能会是,不能将持有B的容器赋值给持有A的容器。但泛型不仅和容器相关,正确的说法是:不能把一个持有B的泛型容器赋值给持有A的泛型容器。可能会认为是向上转型,但实际上不是向上转型,虽然B继承A,但ArrayList并没有继承ArrayList< A>,所以不是向上转型。所以这两者不是等价的。泛型与数组不同,泛型没有内建的协变参数。
如果我们想要两个类型之间建立某种类型的向上转型关系,这正是通配符所允许的。示例:
public class Test{
public static void mian(String[] arg){
ArrayList<? entends A> as = new ArrayList<B>();
// as.add(new A());
// as.add(new B());
// as.add(new C());
// as.add(new D());
// as.add(new Object());
A a = as.get(0);
}
}
List<? entends A>可以将其读为:具有任何从A继承的类型的列表。但实际上并不意味着这个List持有任何类型的A。通配符引用的是明确的类型,他指的是:某种A引用没有指定的的具体类型。如程序中看到,不能向as中添加A类型的对象,甚至Object类型。但是从as中取出的类型一定具有A类型,这是可以保障的。
现在事情好像变得有点极端,因为我们现在甚至不能象as中添加A类型的对象。
编译器有多聪明
由于上面的情况,我们可能会猜想我们已经被阻止调用任何接受参数的方法。请看示例:
public class Test{
List<? extends A> als = Arrays.asList(new B());
B b = (B)als.get(0);
als.contains(new B());
als.indexOf(new B());
}
可以发现,contains()和indexOf()都能得到正常的执行,所以我们的猜想被推翻不成立。那么这是否意味着编译器实际上将检查代码,以查看是否某个特定的方法修改了它的对象?
并非如此,编译器没有那么聪明。contains()和indexOf()方法的参数都是Object类型的,
而add()方法的参数是泛型参数类型的,所以我们指定List<?extends A>时,add()的参数就变成了: ? extends A ,这意味着它可以是A的任何一个具体的子类,而编译器无法验证“任何事物”的类型安全性,所有add()不回接受任何类型的A。就是算是把B上向转型为A也无济于事——编译器将直接拒绝对参数列表中涉及通配符的方法调用(如add())。
contains()和indexOf()方法的参数是Object类型的,因此不设计任何通配符,而编译器也将允许这个调用。这也意味着将由泛型类的设计着来决定哪些调用是“安全的”,并使用Object类型来作为参数的类型。
超类型通配符
可以声明通配符是某个特定类的任何基类来界定,即<?super MyClass>,甚至<? super T>,但不能对泛型参数T给出一个超类型边界,即< T super MyCLass>。有了超类通配符,我们就可以做一下前面说的某个特定类的子类的泛型<? extends MyClass>不能做的一些操作。示例:
class Test{
private ArrayList< ? super B> as = new ArrayList();
public static void mian(String[] agr){
as.add(new B());
as.add(new C());
}
}
这里容器as存放的是B的某个具体超类,所以向as添加B类型是完全的,由于C继承B,即C一定可以向上转型为B的某个具体超类,所以把C类型对象添加到as中也是安全的。
因此,我们可以根据如何能够向一个泛型“写入”(传递给一个方法),以及如何能够从一个泛型类中“读取”(从一个方法返回),来着手思考子类型和超类型边界。示例:
class Test{
public <T> void write(List< T> list , T item){
list.add(item);
}
ArrayList<A> as = new ArrayList<A>();
ArrayList<B> bs = new ArrayList<B>();
void f(){
write(bs , new B());
//即使我们知道这是可以的,但编译会直接拒接,类型参数不一至,容器持有的是A类型,
//但添加的对像是B类型。
// write(as , new B));
}
// 要想做到上面的效果,应该这样写。
public <T> void writeTwo(List<? super T> list , T item){
list.add(item);
}
void f(){
writeTwo(bs , new B());
writeTwo(as , new B));
}
}
无界通配符
无界通配符<?>看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。实际上,它是在声明:它是想用Java泛型类编写这段代码,我在这里并不是要用原生类型,但是当前这种情况下,泛型参数可以持有任何类型。
无界通配符的一个重要应用是:在处理多个泛型时,有时允许一个参数可以是任何类型,同时为其他参数确定某种特定类型的这种能力会显得很重要。如:
Map<? , String> map = new HasMap<Integer , String>();
List(原生类)表示:持有任何Object类型的原生List;
List<?>表示:具有某种特定类型的非原生List,只是我们不知道那种类型是什么。
class A<T>{
T t ;
public A(){
}
public A( T t ){
this.t = t ;
}
public void setT(T t){
this.t = t ;
}
public T getT(){
return t ;
}
}
class Test{
publci static void main(){
}
publi void f(A a , Object obj){
//a.setT(obj); // 会产生警告(可以压住)
Object o = a.getT();
}
publi void f2(A<?> a , Object obj){
// a.setT(obj); // 会产生错误(压不住)
Object o = a.getT();
}
}
我们可能会认为A 和A<?>是相同的。但是从f2()中看到,编译器会拒绝setT()操作。因为原生A将持有任何类型的组合。而A<?>将持有具有某种具体类型的同构集合,因此不能只是向其中传递Object。
搏获转型
有一种情况特别需要使用<?>而不是原生类。如果向一个使用<?>的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法。示例:
public class Test{
static <T> void f1(A<T> at ){
T t = at.getT();
System.out.printel(t.getClass().getSimpleName());
}
static void f2(A<?> at ){
f1(at);
}
@SuppressWarnings("unchecked")
public static void mian (String[] arg){
A a = new A<Integer>(1);
f2(a);
A a1 = new A();
a1.setT(new Object);
f2(a1);
A<?> a3 = new A<Long>(1L);
f2(a3);
}
}
//log
Integer
Object
Long
f1()需要一个确切类型的参数,f2()参数是一个无界通配符。在f2()中调用了f1()。这里发生的是:参数类型在调用f2()的过程中被捕获,因此它可以在对f1()的调用中被使用。
搏获转型只有在这样的情况下可以工作:在方法内部,需要使用确切的类型。
问题
- 任何基类都不能作为类型参数。如
List< int> il ;
- 一个类不能同时实现一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。如:
interface A<T>{
void a();
}
class B extends A<Hot>{
void a(){
}
}
class D extends B implements A<Cat>{
void a(){
}
}
这是不能编译的,因为擦除会是A和A简化为相同的类A,这样D就相当于实现了接口A两次。
- 使用带有泛型类型参数的转型或Instanceof不会有任何效果。
- 重载
class Test<T,U>{
void f(List<T> lt){}
void f(List<U> lu){}
}
由于擦除的原因,重载方法将产生相同的类型签名。
- 基类劫持了接口
class A implements Comparable<A>{
public int compareTo(A a){
return 0 ;
}
}
// Error
class B extends A implements Comparable<A>{
public int compareTo(B b){
return 0 ;
}
}
这是不能编译的,一旦为Comparable确定了A参数,那么其他的任何实现类都不能与A之外的任何对象比较。如下:
class C extends A<A>{
public int compareTo(A a){
return 0 ;
}
}
class D extends A<A>{
public int compareTo(A a){
return 0 ;
}
}