所谓泛型,就是在定义类、接口、方法的时候使用“类型形参”,而在真正使用类、接口、方法的时候,动态的指定实参(也就是指定某一种类型,或者用通配符来通配多种类型)。
泛型类
例如,使用泛型定义 Foo
类:
class Foo<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
使用 Foo
类:
public class Test0830 {
public static void main(String[] args) {
Foo<String> foo = new Foo<String>(); // 可以省略为: Foo<String> foo = new Foo<>();
foo.setT("abc");
System.out.println(foo.getT());
Foo<Integer> fo = new Foo<>();
fo.setT(123);
System.out.println(fo.getT());
}
}
需要注意的是, Foo<A>
和 Foo<B>
是不兼容的,即使A和B是父类和子类的关系。
例如:假定有如下继承关系:
下面的代码编译报错:
Foo<Fruit> foo = new Foo<Apple>(); // 编译报错
究其原因,假如可以这么做,那么 foo
可以动态的指向 Foo<Apple>
或者 Foo<Banana>
对象。既然foo
是 Foo<Fruit>
,那么它一定可以调用 setT()
方法,传入一个 Fruit
(或其子类)对象,下面的代码一定是work的:
foo.setT(new Apple());
但是 foo
可能指向的是 fooBanana
对象,Apple
和 Banana
是不兼容的,这就矛盾了。
(其实要这么设计也行,大不了到运行期再报错,只不过Java泛型的设计原则是,只要编译期没有问题,那么运行期就不会有类型兼容的问题。)
不使用泛型
在不使用泛型的时候,就是这样,编译期干啥都行,如果有问题,到运行期再报错。看下面的例子:
Foo foo = new Foo();
foo.setT(123);
System.out.println(foo.getT()); // 123
foo.setT("abc");
System.out.println(foo.getT()); // abc
这个例子没有任何问题,运行正常,再看下面的例子:
Foo foo = new Foo<Apple>();
foo.setT(123);
System.out.println(foo.getT());
foo.setT("abc");
System.out.println(foo.getT());
编译期或者运行期会有问题吗?看起来编译期应该没问题,毕竟定义foo时没有指定泛型,foo.setT(123)
应该能通过编译,大概到运行期的时候才会报错吧,因为foo实际指向的是 Foo<Apple>
对象。
实际上,在运行期也没有报错!这就奇怪了,明明指向的是 Foo<Apple>
对象, setT()
方法怎么能传入 123
或者 "abc"
而不报错呢?
这就涉及到泛型擦除了,anyway,这样做是允许的,下面的代码才会报错:
Foo<Apple> foo1 = new Foo<>();
Foo foo = foo1;
// foo1.setT(123); //编译期报错
foo.setT(123); // OK
System.out.println(foo.getT()); // OK
System.out.println(foo1.getT()); // OK
foo1.getT().doSth(); //运行期报错,java.lang.ClassCastException
类型通配符
回到前面的例子, Foo<Fruit>
和 Foo<Apple>
不兼容,那如果希望 setT()
方法能接受任意水果,那怎么办呢,总不至于写N个重载方法吧。解决办法就是,使用类型通配符 ?
。
Foo <? extends Fruit> foo = null;
foo = new Foo<Apple>(); // OK
foo = new Foo<Banana>(); // OK
此处的 Foo <? extends Fruit>
可以看做 Foo <Apple>
和 Foo <Banana>
的父类,所以就可以兼容二者了。
但是,这还是不能解决 setT()
的问题啊。如果你这么想,那就对了,这个问题确实无法解决。由于“只要编译期没有问题,那么运行期就不会有类型兼容的问题”的原则,为了不在运行期报错,只好在编译期报错了。
foo.setT(new Apple()); // 编译期报错
实际上,此处不管传什么参数都会报错,也就是说根本无法调用foo的setT()方法。
更典型的例子如下:
List<? extends Fruit> list = new ArrayList<Apple>();
list.add(new Apple()); //编译报错
请注意,上面编译报错是因为定义foo时,指定了其上限(协变)。如果是指定其下限(逆变),或者指定明确的类型,就可以调用 setT()
方法。
Foo <? super Apple> foo = null;
foo = new Foo<Apple>(); // OK
foo = new Foo<Fruit>(); // OK
foo = new Foo<Object>(); // OK
foo = new Foo<Banana>(); // 编译期报错,因为Banana不是Apple的父类
foo = new Foo<GreenApple>(); // 编译期报错,因为GreenApple不是Apple的父类
那么,调用 setT()
方法,都可以传哪些类型的参数呢?
foo.setT(new Apple()); // OK
foo.setT(new GreenApple()); // OK
foo.setT(new Fruit()); //
foo.setT(new Object()); //
foo.setT(new Banana()); //
简言之,可以传下限(本例中是 Apple
)或其子类。道理很简单,不管foo实际指向什么对象,其泛型一定是Apple或其某一种父类,而“Apple或其父类”一定兼容Apple或者子类(向上转型是安全的)。
类型通配符上限( Foo<? extends Xxx>
)
- 兼容
Foo<Yyy>
,其中Yyy
是Xxx
或其子类。 - 不可调用“泛型类型作为参数”的方法(因为无法确定类型实参的真正类型)。
- 可以调用“泛型类型作为返回值”的方法,泛型类型都可看作
Xxx
(当然也可看做Xxx
的父类)。
类型通配符下限( Foo<? super Xxx>
)
- 兼容
Foo<Zzz>
,其中Zzz
是Xxx
或其父类。 - 可以调用“泛型类型作为参数”的方法,可传入
Xxx
或其子类(因为类型实参的真正类型是Xxx
或其父类,一定会兼容Xxx
或其子类)。 - 可以调用“泛型类型作为返回值”的方法,不过只能当做
Object
(因为类型实参的真正类型是Xxx
或其父类,所以一定都可以看作Object
)。
泛型方法
前面的例子中使用了泛型类,其实泛型方法也类似,如果只是在某一个方法上需要使用泛型,那就没必要用泛型类,只需使用泛型方法。
在用法上,泛型类是在类名后面加上 <T>
,而泛型方法则是把 <T>
放在方法返回值类型前面。
class Bar {
public <T> void printT(T t) {
System.out.println("printing " + t);
}
}
使用 Bar
类:
Bar bar = new Bar();
bar.printT(123);
bar.printT("abc");
注意,在使用泛型方法时,不需要显式指定其实际类型,编辑器是根据实际传入的值来推断其类型的。
对于 xxx(T t1, T t2)
这样的泛型方法,调用该方法时,两个实参的类型不需要相同。
class Bar {
public <T> void printT(T t1, T t2) {
System.out.println("printing " + t1 + ", " + t2);
}
}
Bar bar = new Bar();
bar.printT(123, 456); // OK
bar.printT("abc", "def"); // OK
bar.printT(789, "ghi"); // OK
本例中,编译期应该是把 T
推断为 Object
了(兼容Integer和String)。
再看下面的例子:
class Bar {
public <T> void test(Collection<T> t1, Collection<T> t2) {
// do something
}
}
Bar bar = new Bar();
bar.test(new ArrayList<Object>(), new ArrayList<Object>()); // OK
bar.test(new ArrayList<Object>(), new ArrayList<String>()); // 编译期报错
本例中,编译期报错,是因为形参( Collection<T>
)和实参( ArrayList<String>
)不兼容。
要想改为兼容,可使用类型通配符 ?
:
public <T> void test(Collection<T> t1, Collection<? extends T> t2) {
// do something
}
这样, T
可被推断为 Object
,上面报错的那行代码就OK了。不过要注意,t2使用了类型通配符指定了上限,所以无法调用其参数包含了该泛型的方法。
<T>
和 <?>
的区别
前者是形参,后者是实参。也就是说, <T>
出现在类或者方法的定义里面, <?>
出现在类或者方法的使用里面。
定义方法 printT()
:
public <T> void printT(T t1, T t2) {
System.out.println("printing " + t1 + ", " + t2);
}
使用类型 List
:
List<? extends Fruit> list = null;
再看下面的代码:
public void doSth(List<?> list) {
// do something
}
虽然是在定义 doSth()
方法,但是 <?>
修饰的是List,也就是对List的使用。
<T>
和 <?>
可以同时出现:
public <T> void test(Collection<T> t1, Collection<? extends T> t2) {
// do something
}
这个例子中, <?>
是对Collection的使用, <T>
是对test()方法的定义。
如果 <T>
在泛型方法定义中只使用了一次,那它唯一的作用,就是可以在此处传入不同的实际类型,对于这种情况,无需用 <T>
,只要用 <?>
即可。
换句话说,使用 <T>
是为了在多处使用,互相依赖。比如前面的例子:
public <T> void test(Collection<T> t1, Collection<? extends T> t2) {
// do something
}
通配符可用于定义变量:
List<? extends Fruit> list = null;
此处, <? extends Fruit>
是泛型类 List
的类型实参。
思考题
List
, List<Object>
, List<?>
, List<? extends Object>
有何区别?
List
没有泛型的限制,可以“想做什么就做什么”。List<Object>
不兼容其它List<Xxx>
。List<?>
兼容其它的List<Xxx>
,但是不能调用“泛型类型作为参数”的方法。List<? extends Object>
个人感觉它等同于List<?>
。