java随笔系列-泛型2

/ java随笔系列 / 没有评论 / 81浏览

前言

Java 类型系统中最棘手的部分之一是通配符类型,利用有限制通配符来提升 API 的灵活性。

协变和逆协变

一门程序设计语言的类型系统中,一个类型规则或者类型构造器是:

上面的是维基百科上的定义,可能有些不直观,总结来说逆变与协变用来描述类型转换(type transformation)后的继承关系。其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)

变量的协变

在具体说明协变和逆变之前,我们说一下Liskov替换原则(里氏替换原则)

所有引用基类(父类)的地方必须能透明地使用其子类的对象。

Object obj=new Object();
String str=new String("android");
obj=str;//可以,子类变量可以赋值给父类变量
//str=obj;//禁止

上面的例子中,父类object的引用可以指向string类型的变量,但是相反不可以。所以一般来说java中,普通变量(不包含数组和泛型)可以支持协变,但是不支持逆协变。

返回值协变和参数逆变

对输入类型是逆变的而对输出类型是协变的,在java中更为具体的表现分别是子类可以重写父类的方法返回更为具体的类型(重写)和子类覆盖父类方法时接受一个“更宽”的父类型(但并不是重写,而是重载)

泛型协变和逆协变

在java中泛型是不变的,也就是说List 不是List的子类

List<Object> list1=new ArrayList<>();
List<String> list2=new ArrayList<>();
//list1=list2;//禁止赋值

出现上面的原因在于java的安全类型检查机制,我们假设如果上面的能够转换成功。

List<String> strs = new ArrayList<String>();
List<Object> objs = strs; //。Java 禁止这样!
objs.add(1); // 这里我们把一个整数放入一个字符串列表
String s = strs.get(0); // ClassCastException:无法将整数转换为字符串

因此,Java 禁止这样的事情以保证运行时的安全。但是这样会带来一定的影响。例如:在java中Collection 接口中的 addAll()方法。

interface Collection<E> extends Iterable<E>{
    boolean addAll(Collection<? extends E> c);//先不考虑extends
}

如果我们换成boolean addAll(Collection<E> c),那么我们将无法完成下面简单的操作,虽然它是安全的.

public void copy(Collection<Object> dest,Collection<String> src){
    dest.addAll(src)//不被允许的操作,Collection<String>并不是Collection<Object>的子类
}

通配符

为了解决java泛型是不变的问题,提供更大的灵活性,java中引入了通配符的概念以及extends和super关键字

上界限定符

在泛型中使用extends关键字,可以使其泛型发生协变的类型转换,设置上界限定。

通俗点说Collection<String>Collection<? exntends Object>的子类,这样可以带来更大的灵活性,同时保证了java的安全类型检查。

public class Animal{}
public class Dog extends Animal{}
public class Cat extends Animal{}
public static void main(String[] args) {
    List<Dog> dogs = new ArrayList<>();
    dogs.add(new Dog());
    animal(dogs);//可以正常调用
    
    List<Cat> cats=new ArrayList<>();
    cats.add(new Cat());
    animal(cats);//可以正常调用
    
    //甚至
    List<Animal> animals=new ArrayList<>();
    animals.add(new Cat());
    animals.add(new Dog());
    animal(animals);
}

public static void animal(List<? extends Animal> animals) {//可以介绍list泛型为animal子类的集合类(可以是一种或者多种类型)
    Animal animal = animals.get(0);//此处函数并不关心animal的具体类别。
    ....//进行animal的其他操作
    //animals.set(0,new Dog());//禁止,因为不知道这个集合泛型指的具体是什么,如果是cat就会发生类型错误
}

通配符 List<? extends Animal> 表示某种特定类型 ( Animal 或者其子类 ) 的 List,但是并不关心这个实际的类型到底是什么,反正Animal是他的上界。

下界限定符

super关键字事使其泛型发生逆协变,设置下界限定,通俗点说List<? super String>List<Object>的一个超类。

public static void cage(List<? super Animal> animals) {
    animals.add(new Dog());//可以添加animal的子类
    animals.add(new Cat());
    Object o = animals.get(0);//获取的类型只能是object,不能是Animal,因为可能是animal的父类object
}

public static void main(String[] args) {
    List<? super Animal> list = new ArrayList<>();
    cage(list);
    List<Animal> list2 = new ArrayList<>();
    cage(list);
    List<Object> list3 = new ArrayList<>();
    cage(list);
}

方法cage的参数List<? super Animal> animals,代表集合的泛型必须是animal或者其父类,也就是说我们不知道这个集合的具体类型。 我们向这个集合添加dog,cat是安全的,因为这两个类型肯定是animal的子类,但是我们获取的时候只能是object这个超类,因为我不知道这个集合的具体类型。

我们看一下java中Collections的copy方法:

public class Collections { 
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) 
            dest.set(i,src.get(i)); 
    } 
}
List<Animal> animalDest=new ArrayList<>();
List<Animal> animalSrc=new ArrayList<>();
List<Dog> dogDest=new ArrayList<>();
List<Dog> dogSrc=new ArrayList<>();
Collections.copy(animalDest,animalSrc);//src和dest可以是任意相同类型
Collections.copy(animalDest,dogSrc);//src可以是dest的子类

通过使用super和extends,可以让copy方法具有更大的灵活性,避免每个类型实现一个copy方法。 src 是原始数据的 List,因为要从这里面读取数据,所以用了上边界限定通配符:<? extends T>,取出的元素转型为 T及其T的子类。dest 是要写入的目标 List,所以用了下边界限定通配符:<? super T>所以集合的T的泛型指的是T或者T的父类,可以写入的元素类型是 T 或者T的子类。

无边界通配符

无边界通配符,它的使用形式是一个单独的问号:List<?>,也就是没有任何限定。但是由于不知道具体类型,所以这个集合不可以写入任何的数据(包括object)这是不安全的,取出数据只能是object类型。

List<?> list=new ArrayList<>();
//list.add(new Object());//error
Object o = list.get(0);//取出object

//不指明泛型时,也就是默认的泛型object
List list1=new ArrayList();
list1.add(new Object());;//正确,可以写入任何数据

无边通配符的作用主要是用于捕获转换。这个捕获转换,我没发现有啥具体作用(~~丢人...),感觉就是可以把list<?> 传给泛型方法List,这样泛型List就可以以T进行操作。

public static class InstanceHolder<T> {
    private T o;
    public InstanceHolder(T o) {
        this.o = o;
    }
    public void setO(T o) {
        this.o = o;
    }
    public T getInstance() {
        return o;
    }
}
public static void capture(InstanceHolder<?> instanceHolder) {
    fetch(instanceHolder);
}
public static <T> void fetch(InstanceHolder<T> instanceHolder) {
    System.out.println(instanceHolder.getInstance().getClass().getCanonicalName());
}
InstanceHolder instanceHolder = new InstanceHolder<>(new Object());
capture(instanceHolder);
fetch(instanceHolder);

感觉示例并没有完全体现出来,这个地方我会后续补上

pecs- Producer-Extends, Consumer-Super

Joshua Bloch 称那些你只能从中读取的对象为生产者,并称那些你只能写入的对象为消费者。他建议:“为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型”,并提出了以下助记符: PECS 代表生产者-Extends,消费者-Super(Producer-Extends, Consumer-Super)。

我觉的如果你喜欢玩cs的话,那就好记了哈~

pecs概念其实就是对java泛型中的协变和逆协变的总结:

  • 要从泛型类取数据(生产数据),用extends
  • 要往泛型类写数据(消费数据),用super
  • 既要取又要写,就不用通配符(即extends与super都不用)

自限定类型

自限定类型只是一种技巧,它的意义是可以保证类型参数必须与正在被定义的类相同。自限定只能强制作用于继承关系。如果使用自限定,就应该了解这个类所用的类型参数将与使用这个参数的类具有相同的基本类型。

        public class BaseBound<T extends BaseBound<T>>{
            T element;
            BaseBound<T> set(T arg){
                element=arg;
                return this;
            }
            T get(){return element;}
        }

        public class SelfBound extends BaseBound<SelfBound>{}
        public class Out{}
        //public class OutSelfBound extends BaseBound<Out>{}//error

在看另外一个函数,给定一个数组,通过Comparable求出最大值。

/**
* 错误的版本,不能保证数组内存正确的实现Comparable接口
*/
public static <T> T max(T[] array) {
    if (array == null || 0 == array.length) {
        return null;
    }
    T max = array[0];
    for (int i = 1; i < array.length; i++) {
        if (max instanceof Comparable && array[i] instanceof Comparable) {//不能使用array[i] instanceof Comparable<T>,illegal generic type for instanceof
            Comparable comparable = (Comparable) max;
            Comparable next = (Comparable) array[i];
            if (comparable.compareTo(next) < 0) {//警告
                max = array[i];
            }
        }
    }
    return max;
}
/**
* 正确方式,可以保证数组内容正确实现了Comparable接口
*/
public static <T extends Comparable<T>> T maxExtends(T[] array) {
    if (array == null || 0 == array.length) {
        return null;
    }
    T max = array[0];
    for (int i = 1; i < array.length; i++) {
        if (max.compareTo(array[i]) < 0) {
            max = array[i];
        }
    }
    return max;
}

其他

  • extends可以指定多个类和接口,使用&连接
  • extends可以用于class中泛型声明,super不可以

结尾

本文主要介绍了java泛型协变,逆协变,通配符等内容,下一篇会主要介绍java中怎样获取泛型的运行类型,以及相关的应用比如json的序列化。