• Java泛型


    本文是《Java核心技术 卷1》中第12章泛型程序设计阅读总结。

    泛型是在Java5中增加的。是Java公布以来的最大变化。

    使用泛型能够避免在代码中杂乱的使用Object然后再强制类型转化,使得代码具有更好的安全性和可读性。

    尤其是在集合类中。比方ArrayList就是一个使用的很广泛的泛型集合类。

    1 什么是泛型

    泛型程序设计(Generic Programming)以为着编写的代码能够被非常多不同类型的对象所重用。比方。我们并不希望为了聚集String和File而分别设计不同的类,我们须要的是有一个能够聚集不论什么类型的集合类,其实,ArrayList就能够聚集不论什么类型的对象,这就是一个泛型的实例。

    在泛型之前。泛型程序设计是用继承实现的。ArrayList类仅仅维护一个Object引用的数组:

    public class ArrayList
    {
        private Object[] elementData;
        ...
        public Object get(int i){...}
        public void add(Object o){...}
    }
    这会引发两个问题。首先,当获取一个值时必须进行强制类型转换,由于我们得到的是一个Object对象:

    ArrayList files=new ArrayList();
    ...
    String finename=(String)fines.get(0);

    此外。因为Object是全部类的始祖,当加入一个元素时没有错误检查,也就是说能够向数组列表中加入不论什么类的对象:

    files.add(new File("..."));
    上面的代码在编译和执行时都不会出错。只是。在代码的其他地方,比方使用get获取元素并强制类型转化为String后会产生一个错误。

    使用泛型能够非常好的解决问题。

    泛型提供的解决方式就是类型參数(type parameters)。比方,ArrayList类有一个类型參数用来指示元素的类型:

    ArrayList<String> files=new ArrayList<String>();

    这使得代码具有更好的可读性,一看就知道这个数组列表中存放的是String对象。

    同一时候,还能够省略后面尖括号里的String:

    ArrayList<String> files=new ArrayList<>();
    类型參数能够给编译器提供了非常好的有效信息。

    当调用get方法时。不须要进行强制类型转换,编译器就知道返回类型是String。而不是Object:

    String filename=files.get(0);
    同一时候。编译器还知道ArrayList<String>中的add方法的參数类型是String。

    这比使用Object安全一些,由于这时编译器就能够进行类型检查了。以避免插入错误类型的对象:

    files.add(new File("..."));//ERROR
    这将不能通过编译。出现编译错误比类在执行时出现类的强制类型转换异常要好得多。

    类型參数使得程序具有更好的可读性和安全性。

    2 定义简单的泛型类

    一个泛型类就是具有一个或多个类型变量的类。这里使用一个简单的Pair类作为样例。以下是Pair的代码:

    package generic;
    
    public class Pair<T> {
    	private T first;
    	private T second;
    	public Pair(){
    		this.first=null;
    		this.second=null;
    	}
    	public Pair(T first,T second){
    		this.first=first;
    		this.second=second;
    	}
    	public T getFirst() {
    		return first;
    	}
    	public void setFirst(T first) {
    		this.first = first;
    	}
    	public T getSecond() {
    		return second;
    	}
    	public void setSecond(T second) {
    		this.second = second;
    	}
    	@Override
    	public String toString() {
    		return "Pair [first=" + first + ", second=" + second + "]";
    	}
    	
    }
    这里引入了一个类型变量T。使用尖括号(<>)括起来,并放在类名之后。泛型类能够有多个类型变量。比方,假设Pair的两个域类型不同,能够这样:

    public class Pair<T,U>{...}
    类定义中的类型变量指定方法的返回类型以及域和局部变量的类型。比方:

    private T first;
    使用泛型类时能够使用详细的类型取代尖括号里的类型变量。比方:

    Pair<String>
    能够将结果想象成带有构造器的普通类:

    Pair<String>()
    Pair<String>(String,String)
    和方法:

    String getFirst()
    String getSecond()
    void setFirst(String)
    void setSecond(String)
    String toString()
    也就是说。泛型类就相当于一个普通的工厂类。

    考虑以下的測试代码:

    public class GenericTest {
    	public static void main(String[] args) {
    		String[] words={"Mary","had","a","little","lamp"};
    		Pair<String> mm=minmax(words);
    		System.out.println(mm);
    	}
    	public static Pair<String> minmax(String[] a){
    		if(a==null||a.length==0)return null;
    		String min=a[0];
    		String max=a[0];
    		for(int i=1;i<a.length;i++)
    		{
    			if(min.compareTo(a[i])>0)min=a[i];
    			if(max.compareTo(a[i])<0)max=a[i];
    		}
    		return new Pair<String>(min,max);
    	}
    }
    执行结果例如以下:

    Pair [first=Mary, second=little]

    3 泛型方法

    上面介绍了怎样定义并使用一个泛型类。

    实际上,还能够定义一个带有类型參数的简单方法:

    public static <T> T getMiddle(T... a)
    {
        return a[a.length/2];
    }
    能够将这种方法定义在普通类中,而不是在泛型类中。

    这是一个泛型方法。由于这里有一个类型參数T,注意这个类型參数的位置:修饰符的后面、返回类型的前面。

    泛型方法能够定义在普通类中。也能够定义在泛型类中。

    当调用一个泛型方法时。在方法名前的尖括号里放入详细的类型:

    String middle=<String>getMiddle("a","b","c");
    事实上,在这样的情况下能够省略<String>类型參数。

    编译器有足够的信息判断所调用的方法。

    大多数情况下。对泛型方法的类型引用没有问题。只是,有时候也会提示错误:

    double middle=getMiddle(3.14,12,0);
    编译器会自己主动将參数打包成一个Double对象和两个Integer对象,然后寻找这些类的公共超类。这时会找到两个公共超类:Number和Comparable接口。

    这就会报错。

    4 类型变量的限定

    有时,类或方法须要对类型变量加以约束。比方,我们要计算数组中的最小元素:

    class ArrayAlg
    {
        public static <T> T min(T[] a)
        {
            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代码内部使用了T作为smallest的类型,这意味着它能够是不论什么一个类的对象。怎么才干确定T所属的类实现了compareTo方法呢?

    解决办法是将T限制为实现了Comparable接口的类。像这样:

    public static <T extends Comparable> T min(T[] a)...
    如今,泛型的min方法仅仅能被实现了Comparable接口的类的数组调用。假设一个类没有实现Comparable接口而调用这种方法,就会产生一个编译错误。

    一个类型变量或通配符(后面会介绍到)能够有多个限定:

    T extends Comparable & Serializable
    限定类型使用“&”分隔。用逗号来分隔类型变量。

    在Java的继承中。能够依据须要拥有多个接口超类型,但限定中至多有一个类。

    假设用一个类作为限定,它必须是限定列表中的第一个。

    5 泛型代码与虚拟机

    虚拟机没有泛型类型对象。也就是说全部的类都是普通类。那么java是怎样处理泛型的呢?

    5.1 类型擦除

    不管何时定义了一个泛型类型,都自己主动提供了一个对应的原始类型(raw type)。原始类型的名字就是删去类型參数的泛型类型名。擦除(erased)类型变量,并替换为限定类型(没有限定类型就用Object)。

    比方前面的Pair<T>的原始类型例如以下:

    public class Pair {
    	private Object first;
    	private Object second;
    	public Pair(){
    		this.first=null;
    		this.second=null;
    	}
    	public Pair(Object first,Object second){
    		this.first=first;
    		this.second=second;
    	}
    	public Object getFirst() {
    		return first;
    	}
    	public void setFirst(Object first) {
    		this.first = first;
    	}
    	public Object getSecond() {
    		return second;
    	}
    	public void setSecond(Object second) {
    		this.second = second;
    	}
    	@Override
    	public String toString() {
    		return "Pair [first=" + first + ", second=" + second + "]";
    	}
    	
    }
    由于T没有限定类型,所以替换成Object。

    在程序中可使用不同类型的Pair,比方Pair<String>、Pair<Date>,但擦除类型后就变为原始的Pair类型了。

    原始类型用第一个限定的类型变量来替换,假设没有给定限定就用Object替换。

    假如有以下的泛型:

    public class Interval<T extends Comparable & Serializable> implements Seriallizable
    {
    	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){...}
    }
    注意:为了提高效率。应该将标签接口(没有方法的接口)放在边界列表的末尾。

    5.2 翻译泛型表达式

    当程序调用泛型方法时,假设擦除返回类型。编译器将插入强制类型转换。比方:

    Pair<Student> buddies=...;
    Student buddy=buddies.getFirst();
    擦除getFirst的返回类型将返回Object类型。

    编译器自己主动插入Student的强制类型转换。

    也就是说,编译器把这种方法调用翻译为两条虚拟机指令:

    (1)对原始方法Pair.getFirst的调用。

    (2)将返回的Object类型强制转换为Student类型。

    当存取一个泛型域时也要插入强制类型转换。如果Pair的first和second域都是公有的。以下的表达式:

    Student buddy=buddies.first;
    也会在结果字节码中插入强制类型转换。

    5.3 翻译泛型方法

    对泛型方法也会擦除。比方以下的泛型方法:

    public static <T extends Comparable> T min(T[] a)
    擦除类型后。变成:

    public static Comparable min(Comparable[] a)
    使用限定类型Comparable替换了类型參数T。

    方法的擦除带来了来两个问题。

    看看以下这个演示样例:

    import java.util.*;
    public class DateInterval extends Pair<Date>
    {
    	public void setSecond(Date second){
    		if(second.compareTo(getFirst())>0)
    			super.setSecond(second);
    	}
    }
    一个日期区间是一对Date对象。而且要覆盖setSecond方法来保证第二个值永远不小于第一个值。

    来看看擦除后的结果,使用javap -private DateInterval命令:


    能够看到,里面有两个setSecond方法。一个參数类型是Object,还有一个是Date。

    參数类型是Date的好理解。是DateInterval擦除后的方法。那么类型是Object的呢?

    事实上这种方法是从Pair继承来的。

    这显然是一个不同的方法。只是,这不应该不一样。比方:

    DateInterval interval=new Dateinterval(...);
    Pair<Date> pair=interval;//OK
    pair.setSecond(aDate);
    这里,希望对setSecond的调用具有多态性,并调用合适的方法。

    因为pair引用DateInterval对象,所以应该调用DateInterval.setSecond方法。

    问题在于类型擦除与多态发生了冲突。

    为了解决问题。编译器在DateInterval类中生成了一个桥方法(bridge method):

    public void setSecond(Object second){
        setSecond((Date)second);
    }
    变量pair已经声明为类型Pair<Date>,而且这个类型仅仅有一个简单的方法叫setSecond,即setSecond(Object)。虚拟机用pair引用的对象调用这种方法。这个对象是DateInterval类型的,所以会调用DateInterval.setSecond(Object)方法。

    这种方法是合成的桥方法,它调用DateInterval.setSecond(Date),这正是我们所期望的操作效果。

    第二个问题是。有时这个生成的桥方法可能会非常奇怪,超出我们对一般方法的认识。如果DateInterval也覆盖了getSetSecond方法:

    import java.util.*;
    public class DateInterval extends Pair<Date>
    {
    	public void setSecond(Date second){
    		if(second.compareTo(getFirst())>0)
    			super.setSecond(second);
    	}
    	public Date getSecond(){
    		return super.getSecond();
    	}
    }

    使用javap查看擦除后的结果:


    结果显示有以下这两个方法:

    Date getSecond()
    Object getSecond()
    当中第一个返回类型是Date的方法是DateInterval中定义的,第二个返回类型是Object的方法是合成的桥方法。

    奇怪的是,这两个方法仅仅有返回类型不同,而在Java中。这样编写代码时不合法的。只是,在虚拟机中。用參数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机可以正确处理这一情况。

    以下是关于泛型转换的事实:

    • 虚拟机中没有泛型,仅仅有普通的类和方法;
    • 全部的类型參数都用它们的限定类型替换;
    • 桥方法被合成用来保持多态;
    • 为保持类型安全性,必要时插入强制类型转换;

    6 约束与局限

    使用Java泛型时可能会遇到一些局限与限制,大多数限制都是由类型擦除引起的。

    6.1 不能用基本类型实例化类型參数

    不能使用类型參数取代基本类型。Java有8个基本类型。各自是:byte、short、int、long、float、double、char和boolean。因此,没有Pair<double>。仅仅有Pair<Double>。

    原因就是类型擦除。由于擦除后,Pair类含有Object类型的域,而Object不能存储double的值。

    不能使用基本类型。可是能够使用它们的包装器类型,比方Integer、Boolean等。

    6.2 执行时类型查询仅仅适用于原始类型

    虚拟机中的对象总有一个特定的非泛型类型。

    因此,全部的类型查询仅仅产生原始类型。比方:

    if(a instanceof Pair<String>)//ERROR

    实际上只測试a是否是随意类型的一个Pair。以下的測试也是如此:

    if(a instanceof Pair<T>)//ERROR
    或强制类型转换:

    Pair<String> p=(Pair<String>)a;//WARNING
    即不管何时使用instanceof或涉及泛型类型的强制类型转换表达式都会看到一个编译器警告。

    相同。getClass方法总是返回一个原始类型:

    Pair<String> stringPair=...;
    Pair<Student> studentPair=...;
    if(stringPair.getClass()==studentPair.getClass())//true
    上面的比較结果将是true。由于两次调用getClass都会返回Pair.class。

    6.3 不能创建參数化类型的数组

    不能实例化參数化类型的数组,比如:

    Pair<String>[] table=new Pair<String>[10];//ERROR
    这有什么问题?类型擦除后。table的类型是Pair[],能够把它转换为Object[]:

    Object[] objarray=table;
    数组会记住元素类型,假设试图存储其他类型的元素,就会抛出一个ArrayStoreException异常:

    objarray[0]="Hello";
    这会出错,由于元素的类型是Pair。Hello是一个String。

    只是对于泛型类型,擦除会使这样的机制失效:

    objarray[0]=new Pair<Student>();
    这将通过数组的存储检查,由于Pair<Student>擦除后的类型就是Pair。尽管这里没有出错,可是在后序的操作中就会抛出一个类型错误。所以,不同意创建參数化类型的数组。

    须要说明的是。仅仅是不同意创建这些数组,而声明类型为Pair<String>[]的变量仍是合法的。只是不能使用new Pair<String>[10]初始化这个变量。

    6.4 Varags警告

    在上一节中已经知道。Java不支持泛型类型的数组。在这里我们讨论一个相关的问题:向參数可变的方法传递一个泛型类型的实例。

    以下的方法是将一个将ts中的元素加入到一个Collection集合中:

    public static <T> void addAll(Collection<T> coll,T...ts){
    	for(T t:ts){
    		coll.add(t);
    	}
    }
    对于可变參数ts。实际上就是一个数组。数组中的元素就是提供的全部參数。

    看看以下的调用:

    Collection<Pair<String>> table=new ArrayList<Pair<String>>();
    Pair<String> one=new Pair<String>("Hello","World");
    Pair<String> two=new Pair<String>("Hello","Java");
    addAll(table,one,two);
    System.out.println(table);
    向addAll方法传递的可变參数的类型是Pair<String>,可是可变參数不是一个数组么。这样不就是一个泛型类型的数组么?Java不是不支持泛型类型的数组么?

    为了调用这种方法,Java虚拟机必须建立一个Pair<String>数组。只是。这时仅仅会有一个警告。而不是错误:


    能够採用两种方法来抑制这个警告。一种方法是为包括addAll调用的方法添加标注@SuppressWarning("unchecked")。

    或者在Java SE 7中,还能够使用@SafeVaragrs直接标注addAll方法:

    @SafeVargars
    public static <T> void addAll(Collection<T> coll,T...ts){...}
    这就能够提供泛型类型来调用这种方法了。对于仅仅需读取參数数组元素的全部方法。都能够使用这个标记。这仅局限于最常见的用例。

    上述调用的结果例如以下:

    [Pair [first=Hello, second=World], Pair [first=Hello, second=Java]]

    6.5 不能实例化类型变量

    不能使用像new T(...)。new T[...]或T.class这种表达式中的类型变量。比方。以下的Pair<T>构造器就是非法的:

    public Pair(){first=new T();second=new T();}//ERROR
    类型擦除会将T改变成Object。可是本意肯定不希望调用new Object()。可是能够通过反射调用Class.newInstance()方法来构造泛型对象。

    因为不能使用T.class,所以例如以下的代码是非法的:

    first=T.class.newInstance();//ERROR
    能够使用以下的方法:

    public static <T> Pair<T> makePair(Class<T> cl)
    {
        try{
            return new Pair<>(cl.newInstance(),cl.newInstance());
        }catch(Exception e){
            return null;
        }
    }
    能够这样调用这种方法:

    Pair<String> pair=Pair.makePair(String.class);
    事实上,Class本身也是个泛型。

    比方,String.class就是一个Class<String>的实例。

    因此,makePair可以判断出pair的类型。

    不能构造一个泛型数组:

    public static <T extends Comparable> T[] minmax(T[] a)
    {
        T[] mm=new T[2];//ERROR
        ...
    }
    类型擦除会使这种方法构造一个Objec[2]数组。

    假设数组只作为一个类的私有实例域,就能够将这个数组声明为Object[],并在获取元素时进行类型转换。

    事实上,ArrayList类就是这么实现的:

    Object[] elementData;
    elementData用来存储列表中的元素,声明类型为Objec[]数组。以下是ArrayList类中获取元素的代码:

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }
    
    /**
     * Returns the element at the specified position in this list.
     *
     * @param  index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }
    获取元素的时候会进行类型转换。

    假如minmax方法返回一个长度为2的数组,而不是一个Pair。可能这样实现:

    public static <T extends Comparable> T[] minmax(T... a)
    {
    	Object[] mm=new Object[2];
    	T min=a[0];
    	T max=a[0];
    	for(int i=1;i<a.length;i++)
    	{
    		if(min.compareTo(a[i])>0)min=a[i];
    		if(max.compareTo(a[i])<0)max=a[i];
    	}
    	mm[0]=min;
    	mm[1]=max;
    	return (T[])mm;
    }
    当调用:

    String[] ss=minmax("Tom","Dick","Harry");

    编译时不会有不论什么警告。可是当Object引用赋给String[]变量时,将会发生ClassCastException异常。

    结果例如以下:


    这时,能够利用反射。调用Array.newInstance:

    T[] mm=(T[])Array.newInstance(a.getClass().getComponentType(), 2);
    结果例如以下:

    Dick
    Tom

    ArrayList中有两个toArray方法。第一种返回一个Object[]数组:

    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }
    另外一种接收一个參数,假设数组足够大,就使用这个数组;否则用參数的成分类型构造一个足够大的数组:

    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }
    当中构造新数组时使用了Arrays的copyOf方法,第三个參数就是元素的类型。也就是说,和上面minmax的实现类似。

    6.6 泛型类的静态上下文中类型变量无效

    不能在静态域或方法中引用类型变量。比方,以下的代码将无效:

    public class Singleton<T>
    {
        private static T singleInstance;//ERROR
    
        publiv static T getSingleInstance()//ERROR
        {
            if(singleInstance==null) construct new instance of T
            return singleInstance;
        }
    }

    假设这个程序可以执行的话,就行声明一个Singleton<Random>共享随机数生成器,声明一个Singleton<JFileChooser>共享文件选择器对话框。

    遗憾的是,这个程序无法工作。擦除类型后。仅仅剩下Singleton类,它仅仅包括一个singleInstance域。

    因此。禁止使用带有类型变量的静态域和方法。

    6.7 注意擦除后的冲突

    当泛型类型被擦除时,可能会导致名字冲突。比方将以下的equals方法加入到Pair<T>中:

    public boolean equals(T value) {
    	return first.equals(value) && second.equals(value);
    }
    考虑一个Pair<String>,它应该有两个equals方法:

    boolean equals(String)
    boolean equals(Object)
    第一个方法是Pair<T>中定义的,第二个方法是继承自Object的。

    只是,类型擦除后,第一个equals方法将变为:

    boolean equals(Object)
    这就与第二个继承自Object的方法产生了冲突。例如以下所看到的:


    解决的办法就是又一次命名引发冲突的方法。

    对于擦除后引起的名字冲突,另一种可能,即擦除后生成的桥方法之间产生冲突。

    这是有可能的,比方:

    class Calendar implements Comparable<Calendar>{...}
    class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar>{...}//ERROR
    当中,GregorianCalendar会实现Comparable<Calendar>和Comparable<GregorianCalendar>,这是同一个接口的不同參数化。

    实现了Comparable<X>的类能够获得一个例如以下的桥方法:

    public intcompareTo(Object other){
        return compareTo((X)other);
    }
    对于不同类型的X不能有两个这个方案。

    因此,泛型规范还提到还有一个原则:

    要想支持擦除的转换,就须要强行限制一个类或类型变量不能同一时候成为两个接口类型的子类。而这两个接口是同一个接口的不同參数化。

    比方上面的GregorianCalendar。是Comparable<Calendar>和Comparable<GregorianCalendar>的子类,这两个接口是同一个接口的不同參数化。因此出错。

    7 泛型类型的继承规则

    在使用泛型时,还须要了解一些有关继承和子类型的准则。考虑一个类和一个子类,比方Employee和Manager,Manager继承自Employee,那么Pair<Manager>是Pair<Employee>的一个子类么?不是。

    以下的代码将不能编译成功:

    Manager[] top=...;
    Pair<Employee> result=ArrayAlg.minmax(top);//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;
    employeeBuddies.setFirst(lowlyEmployee);
    显然最后一句是合法的,可是employeeBuddies和managerBuddies引用了同一个对象,也就是将一个低级员工和cfo组成一对了,这对于Pair<Manager>来说是不可能的。

    这显示了泛型和数组之间的重要差别。能够将一个Manager[]数组赋给一个类型为Employee[]的变量:

    Manager[] managerBuddies={ceo,cfo};
    Employee[] employeeBuddies=managerBuddies;
    和泛型不同的是,数组带有特别的保护。

    假设试图将一个低级员工放到employeeBuddies数组中,虚拟机将会抛出ArrayStoreException异常。

    永远能够将參数化类型转换为一个原始类型。比方Pair<Employee>是原始类型Pair的一个子类型。

    转换成原始类型之后也可能会出错:

    Pair<Manager> managerBuddies=new Pair<>(ceo,cfo);
    Pair rawBuddies=managerBuddies;//OK
    rawBuddies.setFirst(new File("..."));//only a cimpile-time warning
    最后,泛型类能够扩展或实现其它的泛型类,这与普通类没什么差别。

    比如ArrayList<T>类实现了List<T>接口。这意味着,一个ArrayList<Manager>能够被转换为ArrayList<Employee>或List<Employee>。可是,一个ArrayList<Manager>不是一个ArrayList<Employee>或List<Employee>。

    例如以下图:


    8 通配符类型

    8.1 通配符

    泛型中使用?表示通配符类型。比方:

    Pair<?

    extends Employee>

    表示不论什么泛型Pair类型,它的类型參数是Employee的子类,如Pair<Manager>,但不是Pair<String>。

    如果要编写一个打印雇员对的方法:

    public static void printBuddies(Pair<Employee> p){
        Employee first=p.getFirst();
        Employee second=p.getSecond();
        System.out.println(first.getName()+" and "+second.getName()+" are buddies.");
    }
    前面已经说过,这里不能将Pair<Manager>传递给这种方法。解决的办法就是使用通配符类型:

    public static void printBuddies(Pair<? extends Employee> p)
    类型Pair<Manager>是Pair<? extends Employee>的子类型:


    如今考虑以下的代码:

    Pair<Manager> managerBuddies=new Pair<>(ceo,cfo);
    Pair<? extends Employee> wildcardBuddies=managerBuddies;//OK
    wildcardBuddies.setFirst(lowlyEmployee);//compile-time error
    最后一句出现错误,保证了Pair<?

    extends Employee>不会破坏Pair<Manager>。

    看一下Pair<? extends Employee>。里面似乎有这两个方法:

    ? extends Employee getFirst()
    void setFirst(? extends Employee)
    这样将不能调用setFirst方法。编译器仅仅知道须要某个Employee的子类型。但不知道详细是什么类型,它将拒绝传递不论什么特定的类型,毕竟?不能用来匹配。

    使用getFirst方法就没有问题了,将getFirst方法的返回值赋给一个Employee的引用全然合法。

    8.2 通配符的超类型限定

    通配符限定于类型变量限定非常像,可是。另一个附加的能力。即能够指定一个超类型(supertype bound),例如以下:

    ? super Manager
    这个通配符限制为Manager的全部超类,这里是Employee和Object。

    这个行为与上一个通配符限定正好相反。能够为方法提供參数。但不能使用返回值。比方,Pair<? super Manager>有例如以下的方法:

    void setFirst(? super Manager)
    ?

    super Manager getFirst()

    编译器不知道setFirst方法的确切类型,可是能够用随意Manager对象(或子类型)调用它,而不能用Employee对象调用。然而,假设调用getFirst。返回的对象类型就不能得到保证,仅仅能把它赋给一个Object。

    以下是一个简单的样例,有一个Manager数组。把工资最高和最低的放在一个Pair中,Pair的类型是什么呢?在这里。Pair<Employee>是合理的。Pair<Object>也是合理的,它们的关系例如以下:


    这种方法例如以下:

    public static void minmaxBonus(Manager[] a,Pair<? super Manager> result){
           if(a==null || a.length==0)return;
           Manager min=a[0];
           Manager max=a[0];
           for(int i=1;i<a.length;i++){
               if(min.getSalary()>a[i].getSalary())min=a[i];
               if(max.getSalary()<a[i].getSalary())max=a[i];
           }
           result.setFirst(min);
           result.setSecond(max);
       }
    直观的讲,带有超类型限定的通配符能够向泛型对象写入,带有子类型限定的通配符能够从泛型对象读取。

    拿一个不恰当的比喻。Pair<? extends Employee>限定的是<=Employee,即Employee及子类;而Pair<? super Manager>限定的是>=Manager。即Manager及超类。

    8.3 无限定通配符

    还能够使用无限定通配符,比方Pair<?>。

    看起来这和原始的Pair一样,实际上有非常大的不同。

    类型Pair<?>有例如以下的方法:

    ?

    getFirst() void setFirst(?

    )

    getFirst的返回值仅仅能赋给一个Object,而setFirst方法不能调用,甚至不能用Object调用。Pair<?>与Pair本质的不同在于:能够使用随意Object对象调用原始Pair类的setFirst方法。

    只是,对Pair<?>类的setFirst。能够用null调用。

    Pair<?>有什么用?它对于很多简单的操作非常实用。比方,以下的方法用来測试一个pair是否包括一个null引用,它不须要实际的类型:

    public static boolean hasNulls(Pair<?> p)
    {
        return p.getFirst()==null || p.getSecond()==null;
    }

    9 泛型和反射

    9.1 使用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>。

    9.2 虚拟机中的泛型类型信息

    Java泛型在虚拟机中会擦除泛型类型,可是。擦除的类仍保留一些泛型祖先的记忆。比如,原始的Pair类知道源于泛型类Pair<T>。

    泛型方法:

    public static <T extends Comparable<? super T>> T min(T[] a)
    擦除后变为:

    public static Comparable min(Comparable[] a)
    Java的反射API提供了非常多关于泛型的信息:

    • 这个泛型方法有一个叫做T的类型參数。
    • 这个类型參数有一个子类型限定,其本身又是一个泛型类型。
    • 这个限定类型有一个通配符參数。
    • 这个通配符參数有一个超类型限定;
    • 这个泛型方法有一个泛型数组參数;

    对于虚拟机来说,须要又一次构造实现者声明的泛型类型以及方法中的全部内容。

    可是,不会知道对于特定的对象或方法调用。假设解释类型參数。

    为了表达泛型类型声明,Java SE 5在java.lang.reflect包中提供了一个新的的接口Type。这个接口有下列子类型:

    • Class类,描写叙述详细类型。
    • TypeVariable接口,描写叙述类型变量(比方T extends Comparable<? super T>);
    • WildcardType接口,描写叙述通配符(比方? super T)。
    • ParameterizedType接口。描写叙述泛型类或接口类型(比方Comparable<? extends T>)。
    • GenericArrayType接口,描写叙述泛型数组(如T[]);

    下图是继承层次:


    以下的代码能够打印一个泛型类的基本信息:

    import java.lang.reflect.*;
    import java.util.*;
    public class GenericReflection {
        public static void main(String[] args)
        {
            // read class name from command line args or user input
            String name;
            if (args.length > 0) name = args[0];
            else
            {
                Scanner in = new Scanner(System.in);
                System.out.println("Enter class name (e.g. java.util.Collections): ");
                name = in.next();
            }
    
            try
            {
                // print generic info for class and public methods
                Class<?

    > cl = Class.forName(name); printClass(cl); for (Method m : cl.getDeclaredMethods()) printMethod(m); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static void printClass(Class<?> cl) { System.out.print(cl); printTypes(cl.getTypeParameters(), "<", ", ", ">", true); Type sc = cl.getGenericSuperclass(); if (sc != null) { System.out.print(" extends "); printType(sc, false); } printTypes(cl.getGenericInterfaces(), " implements ", ", ", "", false); System.out.println(); } public static void printMethod(Method m) { String name = m.getName(); System.out.print(Modifier.toString(m.getModifiers())); System.out.print(" "); printTypes(m.getTypeParameters(), "<", ", ", "> ", true); printType(m.getGenericReturnType(), false); System.out.print(" "); System.out.print(name); System.out.print("("); printTypes(m.getGenericParameterTypes(), "", ", ", "", false); System.out.println(")"); } public static void printTypes(Type[] types, String pre, String sep, String suf, boolean isDefinition) { if (pre.equals(" extends ") && Arrays.equals(types, new Type[] { Object.class })) return; if (types.length > 0) System.out.print(pre); for (int i = 0; i < types.length; i++) { if (i > 0) System.out.print(sep); printType(types[i], isDefinition); } if (types.length > 0) System.out.print(suf); } public static void printType(Type type, boolean isDefinition) { if (type instanceof Class) { Class<?> t = (Class<?>) type; System.out.print(t.getName()); } else if (type instanceof TypeVariable) { TypeVariable<?> t = (TypeVariable<?>) type; System.out.print(t.getName()); if (isDefinition) printTypes(t.getBounds(), " extends ", " & ", "", false); } else if (type instanceof WildcardType) { WildcardType t = (WildcardType) type; System.out.print("?"); printTypes(t.getUpperBounds(), " extends ", " & ", "", false); printTypes(t.getLowerBounds(), " super ", " & ", "", false); } else if (type instanceof ParameterizedType) { ParameterizedType t = (ParameterizedType) type; Type owner = t.getOwnerType(); if (owner != null) { printType(owner, false); System.out.print("."); } printType(t.getRawType(), false); printTypes(t.getActualTypeArguments(), "<", ", ", ">", false); } else if (type instanceof GenericArrayType) { GenericArrayType t = (GenericArrayType) type; System.out.print(""); printType(t.getGenericComponentType(), isDefinition); System.out.print("[]"); } } }

    输入Pair,执行结果例如以下:

    class Pair<T> extends java.lang.Object
    public java.lang.String toString()
    public T getFirst()
    public T getSecond()
    public void setSecond(T)
    public void setFirst(T)


  • 相关阅读:
    [GL]行星运行1
    一个图的带权邻接表存储结构的应用
    [GDAL]3.影像金字塔构建
    [GDAL]1.GDAL1.8.1编译与第一个程序
    [GDAL]2.读取栅格和矢量数据
    C#迭代器
    GoogleEarth缓存机制探索
    AE开发三维的不足!
    [GDAL]4.影像的读取和显示
    [STL学习]1.概述
  • 原文地址:https://www.cnblogs.com/cynchanpin/p/7253977.html
Copyright © 2020-2023  润新知