码迷,mamicode.com
首页 > 编程语言 > 详细

Java泛型

时间:2016-04-11 12:25:39      阅读:202      评论:0      收藏:0      [点我收藏+]

标签:

本文是《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)


Java泛型

标签:

原文地址:http://blog.csdn.net/u012877472/article/details/51106834

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!