CoreJava Reading Note(8:Generic programming)
1.定义简单泛型类
一个泛型类(generic class)就是具有一个或多个类型变量的类。本章使用一个简单的Pair类作为例子。下面是Pair类的代码:
public class Pair<T>{ private T first; private T second; public Pair(){ first=null;second=null;} public Pair(T first,T second){this.first=first;this.second=second;} public T getFirst(){return first;} public T getSecond(){return second;} public void setFirst(T newValue){first=newValue;} public void setSecond(T newValue){second=newValue;}
Pair类引入了一个类型变量T,用尖括号(<>)括起来,并放在类名的后面。
泛型类可以有多个类型变量。
public class Pair<T,U>{...}
注:类型变量使用大写形式,且比较短,这是很常见的。在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型。T(需要时还可以用临近的字母U和S)表示“任意类型”。
换句话说,泛型类可看作普通类的工厂。
2.泛型方法
前面已经介绍了如何定义一个泛型类。实际上,还可以定义一个带有类型参数的简单方法。
class ArrayAlg{ public static <T> T getMiddle(T... a){ return a[a.length/2]; } }
这个方法是在普通类定义的,而不是在泛型类定义的。
注意,类型变量放在修饰符(这里是public static)的后面,返回类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:
String middle=ArrayAlg.<String>getMiddle("John","Q.","Public");
在这种情况(实际上也是大多数情况)下,方法调用中可以省略<String>类型参数。编译器有足够的信息能够推断出所调用的方法。它用names的类型(即String[])与泛型类型T[]进行匹配并推断出T一定是String。也就是说,可以调用:
String middle=ArrayAlg.getMiddle("John","Q.","Public");
几乎在大多数情况下,对于泛型方法的类型引用没有问题。偶尔,编译器也会提示错误,此时需要解释错误报告。看一看下面这个示例:
double middle=ArrayAlg.getMiddle(3.14,1729,0);
错误信息会以晦涩的方式指出:解释这句代码有两种方法,而且这两种方法都是合法的。简单地说,编译器会自动打包参数为1个Double和2个Integer对象,而后寻找这些类的共同超类型。事实上,找到2个这样的超类型:Number和Comparable接口,其本身也是一个泛型类型。在这种情况下,可以采取的补救措施是将所有的参数写为double值。
注:如果想知道编译器对一个泛型方法调用最终推断出哪种类型,这里有一个窍门:有目地地引入一个错误,并研究产生的错误信息。例如,看一下调用ArrayAlg.getMiddle("Hello",0,null)。将结果赋给JButton,这不可能正确。将会得到一个错误报告:
found: java.lang.Object&java.io.Serializable&java.lang.Comparable<? extends java.lang.Object&java.io.Serializable&java.lang.Comparable<?>>
大致的意思是:可以将结果赋给Object,Serialiable或Comparable。
3.类型变量的限定
有时,类或方法需要对类型变量加以约束。下面是一个典型的例子。我们要计算数组中的最小元素:
class ArrayAlg{ public static <T> T min(T[] a){ //almost correct if(a==null||a.length==0) return null; T smallest=a[0]; for(int i=1;i<a.length;i++) if(smallest.compareTo(a[i])>0) smallest=a[i]; return smallest; } }
但是,这里有一个问题。请看一下min方法的代码内部。变量smallest类型为T,这以为着它可以是任何一个类的对象。怎么才能确信T所属的类有compareTo方法呢?
解决这个问题的方法是将T限制为实现了Comparable接口的类。可以通过对类型变量T设置限定(bound)实现这一点:
public static <T extends Comparable> T min(T[] a) ...
实际上Comparable接口本身就是一个泛型类型。目前,我们忽略其复杂性以及编译产生的警告。
读者或许会感到奇怪--在此为什么使用关键字extends而不是implements?毕竟,Comparable是一个接口。下面的记法:
<T extends BoundingType>
表示T应该是绑定类型的子类型(subtype)。T和绑定类型可以是类,也可以是接口。选择关键字extends的原因是更接近子类的概念,并且Java的设计者也不打算在语言中再添加一个新的关键字。
一个类型变量或通配符可以有多个限定,例如:
T extends Comparable & Serializable
限定类型用"&"分割,而逗号用来分隔类型变量。
在Java的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。
4.泛型代码和虚拟机
虚拟机没有泛型类型对象--所有对象都属于普通类。
1)类型擦除
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object)。
例如,Pair<T>的原始类型如下所示:
public class Pair{ private Object first; private Object second; public Pair(Object first,Object second){ this.first=first; this.second=second; } public Object getFirst(){return first;} public Object getSecond(){return second;} public void setFirst(Object newValue){first=newValue;} public void setSecond(Object newValue){second=newValue;} }
因为T是一个无限定的变量,所以直接用Object替换。
结果是一个普通的类,就好像泛型引入Java语言之前已经实现的那样。
原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。
例如,假定声明了一个不同的类型:
public class Interval<T extends Comparable & Serializable>implements Serializable{ private T lower; private T upper; ... public Interval(T first,T second){ if(first.compareTo(second)<=0){ lower=first; upper=second; } else{ lower=second; upper=first; } } }
原始类型Interval如下所示:
public class Interval implements Serializable{ private Comparable lower; private Comparable upper; ... public Interval(Comparable first,Comparable second){...} }
注:读者可能想要知道切换限定:class Interval<T extends Serializable &Comparable>会发生什么。如果这样做,原始类型用Serializable替换T,而编译器在必要时要向Comparable插入强制类型转换。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾。
2)翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如,下面这个语句序列
Pair<Employee> buddies=...;
Employee buddy=buddies.getFirst();
擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
a.对原始方法Pair.getFirst的调用。
b.将返回的Object类型强制转换为Employee类型。
当存取一个泛型域时也要插入强制类型转换。假设Pair类的first域和second域都是公有的(也许这不是一种好的编程风格,但在Java中是合法的)。表达式:
Employee buddy=buddies.first;
也会在结果字节码中插入强制类型转换。
3)翻译泛型方法
类型擦除也会出现在泛型方法中。程序员通常认为下述的泛型方法
public static <T extends Comparable> T min(T[] a)
是一个完整的方法族,而擦除类型之后,只剩下一个方法:
public static Comparable min(Comparable[] a)
注意,类型参数T已经被擦除了,只留下了限定类型Comparable。
方法的擦除带来了两个复杂的问题。看一看下面这个示例:
class DateInterval extends Pair<LocalDate>{ public void setSecond(LocalDate second){ if(second.compareTo(getFirst())>=0){ super.setSecond(second); } } ... }
一个日期区间是一对LocalDate对象,并且需要覆盖这个方法来确保第二个值永远不小于第一个值。这个类擦除后变成
class DateInterval extends Pair { //after erasure public void setSecond(LocalDate second){...} ... }
令人感到奇怪的是,存在另一个从Pair继承的setSecond方法,即
public void setSecond(Object second)
这显然是一个不同的方法,因为它有一个不同类型的参数--Object,而不是LocalDate,然而,不应该不一样。考虑下面的语句序列:
DateInterval interval=new DateInterval(...); Pair<LocalDate> pair=interval;//OK--assignment to superclass pair.setSecond(aDate);
这里,希望对setSecond的调用具有多态性,并调用最合适的那个方法。由于pair引用DateInterval对象,所以应该调用DateInterval.setSecond。
问题在于类型擦除与多态发生了冲突。要解决这个问题,就需要编译器在DateInterval类中生成一个桥方法(bridge method):
public void setSecond(Object second){setSecond((Date) second);}
要想了解它的工作过程,请仔细地跟踪下列语句的执行:
pair.setSecond(aDate)
变量pair已经声明为类型Pair<LocalDate>,并且这个类型只有一个简单方法叫setSecond,即setSecond(Object)。虚拟机用pair引用的对象调用这个方法。这个对象是DateInterval类型的,因而将会调用DateInterval.setSecond(Object)方法。这个方法是合成的桥方法。它调用DateInterval.setSecond(Date),这正是我们所期望的操作效果。
桥方法可能会变得十分奇怪。假设DateInterval方法也覆盖了getSecond方法:
class DateInterval extends Pair<LocalDate>{ public LocalDate getSecond(){return (Date) super.getSecond().clone();} ... }
在DateInterval类中,有两个getSecond方法:
LocalDate getSecond()//defined in DateInterval Object getSecond()//overrides the method defined in Pair to call the first method
不能这样编写Java代码(在这里,具有相同参数类型的两个方法是不合法的)。它们都没有参数。但是,在虚拟机中,用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。
注:桥方法不仅用于泛型类型。在一个方法覆盖另一个方法时可以指定一个更严格的返回类型。
总之,需要记住有关Java泛型转换的事实:
a.虚拟机没有泛型,只有普通的类和方法。
b.所有的类型参数都用它们的限定类型替换。
c.桥方法被合成来保持多态。
d.为保持类型安全性,必要时插入强制类型转换。
4)调用遗留代码
设计Java泛型类型时,主要目标是允许泛型代码和遗留代码之间能够互操作。
下面看一个具体的示例。要想设置一个JSlider标签,可以使用方法:
void setLabelTable(Dictionary table)
在这里,Dcitionary是一个原始类型,因为实现JSlider类时Java还不存在泛型,不过,填充字典时,要使用泛型类型。
Dictionary<Integer,Component> labelTable=new Hashtable<>(); labelTable.put(0,new JLabel(new ImageIcon("nine.gif"))); labelTable.put(20,new JLabel(new ImageIcon("ten.gif"))); ...
将Dictionary<Integer,Component>对象传递给setLabelTable时,编译器会发出一个警告。
slider.setLabelTable(labelTable);//Warning
毕竟,编译器无法确定setLabelTable可能会对Dictionary对象做什么操作。这个方法可能会用字符串替换所有的关键字。这就打破了关键字类型为整型(Integer)的承诺,未来的操作有可能会产生强制类型转换的异常。
这个警告对操作不会产生什么影响,最多考虑一下JSlider有可能用Dictionary对象做什么就可以了。在这里十分清楚,JSlider只阅读这个信息,因此可以忽略这个警告。
现在,看一个相反的情形,由一个遗留的类得到一个原始类型的对象。可以将它赋给一个参数化的类型变量,当然,这样做会看到一个警告。例如:
Dictionary<Integer,Component> labelTable=slider.getLabelTable();//Warning
这就行了。再看一看警告,确保标签表已经包含了Interger和Component对象。当然,从来也不会有绝对的承诺。恶意的编码者可会在滑块中设置不同的Dictionary。然而,这种情况并不会比有泛型之前的情况更糟糕。最差的情况就是程序抛出一个异常。
在查看警告之后,可以利用注解(annotation)使之消失。注释必须放在生成这个警告的代码所在的方法之前。
@SuppressWarnings("unchecked") Dictionary<Integer,Components> labelTable=slider.getLabelTable();//No Warning
或者,可以标注整个方法,这个注解会关闭对方法中所有代码的检查。
@SuppressWarnings("unchecked") public void configrure(){...}
5.约束与局限性
在下面几点,将阐述使用Java泛型时需要考虑的一些限制。大多数限制都是由类型擦除引起的。
1)不能用基本类型实例化类型参数。
因此,没有Pair<double>,只有Pair<Double>。当然,其原因是类型擦除。擦除之后,Pair类含有Object类型的域,而Object不能存储double值。
2)运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。例如:
if(a instanceof Pair<String>)//Error
实际上仅仅测试a是否是任意类型的一个Pair。下面的测试同样如此:
if(a instanceof Pair<T>)//Error
或强制类型转换:
Pair<String> p=(Pair<String) a;//Warning--can only test that a is a Pair
为提醒这一风险,试图查询一个对象是否属于某个泛型类型时,倘若使用instanceof会得到一个编译器错误,如果使用强制类型转换会得到一个警告。
同样的道理,getClass方法总是返回原始类型。例如:
Pair<String> stringPair=...; Pair<Employee> employeePair=...; if(stringPair.getClass()==employeePair.getClass())//they are equal
其比较结果是true,这是因为两次调用getClass都将返回Pair.class。
3)不能创建参数化类型的数组
不能实例化参数化类型的数组,例如:
Pair<String>[] table=new Pair<String>[10];//Error
这有什么问题呢?擦除之后,table的类型是Pair[]。可以把它转换为Object[]:
Object[] objarray=table;
数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStoreException异常:
objarray[0]="Hello";//Error--component type is Pair
不过对于泛型类型,擦除会使这种机制无效。以下赋值:
objarray[0]=new Pair<Employee>();
能够通过数组存储检查,不过仍会导致一个类型错误。出于这个原因,不允许创建参数化类型的数组。
需要说明的是,只是不允许创建这些数组,而声明类型为Pair<String>[]的变量仍是合法的。不过不能用new Pair<String>[10]初始化这个变量。
注:1.可以声明通配类型的数组,然后进行类型转换:
Pair<String>[] table=(Pair<String>[]) new Pair<?>[10];
结果将是不安全的。如果在table[0]中存储一个Pair<Employee>,然后对table[0].getFirst()调用一个String方法,会得到一个ClassCastException异常。
2.如果需要收集参数化类型对象,只有一种安全而有效的方法:使用ArrayList:ArrayList<Pair<String>>。
4)Varargs警告
Java不支持泛型类型的数组。这一节我们再来讨论一个相关的问题:向参数可变的方法传递一个泛型类型的实例。
考虑下面这个简单的方法,它的参数个数是可变的:
public static <T> void addAll(Collection<T> coll,T...ts){ for(t:ts) coll.add(t); }
实际上参数ts是一个数组,包含提供的所有实参。
现在考虑以下的调用:
Collection<Pair<String>> table=...; Pair<String> pair1=...; Pair<String> pair2=...; addAll(table,pair1,pair2);
为了调用这个方法,Java虚拟机必须建立一个Pair<String>数组,这就违反了前面的规则。不过,对于这种情况,规则有所放松,你只会得到一个警告,而不是错误。
可以采用两种方法来抑制这个警告。一种方法是为包含addAll调用的方法增加注解@SuppressWarnings("unchecked")。或者在Java SE 7中,还可以@Safe Varargs直接标注addAll方法:
@SafeVarargs public static <T> void addAll(Collection<T> coll,T... ts)
现在就可以提供泛型类型来调用这个方法了。对于只需要读取参数数组元素的所有方法,都可以使用这个注解,这仅限于最常见的用例。
注:可以使用@SafeVarargs标注来消除创建泛型数组的有关限制,方法如下:
@SafeVarargs static <E> E[] array(E... array){return array;}
现在可以调用:
Pair<String>[] table=array(pair1,pair2);
这看起来很方便,不过隐藏着危险。以下代码:
Object[] objarray=table; objarray[0] =new Pair<Employee>();
能顺利运行而不会出现ArrayStoreException异常(因为数组存储只会检查擦除的类型),但在处理table[0]时你会在别处得到一个异常。
5)不能实例化类型变量
不能使用像new T(...),new T[...]或T.class这样的表达式中的类型变量。例如,下面的Pair<T>构造器就是非法的:
public Pair(){ first=new T(); second=new T(); }//Error
类型擦除将T改变成Object,而且,本意肯定不希望调用new Object()。在Java SE 8之后,最好的解决办法是让调用者提供一个构造器表达式。例如:
Pair<String> p=Pair.makePair(String::new);
makePair方法接收一个Supplier<T>,这是一个函数式接口,表示一个无参数而且返回类型为T的函数:
public static <T> Pair<T> makePair(Supplier<T> constr){ return new Pair<>(constr.get(),constr.get()); }
6)不能构造泛型数组
就像不能实例化一个泛型实例一样,也不能实例化数组。不过原因有所不同,毕竟数组会填充null值,构造时看上去是安全的。不过,数组本身也有类型,用来监控存储在虚拟机中的数组。这个类型会被擦除。例如,考虑下面的例子:
public static <T extends Comparable> T[] minmax(T[] a){ T[] mm =new T[2]; ... }//Error
类型擦除会让这个方法永远构造Comparable[2]数组。
如果数组仅仅作为一个类的私有实例域,就可以将这个数组声明为Object[],并且在获取元素时进行类型转换。例如,ArrayList类可以这样实现:
public class ArrayList<E>{ private Object[] elements; ... @SuppressWarnings("unchecked") public E get(int n){ return (E)elements[n];} public void set(int n,E e){ elements[n]=e;}//no cast needed }
实际的实现没有这么清晰:
public class ArrayList<E>{ private E[] elements; ... public ArrayList(){ elements=(E[]) new Object[10];} }
这里,强制类型转换E[]是一个假象,而类型擦除使其无法察觉。
由于minmax方法返回T[]数组,使得这一技术无法施展,如果掩盖这个类型会有运行时错误结果。假设实现代码:
public static <T extends Comparable> T[] minmax(T... a){ Object[] mm=new Object[2]; ... return (T[]) mm;//compiles with warning }
调用
String[] ss=ArrayAlg.minmax("Tom","Dick","Harry");
编译时不会有任何警告。当Object[]引用赋给Comparable[]变量时,将会发生ClassCastException异常。
在这种情况下,最好让用户提供一个数组构造器表达式:
String[] ss=ArrayAlg.minmax(String[]::new,"Tom","Dick","Harry");
构造器表达式String::new指示一个函数,给定所需的长度,会构造一个指定长度的String数组。
minmax方法使用这个参数生成一个有正确类型的数组:
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr,T... a){ T[] mm=constr.apply(2); ... }
比较老式的方法是利用反射,调用Array.newInstance:
public static <T extends Comparable> T[] minmax(T... a){ T[] mm=(T[])Array.newInstance(a.getClass().getComponentType(),2); ... }
ArrayList类的toString方法就没有这么幸运。它需要生成一个T[]数组,但那没有成分类型。因此,有下面两种不同的形式:
Object[] toArray()
T[] toArray(T[] result)
第二个方法接收一个数组参数。如果数组足够大,就使用这个数组。否则,用result的成分类型构造一个足够大的新数组。
7)泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量。例如,下列高招将无法施展:
public class Singleeton<T>{ private static T singleInstance;//Error public static T getSingleInstance(){//Error if(singleInstance==null) construct new instance of T return singleInstance; } }
如果这个程序能够运行,就可以声明一个Singleton<Random>共享随机数生成器,声明一个Singleton<JFileChooser>共享文件选择器对话框。但是,这个程序无法工作。类型擦除之后,只剩下Singleton类,它只包含一个singleInstance域。因此,禁止使用带有类型变量的静态域和方法。
8)不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象。实际上,甚至泛型类扩展了Throwable都是不合法的。例如,以下定义就不能正常编译:
public class Problem<T> extends Exception{/*...*/}//Eroor--can't extend Throwable
catch子句中不能使用类型变量。例如,以下方法将不能编译:
public static <T extends Throwable> void doWork(Class<T> t){ try{ do work } catch(T e){//Error-- can't catch type variable Logger.global.info(...) } }
不过,在异常规范中使用类型变量是允许的。以下方法是合法的:
public static <T extends Throwable> void doWork(T t) throws T{ //OK try{ do work } catch(Throwable realCause){ t.initCause(realCause); throw t; } }
9)可以消除对受查异常的检查
Java异常处理的一个基本原则是,必须为所有受查异常提供一个处理器。不过可以利用泛型消除这个限制。关键在于以下方法:
@SuppressWarnings("unchecked") public static <T extends Throwable> void throwAs(Throwable e) throws T{ throw (T) e; }
假设这个方法包含在类Block中,如果调用
Block.<RuntimeException>throwsAs(t);
编译器就会认为t是一个非受查异常。以下代码会把所有异常都转换为编译器所认为的非受查异常:
try{ do work } catch(Throwable t){ Block.<RuntimeException>throwAs(t); }
下面这个代码包装在一个抽象类中。用户可以覆盖body方法来提供了一个具体的动作。调用toThread时,会得到Thread类的一个对象,它的run方法不会介意受查异常。
public abstract class Block{ public abstract void body() throws Exception; public Thread toThread(){ return new Thread(){ public void run(){ try{ body(); } catch(Throwable t){ Block.<RuntimeException>throwAs(t); } } } } @SuppressWarnings("unchecked") public static <T extends Throwable> void throwAs(Throwable e) throws T{ throw(T) e; } }
例如,如下程序运行了一个线程,它会抛出一个受查异常。
public class Test{ public static void main(String[] args){ new Block(){ public void body() throws Exception{ Scanner in=new Scanner(new File("ququx"),"UTF-8"); while(in.hasNext()) System.out.println(in.next()); } } .toThread().start(); } }
运行这个程序时,会得到一个栈轨迹,其中包含一个FileNotFoundException。
这有什么意义呢?正常情况下,你必须捕获线程run方法中的所有受查异常,把它们“包装”到非抽查异常中,因为run方法声明为不抛出任何受查异常。
不过在这里并没有做这种“包装”。我们只是抛出异常,并“哄骗”编译器,让它认为这不是一个受查异常。
通过使用泛型类,擦除和@SuppressWarnings注解,就能消除Java类型系统的部分基本限制。
10)注意擦除后的冲突
当泛型类型被擦除时,无法创建引发冲突的条件。下面是一个示例。假定像下面这样将equals方法添加到Pair类中:
public class Pair<T>{ public boolean equals(T value){ return first.equals(value) && second.equals(value);} ... }
考虑一个Pair<String>。从概念上讲,它有两个equals方法:
boolean equals(String)//defined in Pair<T> boolean equals(Object)//inherited from Object
但是,直觉把我们引入歧途。方法擦除
boolean equals(T)
就是
boolean equals(Object)
与Object equals方法发生冲突。
当然,补救的办法是重新命名引发错误的方法。
泛型规范说明还提到另外一个原则:“要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。”例如,下述代码是非法的:
class Employee implements Comparable<Employee>{...} class Manager extends Employee implements Comparable<Manager> {...}//Error
Manager会实现Comparable<Employee>和Comparable<Manager>,这是同一接口的不同参数化。
这一限制与类型擦除的关系并不十分明确。毕竟,下列非泛型版本是合法的:
class Employee implements Comparable{...} class Manager extends Employee implements Comparable{...}
其原因非常微妙,有可能与合成的桥方法产生冲突。实现了Comparable<X>的类可以获得一个桥方法:
public int compareTo(Object other){return compareTo((X) other);}
对于不同类型的X不能有两个这样的方法。
5.泛型类型的继承规则
在使用泛型类时,需要了解一些有关继承和子类型的准则。
考虑一个类和一个子类,如Employee和Manager。Pair<Manager>是Pair<Employee>的一个子类吗?答案是“不是”,或许人们会感到奇怪。例如,下面的代码将不能编译成功:
Manager[] topHonchos=...; Pair<Employee> result=ArrayAlg.minmax(topHonchos);//Error
minmax方法返回Pair<Manager>,而不是Pair<Employee>,并且这样的赋值是不合法的。
无论S和T有什么联系,通常,Pair<S>与Pair<T>没有什么联系。
这一限制看起来过于严格,但对于类型安全非常必要。假设允许将Pair<Manager>转换为Pair<Employee>。考虑下面的代码:
Pair<Manager> managerBuddies=new Pair<>(ceo,cfo); Pair<Employee> employeeBuddies=managerBuddies;//illegal,but suppose it wasn't employeeBuddies.setFirst(lowlyEmployee);
显然,最后一句是合法的。但是employeeBuddies和managerBuddies引用了同样的对象。现在将CEO和一个普通员工组成一对,这对于Pair<Manager>来说应该是不可能的。
注:必须注意泛型与Java数组之间的重要区别。可以将一个Manager[]数组赋给一个类型为Employee[]的变量:
Manager[] managerBuddies={ceo,cfo}; Employee[] employeeBuddies=managerBuddies;//OK
然而,数组带有特别的保护。如果试图将一个低级别的雇员存储到employeeBuddies[0],虚拟机将会抛出ArrayStoreException异常。
永远可以将参数化类型转换为一个原始类型。例如,Pair<Employee>是原始类型Pair的一个子类型。在与遗留代码衔接时,这个转换非常必要。
转换成原始类型之后会产生类型错误吗?很遗憾,会!看一看下面这个示例:
Pair<Manager> managerBuddies=new Pair<>(ceo,cfo); Pair rawBuddies=managerBuddies;//OK rawBuddies.setFirst(new File("..."));//only a compile-time warning
听起来有点吓人。但是,请记住现在的状况不会比旧版Java的情况糟糕。虚拟机的安全性还没有到生死攸关的程度。当使用getFirst获得外来对象并赋给Manager对象时,与通常一样,会抛出ClassCastException异常。这里失去的只是泛型程序设计提供的附加安全性。
最后,泛型类可以扩展或实现其他的泛型类。例如,ArrayList<T>实现List<T>接口。这意味着,一个ArrayList<Manager>可以被转换为一个List<Manager>。但是,如前面所见,一个ArrayList<Manager>不是一个ArrayList<Employee>或List<Employee>。
6.通配符类型
1)通配符概念
通配符类型中,允许类型参数化。例如,通配符类型
Pair<? extends Employee>
表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair<Manager>,但不是Pair<String>。
假设要编写一个打印雇员对的方法,像这样:
public static void printBuddies(Pair<Employee> p){ print code }
正如前面讲到的,不能将Pair<Manager>传递给这个方法,这一点很受限制。解决的方法很简单:使用通配符类型:
public static void printBuddies(Pair<? extends Employee> p)
类型Pair<Manager>是Pair<? extends Employee>的子类型。
使用通配符会通过Pair<? extends Employee>的引用破坏Pair<Manager>吗?
Pair<Manager> managerBuddies=new Pair<>(ceo,cfo); Pair<? extends Employee> wildcardBuddies=managerBuddies;//OK wildcardBuddies.setFirst(lowlyEmployee);//compile-time Error
这可能不会引起破坏。对setFirst的调用有一个类型错误。要了解其中的缘由,请仔细看一看类型Pair<? extends Employee>。其方法似乎是这样的:
? extends Employee getFirst() void setFirst(? extends Employee)
这样将不可能调用setFirst方法。编译器只知道需要某个Employee的子类型,但不知道具体是什么类型。它拒绝传递任何特定的类型。毕竟?不能用来匹配。
使用getFirst就不存在这个问题:将getFirst的返回值赋给一个Employee的引用完全合法。
这就是引入有限的通配符的关键之处。现在已经有办法区分安全的访问器方法和不安全的更改器方法了。
2)通配符的超类型限定
通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定(supertype bound),如下所示:
? super Manager
这个通配符限制为Manager的所有超类型。
为什么要这样做呢?带有超类型限定的通配符行为与本节介绍的相反。可以为方法提供参数,但不能使用返回值。例如,Pair<? super Manager>有方法
void setFirst(? super Manager) ? super Manager getFirst()
这不是真正的Java语法,但是可以看出编译器知道什么。编译器无法知道setFirst方法的具体类型,因此调用这个方法时不能接受类型为Employee或Object参数。只能传递Manager类型的对象,或者某个子类型(如Executive)对象。另外,如果调用getFirst,不能保证对象的类型。只能把它赋给一个Object。
下面是一个经典的示例。有一个经理的数组,并且想把奖金最高和最低的经理放在一个Pair对象中。Pair的类型是什么?在这里,Pair<Employee>是合理的,Pair<Object>也是合理的。下面的方法将可以接受任何适当的Pair。
public static void minmaxBonus(Manager[] a,Pair<? super Manager> result){ if(a.length==0) return; Manager min=a[0]; Manager max=a[0]; for(int i=1;i<a.length;i++){ if(min.getBonus()>a[i].getBonus()) min=a[i]; if(max.getBonus()<a[i].getBonus()) max=a[i]; } result.setFirst(min); result.setSecond(max); }
直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
下面是超类型限定的另一种应用。Comparable接口本身就是一个泛型类型。声明如下:
public interface Comparable<T>{ public int compareTo(T other); }
在此,类型变量指示了other参数的类型。例如,String类实现了Comparable<String>,它的compareTo方法被声明为:
public int compareTo(String other)
很好,显式的参数有一个正确的类型。接口是一个泛型接口前,other是一个Object,并且这个方法的实现需要强制类型转换。
由于Comparable是一个泛型类型,也许可以把ArrayAlg类的min方法做的更好一些?可以这样声明:
public static <T extends Comparable<T>> T min(T[] a)
看起来,这样写比只使用T extends Comparable更彻底,并且对许多类来讲,工作得更好。例如,如果计算一个String数组的最小值,T就是String类型的,而String是Comparable<String>的子类型。但是,处理一个LocalDate对象的数组时,会出现一个问题。LocalDate实现了ChronoLocalDate,而ChronoLocalDate扩展了Comparable<ChronoLocalDate>。因此,LocalDate实现的是Comparable<ChronoLocalDate>而不是Comparable<LocalDate>。
在这种情况下,超类型可以用来进行补救:
public static <T extends Comparable<? super T>> T min(T[] a) ...
现在compareTo方法写成
int compareTo(? super T)
有可能被声明为使用类型T的对象,也有可能使用T的超类型(如当T是LocalDate,T的一个子类型)。无论如何,传递一个T类型的对象给compareTo方法都是安全的。
注:子类型限定的另一个常见的用法是作为一个函数式接口的参数类型。例如,Collection接口有一个方法:
default boolean removeIf(Predicate<? super E> filter)
这个方法会删除所有满足给定谓词条件的元素。例如,如果你不喜欢有奇怪散列码的员工,就可以如下将他们删除:
ArrayList<Employee> staff=...; Predicate<Object> oddHashCode=obj->obj.hashCode()%2!=0; staff.removeIf(oddHashCode);
你希望传入一个Predicate<Object>,而不只是Predicate<Employee>。Super通配符可以使你这个愿望成真。
3)无限定通配符
还可以使用无限定的通配符,例如Pair<?>。初看起来,这好像与原始的Pair类型一样。实际上,有很大的不同。类型Pair<?>有以下方法:
? getFirst() void setFirst(?)
getFirst的返回值只能赋给一个Object。setFirst方法不能被调用,甚至不能用Object调用。Pair<?>和Pair本质的不同在于:可以用任意Object对象调用原始Pair类的setObject方法。
注:可以调用setFirst(null)。
为什么要使用这样脆弱的类型?它对于许多简单的操作非常有用。例如,下面这个方法将用来测试一个pair是否包含一个null引用,它不需要实际的类型。
public static boolean hasNulls(Pair<?> p){ return p.getFirst()==null || p.getSecond()==null; }
通过将hasNulls转换成泛型方法,可以避免使用通配符类型:
public static <T> boolean hasNulls(Pair<T> p)
但是,带有通配符的版本可读性更强。
4)通配符捕获
编写一个交换成对元素的方法:
public static void swap(Pair<?> p)
通配符不是类型变量,因此,不能在编写代码中使用"?"作为一种类型。也就是说,下述代码是非法的:
? t=p.getFirst();//Error p.setFirst(p.getSecond()); p.setSecond(t);
这是一个问题,因为在交换的时候必须临时保存第一个元素。幸运的是,这个问题有一个有趣的解决方案。我们可以写一个辅助方法swapHelper,如下所示:
public static <T> void swapHelper(Pair<T> p){ T t=p.getFirst(); p.setFirst(p.getSecond()); p.setSecond(t); }
现在可以由swap调用swapHelper:
public static void swap(Pair<?> p){ swapHelper(p); }
在这种情况下,swapHelper方法的参数T捕获通配符。它不知道是哪种类型的通配符,但是,这是一个明确的类型,并且<T>swapHelper的定义只有在T指出类型时才有明确定义。
当然,在这种情况下不一定要使用通配符。我们已经直接实现了没有通配符的泛型方法<T> void swap(Pair<T> p)。然而,下面看一个通配符类型出现在计算中间的示例:
public static void maxminBonus(Manager[] a,Pair<? super Manager> result){ minmaxBonus(a,result); PairAlg.swap(result);//OK--swapHelper captures wildcard type }
在这里,通配符捕获机制是不可避免的。
通配符捕获只有在有许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个,确定的类型。例如,ArrayList<Pair<T>>的T永远不能捕获ArrayList<Pair<?>>中的通配符。数组列表可以保存两个Pair<?>,分别针对?的不同类型。
7.反射与泛型
1)泛型Class类
Class类是泛型的。例如,String.class实际上是一个Class<String>类的对象(事实上,是唯一的对象)。
类型参数十分有用,这是因为它允许Class<T>方法的返回类型更加有针对性。下面Class<T>中的方法就使用了类型参数:
T newInstance() T cast(Object obj) T[] getEnumConstants() Class<? extends T> getSuperclass() Constructor<T> getConstructor(Class... parameterTypes) Constructor<T> getDeclaredConstructor(Class... parameterTypes)
newInstance方法返回一个实例,这个实例所属的类由默认的构造器获得。它的返回类型目前将声明为T,其类型与Class<T>描述的类相同,这样就免除了类型转换。
如果给定的类型确实是T的一个子类型,cast方法就会返回一个现在声明为类型T的对象,否则,抛出一个返回BadCastException异常。
如果这个类不是enum类或类型T的枚举值的数组,getEnumConstants方法将返回null。
最后,getConstructor与getdeclaredConstructor方法返回一个Constructor<T>对象。Constructor类也已经变成泛型,以便newInstance方法有一个正确的返回类型。
2)使用Class<T>参数进行类型匹配
有时,匹配泛型方法中的Class<T>参数的类型变量很有实用价值。下面是一个标准的示例:
public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException,IllegalAccessException{ return new Pair<>(c.newInstance(),c.newInstance()); }
如果调用
makePair(Employee.class)
Employee.class是类型Class<Employee>的一个对象。makePair方法的类型参数T同Employee匹配,并且编译器可以推断出这个方法将返回一个Pair<Employee>。
3)虚拟机中的泛型类型信息
Java泛型的卓越特性之一是在虚拟机中泛型类型的擦除。令人感到奇怪的是,擦除的类仍然保留一些泛型祖先的微弱记忆。例如,原始的Pair类知道源于泛型类Pair<T>,即使一个Pair类型的对象无法区分是由Pair<String>构造的还是Pair<Employee>构造的。
类似地,看一下方法
public static Comparable min(Comparable[] a)
这是一个泛型方法的擦除
public static <T extends Comparable<? super T>> T min(T[] a)
可以用反射API来确定:
这个泛型方法有一个叫做T的类型参数。
这个类型参数有一个子类型限定,其自身又是一个泛型类型。
这个限定类型有一个通配符参数。
这个通配符参数有一个超类型限定。
这个泛型方法有一个泛型数组参数。
换句话说,需要重新构造实现者声明的泛型类以及方法中的所有内容。但是,不会知道对于特定的对象或方法调用,如何解释类型参数。
为了表达泛型类型声明,使用java.lang.reflect包中提供的接口Type。这个接口包含下列子类型:
Class类,描述具体类型。
TypeVariable接口,描述类型变量(如T extends Comparable<? super T>)。
WildcardType接口,描述通配符(如? super T)。
ParameterizedType接口,描述泛型类或接口类型(如Comparable<? super T>)。
GenericArrayType接口,描述泛型数组(如T[])。
posted on 2018-03-13 22:57 Dingkai_Li 阅读(279) 评论(0) 编辑 收藏 举报