Java泛型中的通配符

作者: 一字马胡
转载标志 【2017-11-03】

更新日志

日期 更新内容 备注
2017-11-03 添加转载标志 持续更新

1、上界通配符

首先,需要知道的是,Java语言中的数组是支付协变的,什么意思呢?看下面的代码:

    static class A extends Base{
        void f() {
            System.out.println("A.f");
        }
    }

    static class B extends A {
        void f() {
            System.out.println("B.f");
        }
    }

    static class C extends B {
        void f() {
            System.out.println("C.f");
        }
    }

    static {

        A[] arrayA = new A[3];

        arrayA[0] = new A();
        arrayA[1] = new B();
        arrayA[2] = new C();

        for (A item : arrayA) {
            item.f();
        }
        
    } 

//output
A.f
B.f
C.f

我们明明让数组的类型为A,但是向其中加入B、C也是可以行得通的,为什么呢?我们发现B继承了A,属于A的子类,C继承了B,属于B的子类,Java中的继承是可以传递的,所以C依然属于A的子类,所以B和C都是A的子类,另外一点,在Java中,类型向上转换是非常自然的,不需要强制转换会自动进行,也就是说,B和C的实例都可以自动转换为类型A的实例。好了,有了这样的背景知识,我们可以来看一下上界通配了,在java中,可以使用<? extends Type> 来界定一个上界,<? extends Type>的意思是所有属于Type的子类,Type是上界,不能突破天界啊,我们具体化一下,<? extends A>的意思就是,所有A的子类都可以匹配这个通配符。所有我们的B、C的实例以及他们的子类的实例都可以匹配,但是Base就不可以,因为Base是A的父类,而我们的上界是A啊,所以当然不能是Base了。很自然的,我们有下面的代码:

        A a = new B();
        A b = new C();
        C c = new C();

       List<? extends A> list = new ArrayList<A>();
    
      list.add(a);
      list.add(b);
      list.add(c);

我们觉得很自然这样做是无可厚非的,对吧?但是编译器很显然不允许我们这样做,为什么?我们的list的类型使用了上界通配符啊,而且匹配的是所有A的子类,而我们add的都是A的子类啊,为什么不可以呢?我们再来看一下<? extends A>,我们的list可以持有任何A的子类对象,也就是A、B、C的实例都是可以的,那我们是不是可以把<? extend B>认为是<? extends A>的子类呢?<? entends C>呢?我们暂且认为是可以这样吧,那看下面的这个方法:

    void joke(List<? extends A> list) {
        A a = new B();
        A b = new C();
        C c = new C();
        
        list.add(a);
        list.add(b);
        list.add(c);
    }

当然上面的代码是无法通过编译的,我们分析一下为什么,记住<? extends A>是设定上界,所以,joke方法的参数是开放的,我们可以传进去一个<? extend A>的list,也可以是一个<? extends B>的list,还可以是一个<? extends C>的list。因为下面的代码是可以通过编译的:

    private static void jokeIn(List<?extends A> list) {
        //
    }
    
    static {
        List<? extends A> list = new ArrayList<>();
        List<? extends B> list1 = new ArrayList<>();
        List<? extends C> list2 = new ArrayList<>();
        
        jokeIn(list);
        jokeIn(list1);
        jokeIn(list2);
    }

好吧,问题来了,当我们传到joke方法中的参数是List<? extends A>的时候,方法内部的add都是可以接受的,但是当我们传进去的参数是List<? extends B>的时候,list.add(a)明显是无法成功的,因为我们的list将可以允许持有B的子类,但是A不在这个范围里面,所以是不合法的,当传进去的是List<? extends C>的时候呢?连list.add(B)也不允许了。所以这就是问题所在,所以不允许这样的代码通过编译是明智的,因为我们不能总是保证调用joke方法的用户会严格传进来一个List<? extends A>的参数。
那怎么使用上界呢?换句话说,如何来产生一个List<? extends A>的list呢?还记得一开始我们说的数组协变吗?下面的代码使用了java语言数组具有协变能力来产生一个具有上界的list:

     List<? extends A> list = Arrays.asList(a, b);

Arrays.asList(T ... data)使用了ArrayList的一个构造函数:

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }

可以看到使用了数组的协变,使得我们可以在Arrays.asList里面传递进去所以A的子类对象。

2、下界通配符

上界定义了可以达到了最高点,超出就是违法的;而下界则是说明了底线,你只能比底线更高级,低于底线就是违法的。在java里面,可以使用<? super Type>来表达下界的意义,具体一点,<? super A>表达的和<? extends A>是两个相反的方向,前者是说所有基于A的基类,后者是说所有基于A的子类,我们再来看一下下面这个方法:

    void joke(List<? super A> list) {
        A a = new B();
        A b = new C();
        C c = new C();
        
        list.add(a);
        list.add(b);
        list.add(c);
    }

此时的joke方法的参数是List<? super A>,此时List<? super B>和List<? super C>都变成了List<? super A>的父类了,因为实际上List<? super B>和List<? super C>表达的能力比List<? super A>更强,也就是List<? super C>包含了List<? super B>和List<? super A>,而List<? super B>则包含了List<? super A>,好了,说明了这些之后,我们再来看一下对joke方法的调用会出现哪些情况:

    static {
        List<? super A> list = new ArrayList<>();
        List<? super B> list1 = new ArrayList<>();
        List<? super C> list2 = new ArrayList<>();
        
        jokeIn(list);
        jokeIn(list1); // error
        jokeIn(list2); //error
    }

好吧,问题出现了,我们可以将List<? super A> 的参数传递给joke,因为这正是我们需要的,而我们也知道List<? super A>的表达能力在List<? super B>和<? super C>中是最低的,所以,当我们将一个表达能力强于List<? super A>的参数传递给joke之后,编译器报错了。当然,这仅仅是为了说明所谓下界的界定。

有了下界,我们可以使用下面的代码来为我们工作了:

       List<? super A> lists = new ArrayList<>();
        lists.add(a);
        lists.add(b);
        lists.add(c);

解释一下,lists里面的元素类型是这样一种类型,这种类型是A的基类,我们只是界定了下界,只要高于这个下界,就可以被lists接收,而b、c的基类都是A,可以被lists接收,所以上面的代码是可以工作的。

3、无界通配符

有了上界和下界,还有无界,需要说明的一点是,不能同时使用上界和下界,因为有无界啊(开玩笑的)!!
我们在java中使用<?>来表达无界,对于<?>,目前来讲锁表达的意思是:

我是想要java的范型来编写这段代码,我在这里并不是想使用原生类
型,但是在当前这种情况下,泛型参数可以持有任何类型。

                     ----来自《java编程思想》15.10.3 无界通配符(686页)

使用无界通配符的一种场景是:如果向一个使用<?>的方法传递了一个原生类型,那么对编译器来说可能会推断出实际的参数类型,使得这个方法可以回转并且调用另外一个使用这个确切类型的方法。这叫做“类型捕获”,看下面的代码:

    static class Holder<T> {
        private T data;

        public Holder() {

        }

        public Holder(T data) {
            this.data =data;
        }

        public T getData() {
            return data;
        }

        public void setData(T data) {
            this.data = data;
        }
    }


    static <T> void actual(Holder<T> holder) {
        System.out.println(holder.getData());
    }

    static void func(Holder<?> holder) {
        actual(holder);
    }

    static {

        Holder<?> holder = new Holder<>("hujian");

        func(holder);
        
    }

可以看到,actual的参数是具体的T,而func的参数是无界的<?>,这里发生了一件参数类型捕获的事情,在调用func的时候,类型被捕获,而可以在actual方法中使用我们从func中传递进来的无界参数。
可以使用无界通配符来接收多个类型的对象,然后根据不同的类型来交付给不同的方法来处理,可以回忆一下操作系统的中断处理程序的处理方法,通过安装一些中断类型和与之对应的handler,然后通过控制程序来将中断处理信号分发到不同的handler中处理,其实思想是一样的,可以看一下下面的代码来理解这个模型:

    static <T> void actual(Holder<T> holder) {
        T data = holder.getData();
        if (data instanceof String) {
            actual((String) data);
        } else if (data instanceof Integer) {
            actual((Integer) data);
        } else if (data instanceof Double) {
            actual((Double) data);
        }
    }

    static void actual(String holder) {
        System.out.print("string:" + holder);
    }

    static void actual(Integer holder) {
        System.out.println("Integer:" + holder);
    }

    static void actual(Double holder) {
        System.out.println("double:" + holder);
    }

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

推荐阅读更多精彩内容