1.概念
泛型实现了参数化类型的概念,其最初的目的是希望类或方法能够具备最广泛的表达能力。通俗来说就是为了避免转换出错,限制数据类型。通过解耦类或者方法与所使用的类型之间的约束。
List theList1 = new ArrayList(Arrays.asList(1,2,3,"string"));
for(Object o : theList1){
Integer i = (Integer)o;
}
List theList2 = new ArrayList(Arrays.asList(1,2,3,"string"));
theList1是原生态类型的List,theList2是参数化类型(泛型化)的List。theList1遍历到“string”的时候会出现ClassCaseException异常。theList2在编译时就会报错。
不严格的说,前者逃避了编译时检查,后者明确告诉编译器,它应该和能够持有的对象,在编译时检查。
2.泛型的子类化
泛型有子类型化的规则,List
考虑如下代码:
List a;
List n = a;
n.add(2.5f);
Integer num = a.get(0);
假设List
泛型是通过擦除来实现的。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。
当原始类型一致,类型参数不一致时,其为同一个对象。
如下代码:
List type1 = new ArrayList();
List type2 = new ArrayList();
System.out.println(type1.getClass() == type2.getClass());
结果为true。
创建泛型数组是非法的,即new List
有一种特殊情况,当一个方法接受泛型的可变类型。这时会出现警告,这是由于 每次调用可变参数方法时,就会创建一个数组来存放可变参数。如果这个数组的元素类型不是可具体会的(比如泛型),就会得到一条警告。除了用SupressWarnings(“unchecked”)消除这些警告,别无他法。
public void theMethod(@SuppressWarnings("unchecked") List ...a){
List args = a[0];
}
3.优先考虑泛型
考虑如下代码:
public class Stack {
private Object[] elements;
private List theOne = new ArrayList();
private List theString;
private int [] a = new int[1];
private int size = 0;
public Stack(){
elements = new E(16);
}
public void push(E e)
{
ensureCapacity();
elements[size++] = e;
}
public E pop(){
if(!isEmpty()){
E e = (E)elements[--size];
return e;
}
return null;
}
private boolean isEmpty(){
return size == 0;
}
private void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
会出现一条错误。不能创建不可具体化的类型的数据,如E,是什么类型,不知道。解决这个问题有两种方法。
第一种:创建一个Object的数组,并将它转换成泛型数组类型。
public Stack(){
elements = (E[])new Object[16];
}
但会出现一条警告,说明类型是不安全的。你必须确保未受检的转换不会危及到程序的类型安全性。Elements保存在一个私有的域中,永远不会被返回到客户端,或者传给任何其他方法。这个数组中保存的唯一元素,是传给push方法的那些元素,而push方法只能接受E类型的参数,从而保证了未受检的转换不会有任何危害。
第二种:将elements域的类型从E[]改为Object[]。
private Object[] elements;
public E pop(){
if(!isEmpty()){
@SuppressWarnings("unchecked")
E e = (E)elements[--size];
return e;
}
return null;
}
有一个错误,需要强制类型转化为E,由push方法能保证elements里的元素类型为E,所以强制转换为E时不会出现ClassCaseException。
4.类型通配符
考虑如下代码:
class Box{
private Integer data;
public Box(){
}
public Box(Integer data){
setData(data);
}
public Integer getData() {
return data;
}
public void setData(Integer data) {
this.data = data;
}
}
我们可以很容易得到类型参数为Integer的Box对象,如果类型参数为String、Number呢,是不是重新定义Box类呢。只要使用类型通配符就可重用之前写好的代码。将Integer改为E。如下代码。
class Box{
private E data;
public Box(){
}
public Box(E data){
setData(data);
}
public E getData() {
return data;
}
public void setData(E data) {
this.data = data;
}
}
当我们需要什么类型时,定义实参为所需类型即可。
5.有限通配符
extends E>表示E继承某种类型的一种类型
看如下代码:
Stack numberStack = new Stack();
Iterable integers = null;
numberStack.pushAll(integers);
会报错,因为pushAll的输入参数类型不应该为“E的Iterable接口”,而应该为“E继承的Iterable接口”。所以应该把pushAll的方法参数改为
public void pushAll(Iterable extends E> src){
for(E e : src)
push(e);
}
表示某种E的超类型
看代码:
Stack numberStack = new Stack();
Collection
原因分析:Collection
修改popAll方法
public void popAll(Collection super E> dst){
while(!isEmpty){
dst.add(pop());
}
}
上面的objects里存放的是Object类型,当传入到popAll时,dst接受的实参类型为Collection super Object>,即以Object为超类的集合。所以,dst当然可以add()Number类型的数据啦。
说明:
public static void test(List> list){
Object e = list.get(0); // get OK
list.set(0, e); // set 编译报错
}
set报错的原因是因为此时方法中的类型是不可具体化的(reified),你可以传递一个String,Number,Book,等任何继承自Object的类作为List的参数类型给test方法,而list要求集合中的类型必须是一致的,set的时候没有办法保证set进去的数据类型是否和list中原本的类型一致,比如你传给test方法的是List
结论:使用了 extends E> 这样的通配符,test方法的参数list变成了只能get不能set(除了null) 或者不严谨的说它变成了只读参数了, 有些类似一个生产者,提供数据。
public static void test(List super Number> list){
Number n = list.get(0); // 编译错误
Object o = list.get(0); // OK
list.set(0, new Object()); // 编译错误
list.set(0, new Long(0)); // OK
list.set(0, new Integer(0)); // OK
}
这时get只能get出最宽泛的父类型,即Object。
这时set的时候,必须是Number或Number的子类。
原因和上面的get类似。
结论: 使用了 这种通配符,test方法的参数list的get受到了很大的制约,只能最宽泛的方式来获取list中的数据,相当于get只提供了数据最小级别的访问权限(想想,你可能原本是放进去了一个Book,却只能当作Object来访问)。它更多适合于set的使用场景,像是一个消费者,主要用来消费数据。
PECS表示producer-extends,consumer-super。来总结什么时候用 extends T>和 super T>。在上诉的Stack示例中,pushAll的src参数产生E实例供Stack使用,因此src相应的类型为Iterable extends E>;popAll的dst参数通过Stack消费E实例,因此dst相应的类型为Collection super E>。
如果你想从一个数据类型里获取数据,使用 extends T>
如果你想把对象写入一个数据结构里,使用< ? super T>
如果你既想存,又想取,那就别用通配符。