600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > [转载] JAVA泛型杂谈--擦除 协变 逆变 通配符等

[转载] JAVA泛型杂谈--擦除 协变 逆变 通配符等

时间:2024-06-13 12:15:39

相关推荐

[转载] JAVA泛型杂谈--擦除 协变 逆变 通配符等

参考链接: Java中的协变返回类型

在《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都不用)。

四. 总结

这篇文章总结的是书里比较重要的知识点,跳过了简单的应用,写了那么多,感觉总结还是很有必要的,你第一遍看书也许概念会有些懵懂,但是再记录总结下,你会解开第一遍看书时有点么棱两可的知识点。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。