泛型概述-通配符

泛型概述-基本概念当中,我们介绍了有关类型参数限定的概念,使用 extends 关键字,给类型参数加以限定,例如:<T extends Fruit>,它表示 Fruit 或者 Fruit 的子类型。

public class Plate<T extends Fruit> {
    //......省略部分代码
    public void addFruits(Plate<T> plate) {
        for (T fruit : plate.getFruitList()) {
            fruitList.add(fruit);
        }
    }
}

Plate 类中,我们添加了一个方法 addFruits(Plate<T> plate),此方法的作用是将参数 plate,添加到当前的 Plate 对象当中。

List<Apple> appleList = new ArrayList<>();
Apple apple1 = new Apple("apple1");
Apple apple2 = new Apple("apple2");
appleList.add(apple1);
appleList.add(apple2);

Plate<Apple> applePlate = new Plate<>(appleList);
Plate<Fruit> fruitPlate = new Plate<>();

fruitPlate.addFruits(applePlate);

我们构造了一个苹果盘子 applePlate,并将它作为参数传递给 fruitPlateaddFruits() 方法,从实际生活的角度中来看,这样的需求是没有任何问题的,但是这么写的话,编译器会报错。因为 Plate<Fruit> 中的 addFruits(Plate<Fruit> plate) 的方法参数接收的是一个 Plate<Fruit> 类型的对象,而我们传递的确是一个 Plate<Apple> 类型的对象。

这个时候我们就需要对类型参数加以限定,来对 addFruits 改造一下:

public <E extends T> void addFruits(Plate<E> plate) {
    for (T fruit : plate.getFruitList()) {
        fruitList.add(fruit);
    }
}

我们对 addFruits 方法能够接收的参数类型进行限定,它能够接受的类型必须是 T 或者是 T 的子类型,这个时候我们上面的代码就可以正常运行了。

通配符

我们可以用一种更简单的,带子类型限定的通配符类型来替换上面的泛型方法。也就是将
public <E extends T> void addFruits(Plate<E> plate) 替换成 public void addFruits(Plate<? extends T> plate)

子类型限定

<? extends T>,称为子类型限定的通配符类型,它表示 T 以及 T 的任意子类型。采用通配符形式的写法,无疑看上去更简单明了。

这里需要注意的是,通配符是用来实例化定义好的类型参数的。如果一个类或者一个方法并不是泛型类或者泛型方法,那我们是没有办法使用通配符的。

public class GenericType<?> {
    ? type;
    
    public ? getType() {
        return type;
    }
    public void setType(? type) {
        this.type=type;
    }
}

GenericType 这种写法是不支持的,可以看到,没有办法使用通配符类型来定义类,声明属性,也没有办法作为方法的返回类型。

子类型限定的通配符也有它的局限性

List<Apple> appleList = new ArrayList<>();
Apple apple1 = new Apple("apple1");
Apple apple2 = new Apple("apple2");
appleList.add(apple1);
appleList.add(apple2);
        
List<? extends Fruit> fruitList = appleList;
    for (Fruit fruit : fruitList) {
        System.out.println(fruit.getName());
}

使用了子类型限定通配符 <? extends Fruit> ,它表示 Fruit 以及 Fruit 的任意子类型。所以我们可以将 List<Apple> 类型的对象 appleList 赋值给它。还记得在泛型概述-基本概念当中有讲过,泛型是不支持协变的,但是使用了这种子类型限定通配符类型之后,它就符合了协变的规则。

List<Apple> appleList = new ArrayList<>();
Apple apple1 = new Apple("apple1");
Apple apple2 = new Apple("apple2");
appleList.add(apple1);
appleList.add(apple2);
        
List<? extends Fruit> fruitList = appleList;
fruitList.add(new Fruit("fruit"));//compile error
fruitList.add(new Apple("apple"));//compile error
fruitList.add(new Banana("banana"));//compile error

我们可以看一下 ArrayList 类中的 add()get() 两个方法

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

我们声明了一个 List<? extends Fruit> 类型的对象 fruitList,那么 add 方法get 方法中的类型参数就变成了 ?extends Fruit 类型

public boolean add(? extends Fruit e) {
    // 可以认为变成了这种形式,实际上没有办法这么编写
}

public ? extends Fruit get(int index) {
    // 可以认为变成了这种形式,实际上没有办法这么编写
}

当我们打印水果名称的时候,也就是从 List<? extends Fruit> 类型的 fruitList 对象当中读取数据的时候是没有问题的,get() 方法返回的是 ? extends Fruit 类型,不论它是什么类型,一定可以向上转型成 Fruit 类型;但是当我们需要调用 add() 方法的时候,编译器就会提示错误,这是因为 add() 方法的参数是 <? extends Fruit> 类型,它代表的是 Fruit 以及 Fruit 的任意子类型,并不能够知道具体是什么类型,所以就不能够调用 add() 方法。add(null) 除外🤢

假设我们允许调用 add() 方法的话,就会出现问题

List<? extends Fruit> fruitList = appleList;
fruitList.add(new Fruit("fruit"));//假设没有问题
fruitList.add(new Apple("apple"));//假设没有问题
fruitList.add(new Banana("banana"));//假设没有问题

这就相当于是向 List<Apple> 类型的 appleList 对象中,添加了 FruitAppleBanana 三种类型的对象,先不说它违背了泛型的类型安全的原则,这等于是埋下了一颗定时炸弹,在我们从 appleList 对象当中取数据的时候,就有可能发生类型转换异常。

fruitList.add(new Apple("apple"));

细心的朋友可能发现了,即使是向 fruitList 当中添加 Apple 类型的对象也是不可以的,上面说了 add 方法 的参数类型是 ? extends Fruit 对于编译器来说它会将 ? extends Fruit 识别为 CAP#1 extends Fruit 类型,它没有办法匹配到 CAP#1的具体类型是什么,所以没有办法进行赋值。

无限定通配符

<?> 称为无限定通配符,当一些操作与具体的类型无关的时候,或者说我们不需要知道类型信息的时候,就可以使用无限定的通配符类型,来实例化我们定义的类型参数,例如交换数组中的元素,比较元素的大小,获取元素的个数等等。

例如我们 Collections 工具类中提供的 swap 方法,用于交换列表中指定位置的元素,这个时候不需要知道具体的类型,可以使用通配符类型。

public static void swap(List<?> list, int i, int j) {
    // instead of using a raw type here, it's possible to capture
    // the wildcard but it will require a call to a supplementary
    // private method
    final List l = list;
    l.set(i, l.set(j, l.get(i)));
}

除了使用原生类型之外,我们还可以通过一个辅助的私有方法来匹配通配符的类型

public static void swap(List<?> list, int i, int j) {
        swapHelp(list, i, j);
}

private static <T> void swapHelp(List<T> list, int i, int j) {
    T temp = list.get(i);
    list.set(i, list.get(j));
    list.set(j, temp);
}

对于编译器来说它会将 ? 类型识别为 CAP#1 类型,在内部会调用 swapHelp 方法,编译器会将推断出 T 的类型就是 CAP#1,之后就像是普通的容器类一样进行读写操作。

上面出现的关于通配符的例子,都可以使用泛型方法替代:

public static void swap(List<?> list, int i, int j)

它的泛型方法形式是

public static <T> void swap(List<T> list, int i, int j)

看到这里你可能有些疑惑,因为 swap 方法的泛型方法形式和 swapHelp 方法的形式是一模一样的,你可能会觉得这么写有一些多此一举,采用通配符的形式简洁明了,减少了参数类型的个数,所以应该尽可能的采用通配符类型。

并不是所有的泛型方法都可以改写成通配符的形式,在这里我直接引用老马说编程中的两个例子来说明:

参数类型间具有依赖关系

public static <D,S extends D> void copy(List<D> dest,
        List<S> src){
    for(int i=0; i<src.size(); i++){
        dest.add(src.get(i));
    }
}

我们只能将其简化成具有一个参数类型的形式

public static <D> void copy(List<D> dest,
        List<? extends D> src){
    for(int i=0; i<src.size(); i++){
        dest.add(src.get(i));
    }
}

返回值依赖参数类型

public static <T extends Comparable<T>> T max(List<T> arr){
    T max = arr.get(0);
    for(int i=1; i<arr.size(); i++){
        if(arr.get(i).compareTo(max)>0){
            max = arr.get(i);
        }
    }
    return max;
}

max 方法的返回值是依赖于参数类型 T 的,也没有办法将其改写为通配符形式。

超类型限定通配符

上面我们说到子类限定的通配符无限定的通配符,两种类型的通配符,这两种通配符类型都有一个缺点,就是没有办法进行写操作。

对于 List<?> 来说,我们获取的元素只能赋值给 Object 类型的引用,并且没有办法进行 add 操作,除了 null 值。

对于 List<? extends T> 来说,我们获取的元素只能赋值给它的上限 T 类型,同样没有办法进行 add 操作,除了 null 值。

现在我们有这样一个需求,需要将苹果盘子中的水果,全部放到另一个水果盘子中区,也就是将 Plate<Apple> 中的所有水果,复制一份到 Plate<Fruit> 当中,这个需求没有什么问题,我们在 Plate<T extends Fruit> 类中加入一个方法:

public void copyTo(Plate<T> dest) {
    List<T> destList = dest.getFruitList();
    for (T t : fruitList) {
        destList.add(t);
    }
}

public static void main(String[] args) {
    List<Apple> appleList = new ArrayList<>();
    Apple apple1 = new Apple("apple1");
    Apple apple2 = new Apple("apple2");

    appleList.add(apple1);
    appleList.add(apple2);

    Plate<Apple> applePlate = new Plate<>(appleList);
    Plate<Fruit> fruitPlate = new Plate<>();

    applePlate.copyTo(fruitPlate);

    for (Fruit fruit : fruitPlate.getFruitList()) {
        System.out.println(fruit.getName());
    }
}

编译器会在这一行 applePlate.copyTo(fruitPlate); 提示错误,copyTo 方法需要 Plate<Apple> 类型,但是我们传递的是 Plate<Fruit> 类型,这个时候就需要用到超类型限定的通配符:<? super T>

 public void copyTo(Plate<? super T> dest) {
    List<? super T> destList = dest.getFruitList();
    for (T t : fruitList) {
    destList.add(t);
}

copyTo 方法改成这个样子之后,就可以正常编译运行了,对于 Plate<Apple> 的类型来说,它的 copyTo 方法接收的参数类型是 Plate<? super Apple> 类型,意思是 Apple 或者 Apple 的任意父类型,这个时候我们就可以将 Plate<Fruit> 类型的对象传递给该方法了。

另外超类型限定通配符允许写入,详细看一下 copyTo 方法中的代码,可以发现我们调用 destList.add(t) 方法将 T 类型的对象写入到了 List<? super T> 的列表中,对于 <? super T> 而言,仅仅能够写入 T 或者 T 类型的子类型。

除了可以灵活的写入之外,超类型限定的通配符类型还可以应用于实现 Comparable 接口

public class Fruit implements Comparable<Fruit> {
    private int weight;
    //... 省略部分代码
    @Override
    public int compareTo(Fruit fruit) {
        if (weight == fruit.getWeight()) {
            return 0;
        } else if (weight > fruit.getWeight()) {
            return 1;
        } else {
            return -1;
        }
    }
}

Fruit 实现 Comparable<Fruit> 接口,根据水果的重量 weight 的大小来作为比较规则

现在我们需要取出 Plate<T extends Fruit> 对象中重量最大的水果,代码如下:

public class PlateUtils {
    public static <T extends Comparable<T>> T max(List<T> fruitList) {
        Iterator<T> i = fruitList.iterator();
        T candidate = i.next();

        while (i.hasNext()) {
            T next = i.next();
            if (next.compareTo(candidate) > 0)
                candidate = next;
        }
        return candidate;
    }
}

List<Fruit> fruitList = new ArrayList<>();
Fruit fruit1 = new Fruit("fruit1");
fruit1.setWeight(2);
Fruit fruit2 = new Fruit("fruit2");
fruit2.setWeight(10);
fruitList.add(fruit1);
fruitList.add(fruit2);

Plate<Fruit> fruitPlate = new Plate<>(fruitList);
Fruit maxFruit = PlateUtils.max(fruitPlate.getFruitList());
System.out.println(maxFruit.getName());

//print:fruit2

似乎没有问题,但是如果我们将 List<Apple> 传递进去的话,编译器就会报错

public class Apple extends Fruit {
    public Apple(String name) {
        super(name);
    }
}

这是由于我们的 Apple 虽然继承了 Fruit 但是并没有实现 Comparable<Apple> 接口,对于 max 方法来说,它会根据参数推断出 T 的实际类型为 Apple 类型,Apple 实现的是 Comparable<Fruit> 接口,然而需要的是 Comparable<Apple> 类型,类型不匹配,所以编译器报错。

对于 Apple 来说,它并不需要重新实现 Comparable<Apple> 接口,Fruit 类中实现的 compareTo 规则已经适用于它了,这个时候超类型限定通配符就派上用场了:

public static <T extends Comparable<? super T>> T max(List<T> fruitList) {
    Iterator<T> i = fruitList.iterator();
    T candidate = i.next();

    while (i.hasNext()) {
        T next = i.next();
        if (next.compareTo(candidate) > 0)
        candidate = next;
    }
    return candidate;
}

我们修改了 T 类型的限定,限制它实现的 Comparable 接口类型必须是 T 或者 T 的超类型,对于 Apple 来说,它实现的是 Comparable<Fruit> 类型,符合要求。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容