在面向对象编程语言中,多态算是一种泛化机制。
例如你可以将方法的参数类型设为基类,那么该方法就可以接受从这个类中导出的任何类作为参数。但是,考虑到除了final类不能扩展,这种灵活性大大降低。
如果方法的参数是一个接口,而不是一个类,这种限制就放松很多,可是有时候,使用了接口,对程序的约束也还是太强了。因为一旦指明了接口,它就要求你的代码必须使用特定的接口。
Java SE5的重大变化之一就是:泛型的概念。泛型实现了参数化类型的概念,使代码可以应用于多种类型。
简单泛型
直接看例子
public class Holder<T> {
private T a;
public Holder(T a) { this.a = a; }
public void set(T a) { this.a = a; }
public T get() { return a; }
public static void main(String[] args) {
Holder<String> holder = new Holder<String>("String");
String s = holder.get();
// Error
// holder.set(1);
// holder.set(1.1);
}
}
在这个例子中可以看出,Holder是持有对象T的类(Holder< T>),在构造器,方法参数,成员变量以及返回值都可以使用对象T的类型。
class I<T> {
class K<T>{
}
class KK{
private T t;
}
// Error
// public static class KKK<T>{ }
// public static class KKK{
// private static T t;
// }
}
这里可以看到,内部类可以持有对象T的类型,而嵌套类不可以。
泛型接口
直接看例子
public interface GenericsInstance<T> {
T next();
void set(T t);
class InnerClass<T>{
}
public static T t;
}
这里可以看到,GenericsInstance是持有对象T的接口,在其返回值,方法参数,成员变量已经嵌套类都可以使用对象T的类型
泛型方法
可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是。
泛型方法使得该方法能够独立于类而产生变化。以下是一个基本的指导原则:无论何时,只要你能做到,就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白
定义泛型方法,只需将泛型参数列表置于返回值之前
public class GenericMethods {
// 泛型方法
public <T> void f(T t){
System.out.println(t.getClass().getName());
}
public static void main(String[] args){
GenericMethods gm = new GenericMethods();
gm.f("");
gm.f(1);
gm.f(1.2);
}
}
这个例子中,只有方法f()拥有类型参数T。
可以看下面例子
class T{
public <T> T get(T t){
T tt = null;
return tt;
}
// 可变参数与泛型方法
public static <E> void f(E... es){
for (E e:es){
// do something
}
}
}
注意,当在使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,比如前面的gm.f(1);。因为编译器会为我们找出具体的类型。这称为类型参数推断
匿名内部类
泛型还可以应用于内部类以及匿名内部类
内部类前面例子已经简单描述,这里看一下匿名内部类的例子
public Interface Generator<T>{
T next();
}
class Customer{
private static long counter = 1;
private final long id = counter++;
private Customer() { }
public String toString() { return "Customer " + id; }
// 匿名内部类与泛型
public static Generator<Customer> generator(){
return new Generator<Customer>(){
public Customer next(){
return new Customer();
}
}
}
}
擦除的神秘之处
例如,可以声明ArrayList.class,但是不能声明ArrayList< Integer>.class。
public class ErasedTypeEquivalence{
public static void main(String[] args){
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
}
/*Output:
true
*/
上面的c1和c2被认为相同的类型
在泛型代码内部,无法获得任何有关泛型参数类型的信息
Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体类型信息都被擦除了,你唯一知道的就是你在使用一个对象。
因此,List< String>和List< Integer>在运行时事实上是相同类型,这两种形式都被擦除成为它们的“原生”类型List
下面看个例子:
public class HasF{
public void f(){
}
}
class Manipulator<T>{
private T obj;
public Manipulator(T t){
this.obj = t;
}
public void manipulate(){
// Error
//obj.f();
}
}
由于有了擦除,所以上面obj.f()方法不能调用。
为了调用f(),我们必须协助泛型类,给定泛型类的边界,这里使用extends关键字:
// 协助泛型类,给定泛型类的边界
class Manipulator<T extends HasF>{
private T obj;
public Manipulator(T t){
this.obj = t;
}
public void manipulate(){
obj.f();
}
}
边界< T extends HasF>声明T必须具有类型HasF或者从HasF导出的类型。这样就可以安全调用f()
擦除的问题
擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作之中,例如转型、instanceof操作和new表达式
另外,使用泛型并不是强制的
class GenericBase<T>{
private T element;
public void set(T arg) { element = arg; }
public T get() { return element; }
}
class Derived1<T> extends GenericBase<T> { }
class Derived2 extends GenericBase { } // No warning
// class Derived3 extends GenericBase<?> { } // Error
// class or interface without bounds
public class ErasureAndInheritance{
@SuppressWarnings("unchecked")
public static void main(String[] args){
Derived2 d2 = new Derived2();
Object obj = d2.get();
d2.set(obj); // Warning here
}
}
边界处的动作
public class ArrayMaker<T> {
private Class<T> kind;
public ArrayMaker(Class<T> kind){
this.kind = kind;
}
@SuppressWarnings("unchecked")
T[] create(int size){
// Type safety: Unchecked cast from Object to T[]
return (T[]) Array.newInstance(kind, size);
}
public static void main(String[] args) {
ArrayMaker<String> stringMaker = new ArrayMaker<String>(String.class);
String[] stringArray = stringMaker.create(5);
System.out.println(stringArray);
}
}
/*Output:
[null, null, null, null, null]
*/
即使kind被存储为Class< T>,擦除也意味着它实际类型将被存储为Class,没有任何参数,因此,在创建数组时,这不会产生具体的结果,所以必须转型,这将产生一条警告。
注意,对于在泛型中创建数组,使用Array.newInstance()是推荐方式
泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型
擦除的补偿
由于擦除,一下操作将无法操纵
public class Erased<T>{
private final int SIZE = 10;
public static void f(Object arg){
if( arg instanceof T ){ } // Error
T var = new T();// Error
T[] array = new T[SIZE];// Error
T[] array = (T) new Object[SIZE];// Warning
}
}
通过引入类型标签对擦除进行补偿,这意味着需要显式地传递类型的Class对象,以便在表达式中使用,如果引入类型标签,就可以转而使用动态的isInstance()。
class Building { }
class House extends Building { }
public static class ClassTypeCapture<T>{
Class<T> kind;
public ClassTypeCapture(Class<T> kind){
this.kind = kind;
}
public boolean f(Object arg){
return kind.isInstance(arg);
}
public static void main(String[] args) {
ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);
System.out.println(ctt1.f(new Building()));
System.out.println(ctt1.f(new House()));
ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);
System.out.println(ctt2.f(new Building()));
System.out.println(ctt2.f(new House()));
}
}
/*Output:
true
true
false
true
*/
创建类型实例
new T()无法实现,部分原因是因为擦除,另一部分原因是因为编译器不能验证T具有默认(无参)构造器,但是C++却可以实现,因为它是在编译期受到检查
Java中的解决方案是传递一个工厂对象
class ClassAsFactory<T>{
T x;
public ClassAsFactory(Class<T> kind){
try{
x = kind.newInstance();
}catch(Exception e){
throw new RuntimeException(e);
}
}
}
class Employee { }
public class InstantiateGenericType{
public static void main(string[] args){
ClassAsFactory<Employee> fe = new ClassAsFactory<Employee>(Employee.class);
// Exception
ClassAsFactory<Integer> fi = new ClassAsFactory<Integer>(Integer.class);
}
}
第一个可以创建,但是ClassAsFactory<Integer>
失败,因为Integer没有默认的构造器,而是应该使用显示工厂
interface Factory<T>{
T create();
}
class Foo<T>{
private T x;
// 构造器:public <F extends Factory<T>> Foo(f factory)
public <F extends Factory<T>> Foo(f factory) {
x = factory.create();
}
// ...
}
class IntegerFactory implements Factory<Integer>{
public Integer create(){
return new Integer(0);
}
}
class Widget{
public static class Factory implements Factory<Widget>{
public Widget create(){
return new Widget();
}
}
}
public class FactoryConstraint{
public static void main(String[] args){
new Foo<Integer>(new IntegerFactory);
new Foo<Widget>(new WidgetFactory);
}
}
另一种方式是模板方法设计模式
abstract class GenericWithCreate<T>{
final T element;
GenericWithCreate() { element = create(); }
abstract T create();
}
class X { }
class Creator extends GenericWithCreate<X>{
X create() { return new X(); }
void f(){
System.out.println(element.getClass().getSimpleName());
}
}
public class GreatorGeneric{
public static void main(String[] args){
Creator c = new Creator();
c.f();
}
}
泛型数组
看一下例子
class Generic<T> { }
public class ArrayOfGenericReference{
// 泛型数组
static Generic<Integer>[] gia;
}
编译器将接受这个程序,而不会产生警告,但是,永远都不能创建这个确切类型的数组(包括类型参数),这一点令人困惑。
既然所以数组无论它们持有的类型如何,都具有相同的结构,那么看起来应该能够创建一个Object数组,并将其转型为所希望的数组类型,事实上这可编译,但是不能运行:
publicclassArrayOfGeneric{
static Generic<Integer>[] gia;
public static void main(String[] args){
// ClassCaseException
// gia = (Generic<Integer>[])new Object[100];
// 成功创建泛型数组
gia = (Generic<Integer>[])new Generic[100];
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<Integer>();
//gia[1] = new Object();
//gia[2] = new Generic<Double>();
}
}
成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型
即:gia = (Generic<Integer>[])new Generic[100];
public class GenericArray<T>{
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int size){
array = (T[])new Object[size];
}
public void put(int index, T item){
array[index] = item;
}
public T get(int index){
return array[index];
}
public T[] rep() { return array; }
public static void main(String[] args){
GenericArray<Integer> gai = new GenericArray<Integer>(10);
// ClassCastException
// Integer[] ia = gai.rep();
Object[] oa = gai.rep();
}
}
rep()方法返回T[],并将结果作为Integer[]引用来捕获,会产生ClassCastException,这是因为实际运行时类型是Object[]。
因为有了擦除,数组的运行时类型就只能是Object[],如果我们立即将其转型为T[],那么在编译期该数组的实际类型就将丢失,而编译器可能错过某些潜在的错误检查。正因为这样,最好是在集合内部使用Object[],然后当你使用数组元素时,添加一个对T的转型。这一点在集合框架的源码可以见到。
示例:
public class GenericArray<T>{
private Object[] array;
public GenericArray(int size){
array = new Object[size];
}
@SuppressWarnings("unchecked")
public T get(int index){ return (T)array[index]; }
@SuppressWarnings("unchecked")
public T[] rep(){
return (T[])array;
}
public static void main(String[] args){
GenericArray<Integer> gai = new GenericArray<Integer>(10);
for(int i = 0; i < 10; i++)
gai.put(i,i);
for( int i = 0; i < 10; i ++)
System.out.println(gai.get(i)+"");
// ClassCastException
// Integer[] ia = gai.rep();
}
}
然而,如果你调用rep(),它还是尝试着将Object[]转型为T[],这仍旧是不正确的,将在编译期产生警告,运行时产生异常。
public class GenericArrayWithTypeToken{
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class<T> type, int sz){
array = (T[]) Array.newInstance(type,sz);
}
public void put(int index, T item){
array[index] = item;
}
public void get(int index) { return array[index]; }
public T[] rep() { return array; }
public static void main(String[] args){
GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<Integer>(Integer.class,10);
// work
Integer[] ia = gai.rep();
}
}
通过传递类型标记Class<T>
到构造器中,以便从擦除中恢复,使得我们可以创建需要的实际类型的数组。
边界
因为擦除移除了类型信息,所以可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。但是如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法
interface HasColor { int getColor(); }
class Colored<T extends HasColor>{
T item;
Colored(T item){ this,item = item; }
T getItem() { return item; }
// 泛型边界
int color() { return item.getColor(); }
}
class Dimension { public int x,y,z; }
//T extends Dimension & HasColor:类在前,接口在后
class ColoredDimension<T extends Dimension & HasColor>{
T item;
ColoredDimension(T item) { this.item = item; }
T getItem() { return item; }
// 泛型边界
int color() { return item.getColor(); }
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
}
interface Weight { int weight(); }
//T extends Dimension & HasColor & Weight:类在前,接口在后
class Solid<T extends Dimension & HasColor & Weight>{
T item;
ColoredDimension(T item) { this.item = item; }
T getItem() { return item; }
// 泛型边界
int color() { return item.getColor(); }
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
int weight() { return item.weight(); }
}
class Bounded extends Dimension implements HasColor, Weight{
public int getColor() { return null; }
public int weight() { return 0; }
}
public class BasicBounds{
public static void main(String[] args){
Solid<Bounded> solid = new Solid<Bounded>(new Bounded());
solid.color();
solid.getY();
solid.weight();
}
}
可以看到使用<T extends Dimension & HasColor & Weight>
实现多边界,而且类在前,接口在后。
更多层次的情况在书中。
通配符
通配符被限制为单一边界,所以List<? extends A & B>
会报错
看一下例子:数组向导出类型的数组赋予基类型的数组引用
class Fruit{ }
class Apple extends Fruit{ }
class Jonathan extends Apple{ }
class Orange extends Fruit{ }
public class GovariantArrays{
public static void main(String[] args){
//
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonahan(); // OK
// 编译器不会报错,运行期报错
// fruit[0] = new Fruit(); // ArrayStoreException
// fruit[1] = new Fruit(); // ArrayStoreException
}
}
实际数组类型是Apple[],应该只能放置Apple和Apple的子类型,这在编译期和运行时都可以工作。
但是编译器允许你将Fruit放置到这个数组中,因为它有一个Fruit[]引用,但是运行时数组机制知道它处理的是Apple[],因此会抛出异常
泛型的主要目标之一就是将这种错误检测移入到编译期
因此使用泛型容器来代替数组
public class NonCovariantGenerics{
// 编译错误
List<Fruit> flist = new ArrayList<Apple>();
}
这里要明确一点:Apple的List不是Fruit的List,尽管Apple是一种Fruit类型
那怎么解决这问题呢?
这时候需要在两种类型之间建立某种类型的向上转型关系
public class GenericsAndCovariance{
public static void main(String[] args){
List<? extends Fruit> flist = new ArrayList<Apple>();
// 编译错误
flist.add(new Apple());
flist.add(new Fruit());
flist.add(new Object());
// 可以运行
flist.add(null);
Fruit f = flist.get(0);
flist.contains(new Apple()); // 参数类型是Object
flist.indexOf(new Apple()); // 参数类型是Object
}
}
flist类型现在是List<? extends Fruit>
,可以将其读作“具有任何从Fruit继承的类型的列表”。但是,这实际上并不意味着这个List将持有任何类型的Fruit。
通配符引用的是明确的类型,因此它意味着“某种flist引用没有指定的具体类型”
因此当你指定一个ArrayList<? extends Fruit>
时,add的参数也变成? extends Fruit。编译器不能了解这里需要Fruit哪个具体子类型,因此它不会接受任何类型的Fruit
但是,在使用contains和indexOf时,类型参数是Object,因此不涉及任何通配符,而编译器也将允许这个调用。这一点在ArrayList等源码可以看到
那应该怎么解决呢??
另一条路:超类型通配符
这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定<? super MyClass>
,甚至或者使用类型参数:<? super T>
(尽管你不能对泛型参数给出一个超类型边界;即不能声明<T super MyClass>
)
因此:
public class SuperTypeWildcards{
static void writeTo(List<? super Apple> apples){
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
}
}
参数Apple是Apple的某种基类型的List,这样你就知道向其中添加Apple或Apple的子类型是安全的
超类型边界放松了在可以先方法传递的参数上所作的限制,再看一个例子:
public class GenericWriting{
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static <T> void writeExact(List<T> list,T item){
list.add(item);
}
static void f1(){
writeExact(apples, new Apple());
// Error
// writeExact(fruit, new Apple());
}
static <T> void writeExactWildcard(List<? super T> list, T item){
list.add(item);
}
static void f2(){
writeExactWildcard(apples, new Apple());
writeExactWildcard(fruit, new Apple()); // OK
}
public static void main(String[] args){
f1();
f2();
}
}
继续一个例子:
public class GenericReading{
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static <T> T readExact(List<T> list){
return list.get(0);
}
static void f1(){
Apple a = readExact(apples);
Fruit f = readExact(fruit);
f = readExact(apples);
}
// 通过泛型类读取
static class Reader<T> {
T readExact(List<T> list) { reutrn list.get(0); }
}
static void f2(){
Reader<Fruit> fruitReader = new Reader<Fruit>();
Fruit f = fruitReader.readExact(fruit);
// Error
// Fruit f = fruitReader.readExact(apples);
}
// 改进
static class CovariantReader<T>{
T readCovariant(List<? extends T> list){
return list.get(0);
}
}
static void f3(){
CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
Fruit f = fruitReader.readExact(fruit);
// OK
Fruit a = fruitReader.readExact(apples);
}
public static void main(String[] args){
f1(); f2(); f3();
}
}
上面这例子说明,List<? extends T>
列表中所有对象至少是一个T,并且可能是从T导出的某种对象
问题
实现参数化接口
一个类不能实现同一个泛型接口的两个变体,由于擦除的原因,这两个变体会成为相同的接口
interface Payable<T>{ }
class Employee implements Payable<Employee>{ }
class Hourly extends Employee implements Payable<Hourly>{ } // Error
Hourly不能编译,因为擦除会将Payable<Employee>
和Payable<Hourly>
简化为相同的类Payable,这样,上面的代码就意味着在重复两次实现相同的接口。
十分有趣的是,如果从Payable的两种用法都移除掉泛型参数(就像编译器在擦除阶段所作的那样),这段代码就可以编译。
重载
下面程序是不能编译的
public class UserList<W,T>{
void f(List<T> v) {}
void f(List<W> w) {}
}
由于擦除的原因,重载方法将产生相同类型的签名
因此可以改成
public class UserList<W,T>{
void f1(List<T> v) {}
void f2(List<W> w) {}
}
基类劫持了接口
class ComparablePet implements Comparable<ComparablePet>{
public int compareTo(ComparablePet arg) { return 0; }
}
//Error
class Cat extends ComparablePet implements Comparable<Cat>{ }
class Hamster extends ComparablePet implements Comparable<ComparablePet>{
public int compareTo(ComparablePet arg) { return 0; }
}
可以看到,一旦为Comparable确定了ComparablePet参数,那么其他任何实现类都不能与ComparablePet之外的任何对象比较