JAVA泛型杂谈--擦除,协变,逆变,通配符等

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lovejj1994/article/details/78335461

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

四. 总结

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

展开阅读全文

没有更多推荐了,返回首页