泛型笔记3-下界通配符

上一节讨论了上界通配符的作用以及它的使用事项.今天将讨论下界通配符.

下界通配符

当我们需要动态地传入类对象及其超类类型的时候,由于擦除性质,编译器并不能确定所传入的对象是否是某一个对象的超类,而将它们都视作Object对象.当我们需要一个能使编译器识别这种关系的一种通配符,所以我们就有了下界通配符这一概念.
它的格式为:<? super 类名>.
它告诉编译器,你所要传入的参数类型只能是这个类及其超类类型.我们来看下面的一个关系图.
在这里插入图片描述
如果我们以水果为基类(作为super后面的类名),那么水果及其父类类型就是应当传入的对象类型.注意到它的水果的所有子类也应视作水果类型.那么在这里,仅有蔬菜及其子类类型不应当传入.
在这里插入图片描述

现在假设我们有一个盘子,对应一个容器类,如果这个盘子只能装水果,这其实对应到上界通配符.但我们不止想要它装水果,并且装植物的说法也是不对的,那么我们只能这么说,我们希望这个盘子不止能装水果,还要能装包含水果的任何门类.似乎这在现实中很诡异.因此我觉得下界通配符的真正意义不在于我需要这个盘子装什么,而是这个盘子不要装什么,所以自然一点的说法就是:我不希望这个盘子装蔬菜.
下界通配符给我们提供了一个手段,让我们能够排除一个基类的所有兄弟类型及其祖先的所有兄弟类型.接下来我们将通过一个例子展示这一点.
定义一个水果类,继承植物类.它具有如上图所示的若干子类,蔬菜类的略去不写.为了考虑它能否被传入一个容器类,这里可以不用定义它的成员变量和方法,虽然我这里确实写了,但其实大家完全可以忽略,定义一个空类型空构造器即可.如果说,想看一下容器类对象能否通过元素调用其成员方法,那么可以声明一个成员方法.

package 泛型演示;


//声明一个水果类,以及它的一些子类
public class Fruit extends Plants{
    private String color;

    private double sweetness;

    private boolean isSmooth;

    public Fruit(String color, double sweetness, boolean isSmooth) {
        this.color = color;
        this.sweetness = sweetness;
        this.isSmooth = isSmooth;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public double getSweetness() {
        return sweetness;
    }

    public void setSweetness(double sweetness) {
        this.sweetness = sweetness;
    }

    public boolean isSmooth() {
        return isSmooth;
    }

    public void setSmooth(boolean smooth) {
        isSmooth = smooth;
    }
}


class Strawberry extends Fruit{
    public Strawberry(String color, double sweetness, boolean isSmooth) {
        super(color, sweetness, isSmooth);
    }
}

class Banana extends Fruit{
    public Banana(String color, double sweetness, boolean isSmooth) {
        super(color, sweetness, isSmooth);
    }
}

class NorthBanana extends Banana{
    public NorthBanana(String color, double sweetness, boolean isSmooth) {
        super(color, sweetness, isSmooth);
    }
}

class SouthBanana extends Banana{
    public SouthBanana(String color, double sweetness, boolean isSmooth){super(color, sweetness, isSmooth);}
}

class XinjiangBanana extends NorthBanana{
    public XinjiangBanana(String color, double sweetness, boolean isSmooth) {
        super(color, sweetness, isSmooth);
    }
}

class HainanBanana extends SouthBanana{
    public HainanBanana(String color, double sweetness, boolean isSmooth) {
        super(color, sweetness, isSmooth);
    }
}

在主方法中,如果我们以北方香蕉为基类,那么声明一个容器类对象,采用下界通配符修饰泛型,然后我们向里面加入一些对象,观察结果.
在这里插入图片描述
从图片中我们可以看到,除了北方香蕉和它的子类,添加其他类型的水果均会编译报错.这似乎很奇怪,难道它违反了下界通配符的原则,即它不能装入基类类型的父类型?事实上,这还是擦除的"锅".
sun公司的开发人员在java5开始引入泛型以后,希望今后的第三方库都将采用泛型来声明,构造,实现.但对于java5以前的类库,则不可避免地将面临重写的困境,但这需要一定的时间.不可能为了重写而让用户停止使用这些类库.开发人员采取了这样一个折中的办法,使得泛型能够跨类使用.直白地说,就是当过去的类和现在的泛型类库互相使用,过去的类将无法识别泛型,对它们来说仍然是没有规定类型约束的一些类,虽然这极大限制了泛型的能力,但保证了可维护性,称这样一种特性为泛型的擦除性质.
因为这个特性,编译器经常会陷入迷惑,当它不能明确你要传入一个什么类型的参数,为了安全,它不会允许你随意插入它.在这里,编译器通过下界通配符,知道了要允许基类及其父类传入容器,但不知道传入容器的对象的具体类型.这样,就无法为之分配内存,自然就不能执行代码.为什么允许插入子类型,我们说过,子类型也是父类型,即香蕉是一种水果是对的,水果却不一定是香蕉,当我们插入的是基类的子类型,编译器能识别出它是子类型,虽然由于擦除还是无法确切知道是哪种子类型,但它肯定都是基类类型,那么分配一个基类对象的内存空间就可以了.子类会完整继承父类的所有堆栈信息,所以不用担心会产生什么多余的变量或地址.
用下界通配符修饰的容器对象不可以直接调用get方法,这恰与上界通配符相反.除非你用Object类型的引用指向它,这也正是编译器的看法.但是这种做法没有意义.因为你还是无法知道它是一个什么类型对象,它的一切方法,包括继承过来的方法还是特有的方法都无法调用,除了Object对象的几个方法.它已经完全失去了一切身份信息.造成这种情况的原因是,执行get()方法时,编译器只知道获得的元素是基类类型或是它的超类类型,但并不知道应是哪种具体类型,不同于上界通配符的做法,它不能视之为基类类型而进行向下的类型转换,而进行向上的类型转换又无法确定这个类型是否真的是你获得的类型的超类类型,除非你一直向上转换成Object类型.这也就是为何只有用Object类型引用才能指向get()方法获得的对象的原因.
基于同样的考虑,如果不能获得数据,那么这个数据的存放也将失去意义.所以必须想个办法,使得我们使用了下界通配符存入的数据还能对我们可见.我们采用反射原理.调用Object的getClass()方法,就可以获得对象的具体类型.然后输出这个类型(这里我不清楚如何直接利用getClass的返回结果,所以只能先输出看看),了解到这个结果后,利用强制类型转换,就可以调用对象的特有方法或继承来的方法了.
请仔细观察下面的代码:

package 泛型演示;

import java.util.ArrayList;
import java.util.List;

public class GenericTest2 {
    public static void main(String[] args) {
        //2 采用下界通配符?super, 表示list中所有的元素都是NorthBanana类或其父类
        List<? super NorthBanana> list = new ArrayList<>();

        //2.1 下界通配符正确用法:可以使用add方法,但要符合泛型规定
        list.add(new XinjiangBanana("green", 0.3, true));

        list.add(new NorthBanana("green", 0.3, true));

        //2.2 下界通配符错误用法:添加XinjiangBanana的父类,无法通过编译
//        list.add(new Banana("green", 0.3, true));

        //2.3 因为不能保证返回的结果是哪一个父类,所以只能向上转型到最高类Object.
//        Fruit f1 = list.get(0);
//
//        NorthBanana nb1 = list.get(0);

        Object o1 = list.get(0);

        Object o2 = list.get(1);

//        o1.getColor();//直接调用肯定不行,因为多态不允许父类对象调用子类特有的方法

        System.out.println(list.get(0).getClass());

        System.out.println(o1.equals(list.get(0)));

        System.out.println(list.get(1).getClass());

        Class cl = list.get(0).getClass();

        System.out.println(o2.equals(list.get(0)));

        XinjiangBanana xb = (XinjiangBanana)o1;
        System.out.println(xb.getColor());
    }
}

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值