泛型程序设计
为什么要使用泛型程序设计
泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用。一个实例是,ArrayList可以聚集任何类型的对象。
12.2定义简单的泛型类
一个泛型类就是具有一个或多个类型变量的类。下面我们用一个简单的例子来关注泛型类如何定义
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;
}
}
12.3 泛型方法
介绍了如何定义一个简单的泛型类,接下来介绍一下如何定义一个带有类型参数的简单方法。
class ArrayAlg
{
public static <T> T getMiddle(T... a)
{
return a[a.length / 2];
}
}
上述所示是在一个普通类中定义的泛型方法。
当调用一个泛型方法时,在方法名前的尖括号中放入具体类型:
String middle = ArrayAlg.<String>getMiddle("John","Q.","Public");
但是在这种情况下,方法调用中可以省略类型参数。编译器有足够的信息能够推断出所调用的方法。
String middle = ArrayAlg.getMiddle("John","Q.","Public");
有时候省略类型参数会行不通,如下:
double middle = ArrayAlg.getMiddle(3.14,1729,0);
解释这句代码有两种方法,并且两种方法都是正确的。简单来说,编译器会自动打包参数为1个Double和2个Integer对象,然后寻找这些类的共同超类。事实上,能够找到2个这样的超类:Number 和 Comparable接口,其本身也是一个泛型类型。补救措施是将所有的参数写为double值。
12.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;
}
}
在上面的代码中,smallest的类型为T,但是她却调用了compareTo方法,如何保证T所属的类一定有compareTo方法呢?需要将T限制为实现了Comparable接口。
public static <T extends Comparable> T min(T[] a) ...
然而实际上Comparable接口也是泛型类型,在之后的小节中会继续讨论这个问题。有了这个限定之后,泛型的min方法只能被实现了Comparable借口就的类(String,Date等)的数组调用。
那上面为什么不用implement是而是用extends呢?毕竟Comparable是一个接口。下面的符号
<T extends BoundingType>
表示T 应该是绑定类型的子类型。T和绑定类型可以是类,也可以是接口。
一个类型变量或者通配符可以有多个限定,例如:
T extends Comparable & Serializable
限定类型用 “&”分隔,用逗号来分隔类型变量。在Java的继承中,可以根据需要拥有多个接口超类型,但限定中之多有一个类。如果用一个类作为限定,它必须是限定列表的第一个,,以下是我的理解。
T extends ClassName & Comparable & Serializable & ...(more interfaces)
12.5 泛型代码与虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类。无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定的变量用Object)。
例如,Pair的原始类型如下所示
pubilc class Pair
{
private Ojbect 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 new Value) {first = newValue;}
public void setSecond(Object newValue) {second = newValue;}
}
T 没有被限定,所以用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) {...}
}
12.5.1 翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器将插入强制类型转换。
Paire<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
对上述语句序列,擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。上面这个方法调用被翻译为两条虚拟机指令
对原始方法Pair.getFirst的调用
将返回的Object类型强制转换为Employee类型。
当存取一个泛型域时也要插入强制类型转换。
12.5.2 翻译泛型方法
类型擦除也会出现在泛型方法中。
public static <T extends Comparable> T min(T[] a)
上述方法通常被认为是一个完整的方法族。擦除类型后,只剩下一个方法
public static Comparable min(Comparable[] a)
方法的擦除带来了两个复杂问题。看下面的示例
class DateInterval extends Pair<Date>
{
public void setSecond(Date second)
{
if(second.compareTo(getFirst()) >= 0)
super.setSecond(second);
}
}
这个类擦除后变成
class DateInterval extends Pair
{
pubilc void setSecond(Date second){...}
}
但是存在另一个从Pair继承的setSecond方法,即
public void setSecond(Object second)
这显然是一个不同的方法,因为类型参数不同(是Object而不是Date)。子类中的方法本意是想覆盖父类中的方法的,但就目前看来,由于类型擦除的缘故,没有覆盖起来。也就是说现在DateInterval类中有两个方法。考虑下面的语句序列
DateInterval interval = new DateInterval(...);
Pair<Date> pair = interval; //OK
pair.setSecond(aDate);
上述语句序列中,希望对setSecond的调用具有多态性,并调用最合适的那个方法。这里希望调用的应该是DateInterval.setSecond(Date)方法。问题在于类型擦除与多态发生了冲突。要解决这个问题,就需要在编译器DateIterval类中生成一个桥方法:
public void setSecond(Object second) {setSecond(Date) second;}
这个方法里面调用的就是正确的子类的方法。实际调用的时候,由于实际对象时DateInterval,所以,会调用DateInterval.setSecond(Object)方法。(这里最好再复习一下第5章,5.1所述的方法表),所以最后调用正确。
桥方法有时候可能会让程序员感到疑惑。假设DateInterval方法中也覆盖了getSecond方法:
class DateInterval extends Pair<Date>
{
public Date getSecond() {return (Date) super.getSecond().clone();}
}
...
类型擦除后,会有两个方法
public Date getSecond() {...}
public Object getSecond() {...}
从程序员的角度来说,这两个方法的方法签名是一样的,只有返回类型不同而已。但是在虚拟机中,用参数类型和返回类型确定一个方法,因此,虚拟机能正确地处理这一情况。
12.5.3 调用遗留代码
略
12.6 约束与局限性
12.6.1 不能用基本类型实例化类型参数
Pair是错误的,只有Pair。主要是类型擦除后,Pair类含有Object类型的域,而Object不能存储double值。
12.6.2 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询值产生原始类型。
if (a insrtanceof Pair<String>) //Error
仅能测试a是否是任意类型的一个Pair。强制类型转换也是如此
Pair<String> p = (Pair<String>) a; //WARNING--can only test that is a Pair
getClass方法总是返回原始类型:
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if (stringPair.getClass() == employeePair.getClass()) //they are equal
都将返回Pair.class
12.6.3 不能创建参数化类型的数组
没有泛型数组
Pari<String>[] table = new Pair<String>[10]; //Error
经过类型擦除之后,table的类型是Pair[].可以把它转换为Object[];
Object[] objarray = table;
数组会记住它的元素类型,存储其他类型的值,会抛出异常
objarray[0] = "Hello"' //Error--component type is Pair
但是以下赋值
objarray[0] = new Pair<Employee>(); //这就会导致ClassCastException
能够通过数组存储检查,不过仍会导致一个类型错误。出于这个原因,不允许创建参数化类型的数组。
12.6.4 Varargs警告
考虑下面这个方法
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);
为了调用这个方法,虚拟机必须创建一个Pair数组,这就违反了前面提到的规则。在Java SE 7中,可以用@SafeVarargs直接标注addAll方法。
12.6.5 不能实例化类型变量
语句:
public Pair() {first = new T(); second = new T();}
是非法的。不能使用像new T(…), new T[…]或T.class这样的表达式中的类型变量。但是,可以通过反射调用Class.newInstance方法来构造泛型对象。但是必须像下面这样调用
public static <T> Pair<T> makePair(Class<T> cl)
{
try {return new Pair<>(cl.newInstance(),cl.newInstance());}
catch(Exception ex) {return null;}
}
接下来,可以这样调用
Pair<String> p = Pair.makePair(String.class);
不能用上面的方法构造泛型数组:
pbulic static <T extends Comparable> T[] minmax(T[] a) {T[] mm = new T[2]; ...}
上面的语句是错误的。但是如果像下面这样写
public static <T extends Comparable> T[] minmax(T... a)
{
Object[] mm = new Object[2];
...
return (T[]) mm;
}
调用 String[] ss= minmax(“Tom”,”Dick”,”Harry”);
编译时不会有任何警告。当Object[]引用赋给String[]变量时,将会发生ClassCastExcpetion异常。在这种情况下,可以利用翻身,调用Array.newInstance:
public static <T extends Comparable> T[] minmax(T... a)
{
T[] mm = (T[]) Array.newInstance(a.getClass().getComponentType(),2)
}
12.6.6 泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用泛型变量
public class Singleton<T>
{
private static T singleInstance; // Error
public static T getSingleInstance() //Error
{
if(singleInstance == null) construct new instance of T
return singleInstance;
}
}
这样是不行的,由于类型擦除的原因,最后只剩下了Singleton类,它只包含一个singleInstance域
12.6.7 不能抛出或捕获泛型类实例
暂略
12.6.8 注意擦除后的冲突
假定有这样的代码
public class Pair<T>
{
public boolean equals(T value)
{
return first.equals(value) && second.equals(value);
}
...
}
那么一个Pair,从概念上讲就有两个equals方法
boolean equals(String); //defined in Pair<T>
boolean equals(Object); //inherited from Object
但是,直接将我们引入歧途,上面的理解是不正确的。如果在一个泛型类中定义如上所示的equals方法,连编译都通不过,会引发这样一个错误
Name clash: The method equals(T) of type Pair has the same erasure as equals(Object) of type Object but does not override it
这是一个命名冲突的错误,之前讲过,虚拟机用返回类型和方法签名来辨别区分方法,但是由于泛型方法类型擦除的缘故, ,导致
boolean equals(T)
等同于
boolean equals(Object)
这就引发冲突了。补救的措施就是重新命名引发错误的方法。
泛型规范说明还提到另外一个原则:“要想支持擦除的转换,就需要强制限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同意接口的不同参数化。”
class Calendar implements Comparable<Calendar> {...}
class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar> {...}
上述代码是非法的,因为GregorianCalendar会实现Comparable和Comparable,这是同一个接口的不同参数化。
但是下面的非泛型代码时合法的
class Calendar implements Comparable {...}
class GregorianCalendar extends Calendar implements Comparable {...}
这里的原因是有可能与合成的桥方法产生冲突。实现了Comparable的类可以获得一个桥方法:
public int compareTo(Object other) {return compareTo((X) other);}
对于不同的X不能有两个这样的方法。
12.7 泛型类型的继承规则
Employee是Mangager的超类,但是Pair< Employee >和Pair< Manager >之间没有继承关系。
12.8 通配符类型
例如
Pair<? extends Employee>
表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair,但不是Pair。
如果有下面的代码
:
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传递给这个方法,这一点就很受限制。使用通配符类型就能解决这个问题
public static void printBUddies(Pair<? extends Employee>) {...}
那么使用通配符会通过Pair
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的子类来进行匹配,然而具体是哪个子类并不能确定,所以传递任何非null的参数都将会报错,它拒绝传递任何特定的类型。
但是getFirst就没有任何问题,因为将返回值可以赋给Employee的一个引用,其他子类可以向上转型称为被Employee的类型所指.
这就是引入有限定的通配符的关键之处。现在已经有办法区分安全的访问器方法和不安全的更改器方法了。通配符的子类型限定就提供了安全的访问器方法。下面所讲的超类型限定提供了安全的更改器方法。
12.8.1 通配符的超类型限定
语法没什么特殊的
? super Manager
这个通配符限制为Manager的所有超类型。与上面所讲的子类型限定做对比。假设有这样的类
Pair<? super Manager>
类中的方法应该是这样的
void setFirst(? super Manager)
? super Manager getFirst()
编译器不知道setFirst方法的确切参数类型,只知道应该为Manager类的超类型。所以可以用Manager类或者Manager的子类型来调用这个方法,因为它们可以安全的转型为Manager类的任何超类型。注意,不能用Manager类的超类型来调用这个方法。但是对于getFirst方法,返回的对象就不会得到保证,因为不知道具体是哪个超类,所以只能赋给Object。
直观的讲,带有超类限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
超类限定的另一种应用
Comparable本身就是一个泛型类型。声明如下:
public interface Comparable<T>
{
public int compareTo(T other);
}
在此,类型变量指示了other参数的类型。例如,String类实现Comparable,它的compareTo方法被声明为
public int compareTo(String other)
由于Comparable是一个泛型类型,可以把ArrayAlg类的min方法做得更好一些
public static <T extends Comparable<T>> T min(T[] a)
这样写看起来比只使用T extends Comparable更彻底,并且对许多类来讲,工作得更好。但是,当处理一个GregorianCalendar对象的数组时,就会出现问题,。GregorianCalendar是Calendar的子类,并且Calendar实现了Comparable。因此GregorianCalendar实现的是Comparable,而不是Compara。
用超类型可以进行救助
public static <T extends Comparable<? super T>> T min(T[] a) ..
现在compareTo方法写成
int compareTo(? super T)
这样一来,有可能被省秘境为使用类型T的对象,也有可能使用T的超类型(如果T是GregorianCalendar)。无论如何,传递一个T类型的对象给compareTo方法都是安全的。
12.8.2 无限定通配符
Pair
? getFirst()
void setFrist(?)
getFrist的返回值智能赋给一个Object。setFirst方法不能被调用,甚至不能用Object调用。Pair
public static boolean hasNulls(Pair<?> p)
{
return p.getFirst() == null || p.getSecond() == null;
}
带有通配符的可读性更强。
(个人理解,在实际运用中,单独使用?通配符的情况可能比较少。更多的是带有限定的通配符的使用。如果有误,欢迎指正)
12.8.3 通配符捕获
编写一个交换一个pair元素的方法:
public staticvoidswap(Pair<?> p)
由于通配符不是类型变量,因此,下述代码是错误的
? t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
这个问题有一个有趣的解决方案。我们可以写一个辅助方法swapHelper,如下:
public static <T> void swapHelper(Pair<T> p)
{
T t = p.getFirst();
p.setFirst(p.setSecond());
p.setSecond(t);
}
swapHelper是一个泛型方法,swap不是,它有特定的类型,Pair
public static void swap(Pair<?>p) {swapHelper(p);}
在这种情况下,swapHelper方法的参数T捕获通配符。它不知道是哪种类型的通配符,但是,这是一个明确的类型。并且swapHelper的定义只有在T指出类型时才有明确的含义。
如果直接实现了泛型版本的swap,并不一定要使用通配符,但是某些情况下,通配符捕获机制必不可免
public static void maxminBonus(Manager[] a, Pair<? super Manager> result)
{
minmaxBonus(a,result);
PairAlg.swapHelper(result);
}
12.9 反射和泛型
暂略