浅谈Java泛型
相信每个Java程序员对泛型都不陌生,不少人也用过泛型,但是泛型中确实有些点容易让人迷惑,下面我结合自己的使用经历和理解谈谈对泛型的认识,不求面面俱到,但求切中要害。
一、泛型是什么
引用Java文档的解释,
A generic type is a generic class or interface that is parameterized over types.
大致的意思就是类型经过参数化的类或接口。
二、为什么要泛型
在泛型出现之前你要定义一个存储水果类的列表,你只能这样写,
List fruits = new ArrayList();
Elephant e = new Elephant();
fruits.add(e);
Fruit f = (Fruit) fruits.get(0);
虽然定义了一个名叫fruits的列表,但是你里面存大象也没人管你,只有在运行时你试图将列表的元素赋值给一个水果时才会报错。为了更早的发现这种错误,Java在5.0引入了泛型机制(Generics)。有了泛型上面的程序就可以这么写,
List<Fruit> fruits = new ArrayList<Fruit>();
Elephant e = new Elephant();
fruits.add(e); // Compile error
这样当你往水果的列表里塞一个大象时编译器就会报错,而不用等到运行时,而且也避免了显式的转型。
三、泛型分类
从程序的层次上,泛型分为泛型类和泛型方法。比如Java中的ArrayList
类就是一个泛型类,
public class ArrayList<E>
而集合工具类Collections
中的emptyList
方法就是一个泛型方法,
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
到这里其实没什么要说的。
四、更进一步
1、通配符?
通配符代表的意思应该是“某个或某些具体但不确定的类型”,首先具体的是指将来要用某个具体的类来替换通配符,其次不确定是指当前还确定不了是哪种具体类型。
2、边界
边界也就是某种类型的子类型或父类型,即super
定义的下界和extends
定义的上界。
3、类型擦除
如果用一句话解释就是用不用泛型编译后的代码是一样的,更详细更准确的解释是由于泛型是在Java 5.0引入的,为了兼容老版本的Java,编译器会将泛型参数替换为它的边界(上界),如果有多个边界,只保留最左边的,如果没有边界替换为Object
,最终保留下来的只有正常的类、接口和方法。
public class GeneralTest<T extends Comparable<T> & Iterable<T> & Serializable> {
void test1(T t) {
...
}
<E> void test2(E e) {
...
}
}
编译后生成的代码为,
void test1(T);
descriptor: (Ljava/lang/Comparable;)V
<E> void test2(E);
descriptor: (Ljava/lang/Object;)V
可以看到T的边界有三个Comparable<T> & Iterable<T> & Serializable
,编译后只保留了Comparable
,而且Comparable
的泛型<T>
也去掉了;test2
中的E没有边界,它直接被替换为了Object
,而且test2
作为泛型方法编译后也没有任何泛型的信息。
4、替换原则
一个类型的变量可以接受子类型的变量,一个据有某种参数的方法可以在参数的子类型上调用。这个原则几乎是面向对象编程的基础,它可以让我们这么写代码,
Fruit a = new Apple();
List l = new ArrayList();
可以把一个“苹果”赋值给“水果”,可以把ArrayList
赋值给List
。
5、PECS原则
也就是所谓的Producer Extends, Consumer Super原则,作为生产者时使用extends
,作为消费者时使用super
,这条原则其实是替换原则的推论。
List<??> list = Arrays.asList(1, 1.3, 5L);
Number i = list.get(0);
Map<String, ??r> map = new HashMap<>();
map.put("212", 1);
比如你想要从list
中取出的数据可以赋值给一个Number
,根据替换原则,“一个类型的变量可以接受子类型的变量”,你就得定义list
中的类型都是Number
的子类型。那么如何表示一个Number
的子类型呢,因为extends
在Java中本来就表示继承的意思,所以很自然的想法就是? extends Number
,这恰恰就是正确答案。
再比如,你想在map
值里存储各种数字,根据替换原则,“一个据有某种参数的方法可以在参数的子类型上调用”,也就是你想要put
方法适用在各种数字类型上,“各种数字类型”是这里的“子类型”,所以你需要定义map
的值是“各种数字类型”的父类型才可以,表示父类Java中同样有个关键字super
,所以你可能猜想? super Number
表示的就是这个意思,没错,答案就是这个。
你可以看到,在两种情况下,确定泛型是替换原则中的子类型还是父类型是关键中的关键。这恰恰就是PECS原则的内容,即作为生产者,将泛型传递给别的变量时,使用extends
;作为消费者,将别的变量传递给泛型时,使用super
。
6、是泛型方法还是通配符
在泛型中,经常面临的一个抉择就是是使用泛型方法还是使用通配符。比如下面的方法,
interface Collection<E> {
public boolean containsAll(Collection<?> c);
public boolean addAll(Collection<? extends E> c);
}
如果写成泛型方法,
interface Collection<E> {
public <T> boolean containsAll(Collection<T> c);
public <T extends E> boolean addAll(Collection<T> c);
}
可以看到类型参数T在方法中只使用了一次,和别的类型参数也没有关系,和函数返回值也没有关系,所以T在这里就显得有点多余。
再看另一个例子,
class Collections {
public static <T> void copy(List<T> dest, List<? extends T> src) {
...
}
这里的类型参数T就是必须的了,它表明了源列表和目标列表元素的依赖关系,如果没有一个具体的类型参数,这种依赖没法表述。但是如果你写成下面这个样子,就有点画蛇添足了。
class Collections {
public static <T, S extends T> void copy(List<T> dest, List<S> src) {
...
}
可以说,原则就是尽可能的使用通配符,因为它更加精炼,当通配符达不到目的的时候再使用具体的类型参数。