在《JAVA核心思想》这本书里,关于泛型的章节意外的很多,小小的泛型里其实有很多可以学习的内容,我总结下最近看书的成果。
一. 泛型的好处和应用
最基础的用到泛型的地方无非是在容器里 使用泛型用以保证容器内数据的类型统一,所以我们先总结下泛型使用的好处:
- 可以统一集合类型容器的存储类型,防止在运行期出现类型装换异常,增加编译时类型的检查
- 解决重复代码的编写,能够复用算法。可以起到重载的作用
第二个作用很好理解,泛型 的 ‘泛’ 即代表着 ‘泛化’,不仅仅是保证容器的安全性,更重要的是减少类型的限制,让我们编写更加通用的代码。我们举个例子:
在javaweb中,我们经常向前台返回JSON对象信息,不同的业务可以会组装不同的bean,为了节省提高代码的复用性,我们可以这么写一个类:
public class ReturnObject<A, B> {
public final A a;
public final B b;
public ReturnObject(A a, B b) {
this.a = a;
this.b = b;
}
public <A> A t(A a) {
return a;
}
@Override
public String toString() {
return "ReturnObject{" +
"a=" + a +
", b=" + b +
'}';
}
}
这个类没有具体的类型,意思也很简单,就是不限定变量的类型,根据你业务的不同,你可以传不同的类型进去(通过构造器),更方便的是,java泛型支持继承,你可以随意拓展你的业务字段,也就不再需要为了一种业务专门创建一个bean类了。
// 业务字段拓展
public class ReturnObjectExtender<A, B, C> extends ReturnObject<A, B> {
public final C c;
public ReturnObjectExtender(A a, B b, C c) {
super(a, b);
this.c = c;
}
@Override
public String toString() {
return "ReturnObjectExtender{" +
"c=" + c +
", a=" + a +
", b=" + b +
'}';
}
}
二. 重要!泛型的擦除
JAVA的泛型都是通过擦除来实现的,这句话的意思是 当你的程序真正跑起来的时候,任何具体的类型其实都已经被擦除了,所以在下面的例子中,输出的结果是true,aClass和aClass1都是一样的class生成的对象。
Class aClass = new ArrayList<Integer>().getClass();
Class aClass1 = new ArrayList<String>().getClass();
System.out.println(aClass == aClass1);
擦除的负面效应直接体现在如果你写下面这段代码,T类型并不能认出你传给它的是String类型,T直接会被Object替代,
public class WildCardTest<T> {
public void f(T t) {
//这一段编译报错
t.isEmpty();
}
public static void main(String[] args) {
WildCardTest<String> stringWildCardTest = new WildCardTest<>();
stringWildCardTest.f("");
}
}
要让String调用它的isEmpty()方法,需要给泛型一个边界,代码只需要重新改一下,T extends String表明T可以是String类或是String的子类,如果传入的没有问题,那就可以调用isEmpty()方法。
public class WildCardTest<T extends String> {
public void f(T t) {
t.isEmpty();
}
public static void main(String[] args) {
WildCardTest<String> stringWildCardTest = new WildCardTest<>();
stringWildCardTest.f("");
}
}
擦除是历史遗留问题
java的泛型不是从jdk1.0就出现的,为了跟以往没有泛型代码的源代码兼容,例如List被擦除为List,而普通的类型变量在未指定边界的时候被擦除为Object,从而实现泛型的功能并且向后兼容。
三. 泛型的通配符(逆变与协变)
这是两个赋值,一个是数组,ArrayList因为是Collection的子类,所以数组向上转型是可以的;另一个是带泛型的ArrayList,带ArrayList的泛型并不能赋值给带Collection的泛型。
Collection[] collections = new ArrayList[]{};
//泛型会报错
ArrayList<Collection> collections1 = new ArrayList<ArrayList>();
逆变,协变与不变
为什么会导致这样的差异呢?这里又引出了一个概念– 逆变,协变与不变。逆变与协变用来描述类型转换后的继承关系。
f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
f(⋅)是协变(covariant)的,当A≤B时有成立f(A)≤f(B)成立;
f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
根据上面两个赋值语句做解释,A 是ArrayList ,B是Collection,所以B > A,这个没有问题,然后 A[] 数组当作f(A),B[] 数组当作f(B),并且A[]可以赋值给B[]数组,说明 f(B) >=f(A),符合协变原则,所以数组是协变的。
而泛型也套用这个规则,发现 泛型其实是不变的。
Java中泛型是不变的,可有时需要实现逆变与协变,怎么办呢?这时,通配符?派上了用场:
// <? extends>实现了泛型的协变,它意味着某种Collection或Collection子类的类型,规定了该容器持有类型的上限,它是具体的类型,只是这个类型是什么没有人关心 比如:
List<? extends Collection> list = new ArrayList<ArrayList>();
// <? super>实现了泛型的逆变,它意味着某种ArrayList或ArrayList父类的类型,规定了该容器持有类型的下限,比如:
List<? super ArrayList> list = new ArrayList<Collection>();
逆变,协变的应用
在协变中,还是先以数组做举例,协变中对持有对象的存入有严格限制:
Collection[] collections = new ArrayList[2];
collections[0] = new ArrayList();
//这一步编译没问题。但是运行时发现数组里已经定好了只能存ArrayList类型,所以会抛ArrayStoreException
collections[1] = new LinkedList();
所以在泛型的协变中,例如ArrayList的add方法是不能调用了,在编译期间直接报错。
这是ArrayList 的add,get方法定义,当使用协变时,E e 会被直接替换成 ? extends E
public boolean add(E e);
public E get(int index);
具体事例:
ArrayList<? extends Set> sets = new HashSet<>();
//因为 ? extends Set 编译器不知道sets引用指向什么对象,有可能是 HashSet,可能是TreeSet,这种不确定性导致sets不能使用add方法。
//sets.add(new HashSet());
//能插入null值
sets.add(null);
//因为这个泛型参数的上限是Set,为了安全性,所以只返回set类型
Set set = sets.get(0);
在逆变中,因为规定了泛型的下界,所以get set 方法的使用限制又有所不同:
ArrayList<? super List> list = new ArrayList<Collection>();
//对于 add方法,只能放List或List的子类,因为list容器泛型参数都是List的父类 不会出现问题
list.add(new ArrayList());
//不能add HashSet
//list.add(new HashSet());
//不能add Object
//list.add(new Object());
//因为这个泛型参数的下限是List 所以无法确定这是个什么类型,只能返回Object
Object object = list.get(0);
无界通配符
除了extends和super,还有一种 List<\?>这种通配符,代表着任何事物,但它与List不同的是:
- List<\Object> = List = ‘持有任何Object类型的原生List’
- List<\?>表示–’具有某种特定类型的非原生List,只是我们不知道那种类型是什么’。
具体用代码看出区别:
ArrayList<Collection> collections2 = new ArrayList<>();
ArrayList<?> objects = new ArrayList<>();
//?代表持有某种特定类型 ,所以也可以是Collection,这种赋值时合法的
objects = collections2;
//?代表持有某种特定类型,d但是不确定具体哪种,所以只能返回Object
Object o = objects.get(0);
//?代表持有某种特定类型,但是什么类型编译器并不知道,所以为了安全起见,不会让你用add方法
//objects.add(new Object());
//可以add null
objects.add(null);
总结来说:
- 要从泛型类取数据时,用extends;
- 要往泛型类写数据时,用super;
- 既要取又要写,就不用通配符(即extends与super都不用)。
四. 总结
这篇文章总结的是书里比较重要的知识点,跳过了简单的应用,写了那么多,感觉总结还是很有必要的,你第一遍看书也许概念会有些懵懂,但是再记录总结下,你会解开第一遍看书时有点么棱两可的知识点。