泛型的作用
使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性.泛型对于集合类尤其有用.
说白了就是减少类型转换,增加可读性,同时避免代码中大量的Objcet的使用带来的转换安全隐患。
简单泛型类的定义
一个泛型类(generic class)就是具有一个或多个类型变量的类.下面是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,用尖括号<>括起来,并放在类名的后面.泛型类可以有多个类型变量.例如,可以定义Pair类,其中第一个域和第二个域使用不同的类型:
-
public class Pair<T, U> { ... }
类定义中的类型变量指定方法的返回类型以及域和局部变量的类型.例如:
-
private T first; // uses the type variable
注释:类型变量使用大写形式,且比较短,这是很常见的.在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型. T(需要时还可以用临近的字母U和S)表示"任意类型".
用具体的类型替换类型变量就可以实例化泛型类型,例如:
-
Pair<String>
可以将结果想象成带有构造器的普通类:
-
Pair<String>()
-
Pair<String>(String, String)
和方法:
-
String getFirst()
-
String getSecond()
-
void setFirst(String)
-
void setSecond(String)
换句话说,泛型类可以看做普通类的工厂.
泛型方法
-
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");
书上说在大多数情况下,对泛型方法的推断没有问题,但是,如果编译器无法推断出一个合适的类型,此时编译器就会报错。
如下:
-
package com.zjf;
-
-
public class Test {
-
public static void main(String[] args) {
-
test("zjf","xhj");
-
test("zjf",11);
-
test("zjf",11,3.14);
-
test(11,3.14,0);
-
}
-
-
public static <T> void test(T... t)
-
{
-
-
}
-
}
实际上,在我的环境下(java7),不仅编译没有报错,运行也没有问题。
我猜想因为他们都是属于Object的子类,再怎么推断不出来,也可以使用Object。
类型变量的限定
使用extends 进行限制
-
public static <T extends Comparable> void test(T t1,T t2)
-
{
-
t1.compareTo(t2);
-
}
这样能保证,T必然是Comparable的子类或者接口实现,在方法里面就可以对变量a的内容使用Comparable具有的方法。
一个类型变量或通配符可以有多个限定,例如:
T extends Comparable & Serializable
泛型代码和虚拟机
虚拟机没有泛型类型对象--所有对象都属于普通类.无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(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;
-
}
-
...
-
}
因为T是一个无限定的变量,所以直接用Object替换.
在程序中可以包含不同类型的Pair,例如,Pair<String>或Pair<GregorianCalendar> .而擦除类型后就变成原始的Pair类型了.
原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换.例如,类Pair<T>中的类型变量没有显式的限定,因此,原始类型用Object替换T .假定声明了一个不同的类型.
-
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 = second;
-
}
-
}
-
}
原始类型Interval如下所示:
-
public class Interval implements Serializable
-
{
-
private Comparable lower;
-
private Comparable upper;
-
...
-
public Interval(Comparable first, Comparable second)
-
{...}
-
}
翻译泛型表达式
当程序调用泛型方法时,如果擦掉返回类型,编译器插入强制类型转换.例如,下面这个语句序列
-
Pair<Employee> buddies = ...;
-
Employee buddy = buddies.getFirst();
擦除getFirst的返回类型后将返回Object类型.编译器自动插入Employee的强制类型转换.也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
对原始方法Pair.getFirst的调用
将返回的Object类型强制转换为Employee类型
翻译泛型方法
类型擦除也会出现在泛型方法中.程序员通常认为下述的泛型方法
-
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
-
{
-
public void setSecond(Date second) { ... }
-
}
注意,此时超类Pair的setSecond中的T被擦除了,编程了setSecond(Object second),但是子类的方法中是Date,不会被擦除,依然是setSecond(Date second)。
这样,子类中的setSecond方法就不是超类中方法的重写(overwrite)。
要解决这个问题,就需要编译器在DateInterval类中生成一个桥方法(bridge method):
-
public void setSecond(Object second) { setSecond((Date)second); }
上面说的是方法参数擦除导致的问题。
还有一种方法返回类型擦除导致的问题。
如:
-
class DateInterval extends Pair<Date>
-
{
-
public Date getSecond() { }
-
...
-
}
在擦除的类型中,有两个getSecond方法:
-
Date getSecond(); // 定义在DateInterval
-
Object getSecond(); // 继承自Pair
在我们的代码中,是不允许出现这种代码的,方法的签名是方法名和参数,这两个可以说是一个方法。但是虚拟机做了特殊处理,可以应对这种情况。
注释:桥方法不仅用于泛型类型.在一个方法覆盖另一个方法时可指定一个更严格的返回类型.例如:
-
public class Employee implements Cloneable
-
{
-
public Employee clone() throws CloneNotSupportedException { ... }
-
}
Object.clone和Employee.clone方法被说成具有协变的返回类型(covariant return types).
实际上,Employee类有两个克隆方法:
-
Employee clone(); // defined above
-
Object clone(); // synthesized bridge method, overrides object.clone
合成的桥方法调用了新定义的方法.
总之,需要记住有关Java泛型转换的事实:
虚拟机中没有泛型,只有普通的类和方法.
所有的类型参数都用它们的限定类型替换.
桥方法被合成来保持多态.
为保持类型安全性,必要时插入强制类型转换.
调用遗留代码
为了保证老的版本的兼容性。jdk有如下折中:
如,在调用一个需要MyClass<String>类型作为参数的方法时,我们可以传入MyClass.
反过来,在调用一个需要MyClass类型作为参数的方法时,我们可以传入MyClass<String>.
乱入。。
HashTable是Dictionary的子类,在Java1.2版本之后,已经被Map和HashMap替换,标注为废弃了。
约束与局限性
在下面几节中,将阐述使用Java泛型时需要考虑的一些限制.大多数限制都是由类型擦除引起的.
-
不能用基本类型实例化类型参数
不能用类型参数代替基本类型,因此,没有Pair<double>,只有Pair<Double>.当然,其原因是类型擦除.擦除之后,Pair类含有Object类型的域,而Object不能存储 double 值.
虚拟机中的对象总有一个特定的非泛型类型.因此,所有的类型查询只产生原始类型.例如:
-
if (a instanceof Pair<String>) // error
实际上仅仅测试a是否是任意类型的一个Pair .下面的测试同样如此:
-
if (a instanceof Pair<T>) // error
不能实例化参数化类型的数组,因为数组有存储检查,只能存入定义的类型,而泛型化类型会使用类型擦除。我们不能保证存储的是什么。如果你定义了一个Pair<String>[]的数组,在运行期间你即使存储了一个字符串都是可以的。因为它是Object型的。
这一段书上讲的很浅,没有具体申深入探讨原因,我搜索了一下,整理如下。
先假设Java可以创建泛型数组,由于java泛型的类型擦除和数组的协变。下面的代码将会编译通过。
List<String>[] stringLists=new List<String>[1];
List<Integer> intList = Arrays.asList(40);
Object[] objects = stringLists;
objects[0]=intList;//数组是协变的 在编译期间这是个object数组 可以存储任何类型 真正的类型检查是在运行时处理的。
String s=stringLists[0].get(0);
由于泛型的类型擦除,List<Integer>,List<String>与List在运行期并没有区别,所以List<String>放入List<Integer>并不会产生ArrayStoreException异常。但是String s=stringLists[0].get(0);将会抛出ClassCastException异常。如果允许创建泛型数组,就绕过了泛型的编译时的类型检查,将List<Integer>放入List<String>[],并在实际存的是Integer的对象转为String时抛出异常。
不能使用像 new T(...),new T[...]或T.class这样的表达式中的类型变量.例如下面的Pair<T>构造器就是非法的:
-
public Pair() { first = new T(); second = new T(); } // error
类型擦除将T改变为Object,而且本意肯定不希望调用 new Object().
而且不能调用: first = T.class.newInstance();//ERROR
因为, 表达式 T.class 是不合法的,必须像下面这样设计 API 以便可以支配Class 对象:
-
public static<T> Pair<T> makePair(Class<T> cl)
-
{
-
try{ return new Pair<>(c1.newInstance(), c1.newInstance()) }
-
catch(Exception ex) {return null;}
-
}
-
不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象。且泛型类扩展 Throwable 也是不合法的;
如, public class Problem<T> extends Exception {} // ERROR--can't extend Throwable
catch 子句中不能使用类型变量, 以下方法不能通过编译:
public static <T extends Throwable> void dowork(Class<T> t)
{
...
catch(T e) // ERROR--can't catch type variable
}
不过, 在异常规范中适用类型变量是允许的,以下方法是合法的:
public static <T extends Throwable> void dowork(T t) throws T // OK
这个也不太清楚为什么,应该是异常框架的机制导致的。
-
不能在静态域或方法中引用类型变量:
public class Singleton<T>
{
private static T single; // ERROR
private static getSingle() // ERROR
{}
}
因为类型擦除后, 只剩下 Singleton 类, 它只包含一个 singleInstance 域;
这个原则不太清楚,完全可以使用Object来消除,这个是可以跑起来的。不知道为什么不让这么用。
泛型类型的继承规则
无论S与T有什么联系(甚至是父类与子类的关系),通常,Pair<S>与Pair<T>没有什么联系.
也就是说在一个定义了需要Pair<Person>参数的方法,不能传入Pair<Man>。反之也不行。
Pair<Person> 和 Pair<Man>也不能相互赋值。
之所以可以把一个Men作为Person使用,因为它可以代表Person做一切操作。
但是如果你把一个List<Men>作为List<Person>使用,那么:
-
List<Men> m = new ArrayList<Men>();
-
List<Person> p = m;
-
p.add(new Person());
假设第二行可以执行,那么第三行就出事儿了。我们可以向一个List<Person>中add Person对象,但是其实p指向的是一个List<Men>对象,我们不能对一个List<Men>对象中add Person对象。
泛型类可以扩展实现其他泛型类,这一点和普通的类没有区别。
如List<T>和ArrayList<T>。
ArrayList<T>实现了List<T>,它仍然是一个泛型类。
还有一种情况,实现了一个具体的Comparable<Person>,Person类已经不是泛型类。如:
-
public class Person implements Comparable<Person>{
-
public Person(Integer id) {
-
this.id = id;
-
}
-
private Integer id;
-
-
public Integer getId() {
-
return id;
-
}
-
public void setId(Integer id) {
-
this.id = id;
-
}
-
@Override
-
public int compareTo(Person o) {
-
// TODO Auto-generated method stub
-
return this.id.compareTo(o.id);
-
}
-
}
通配符类型
extends
通配符类型Pair<? extends Employee>表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair<Manager>,但不是Pair<String>.
但是这样岂不是会出现我们说的对一个List<Men>对象中add Person对象的现象吗?
所以,java做了限制:
Pair<? extends Employee>里的所有T,如果是参数就变成了? extends Employee类型,返回类型也变成了? extends Employee。
对于参数? extends Employee,我们不确定他是Employee的哪一个子类(还是那个道理,我们不能对一个运行时T为Manager的方法,传入Employee),所以只能传入null。
对于返回类型是? extends Employee:
我们可以使用Employee = 来代表。因为他们都是Employee的子类。
super
-
? super Manager
这个通配符限制为Manager的所有超类型.
? super Manager如果是参数,那么可以向它传入所有Manager的子类。
? super Manager如果是返回值,那么只能把它赋给一个Object
这基本和extends是相反的。
也就是说,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取.
无限定通配符
还可以使用无限定的通配符,例如,Pair<?>.初看起来,这好像与原始的Pair类型一样.实际上,有很大不同,类型Pair<?>有方法如下所示:
-
? getFirst()
-
void setFirst(?)
getFirst的返回值只能赋给一个Object, 可以调用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)
但是,带有通配符的版本可读性更强.
List<?>和List和List<Object>的差别:
通配符类型 List<?> 与原始类型 List 和具体类型 List<Object> 都不相同。如果说变量 x具有 List<?> 类型,这表示存在一些 T 类型,其中 x 是 List<T>类型,x 具有相同的结构,尽管我们不知道其元素的具体类型。这并不表示它可以具有任意内容,而是指我们并不了解内容的类型限制是什么 — 但我们知道存在 某种限制。另一方面,原始类型 List 是异构的,我们不能对其元素有任何类型限制,具体类型 List<Object> 表示我们明确地知道它能包含任何对象(当然,泛型的类型系统没有 "列表内容" 的概念,但可以从 List 之类的集合类型轻松地理解泛型)。
通配符捕获
编写一个交换一个Pair元素的方法:
-
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);
-
}
注意,swapHelper是一个泛型方法,而swap不是,它具有固定的Pair<?>类型的参数.
现在可以由swap调用swapHelper:
-
public static void swap(Pair<?> p) { swapHelper(p); }
在这种情况下,swapHelper方法的参数T捕获通配符.它不知道是哪种类型的通配符,但是,这是一个明确的类型,并且<T>swapHelper的定义只有在T指出类型时才有明确的含义.