带你撸出一手好代码
Java泛型通配符

多态是Java的一项语法特性,多态使得任何使用父类的地方都可以被子类代替, 比如说方法的参数

 

public class Main {
    public static void main(String[] arg)  {
        Integer intVal = 100;
        Float floatVal = 10.5F;
        printNum(intVal);
        printNum(floatVal);
    }
 
    public static void printNum( Number num ) {
        System.out.println(num);
    }
}

 

如上所示,利用多态,printNum方法既可以接受Integer类型参数, 也可以接受Float类型参数, 因为Integer类型和Float类型都继承至Number类型,因此,printNum函数的签名虽然指定要接受一个Number类型参数,其实也可以使用Integer和Float类型代替。 用继承的术语来说 Integer和Float与Number的关系是IS-A

 

但在泛型中,这种关系不成立。

 

public class Main {
    public static void main(String[] arg)  {
        List<Integer> intList = Arrays.asList(1,2,3,4,5);
        List<Float> floatList = Arrays.asList(3.1F,2.7F,8.3F,11F,15.6F);
        printNumList(intList);
        printNumList(floatList);
    }
 
    public static void printNumList(List<Number> numberList) {
        for(Number item : numberList) {
            System.out.println(item);
        }
    }
}

 

上面的代码编译无法通过, Java编译器会报错

 

Error:(11, 22) java: 不兼容的类型: java.util.List<java.lang.Integer>无法转换为java.util.List<java.lang.Number>

Error:(12, 22) java: 不兼容的类型: java.util.List<java.lang.Float>无法转换为java.util.List<java.lang.Number>

 

虽然 Integer和Float与Number是IS-A的关系, 但List<Integer>和List<Float>与List<Number>却不是IS-A关系,因为List<Integer>和List<Float>并非继承至List<Number>

 

我们可以证明上面代码的语法错误,然而从直觉上来说却希望它成立,既然使用Number的地方可以使用Integer和Float代替,那为什么使用List<Number>的地方不可以使用List<Integer>和List<Float>代替? 事实上有办法做到,只不过并非使用多态来实现。

 

1、上界通配符 <? extends T>


在泛型中实现这种特性的方式叫做通配符。 稍微把上面的错误代码以泛型通配符的方式修改一下便可编译运行。

 

public class Main {
    public static void main(String[] arg) throws InterruptedException {
        List<Integer> intList = Arrays.asList(1,2,3,4,5);
        List<Float> floatList = Arrays.asList(3.1F,2.7F,8.3F,11F,15.6F);
        printNumList(intList);
        printNumList(floatList);
    }
 
    public static void printNumList(List<? extends Number> numberList) {
        for(Number item : numberList) {
            System.out.println(item);
        }
    }
}

 

只需要将printNumList的方法参数类型从 List<Number>改为List<? extends Number>即可。

 

<? Extends T>这种形式的通配符称之为上界通配符。 这里的问号「?」即为泛型持有的类型,它的范围必须是Number类的子类型或者本身,但不可以是Number的超类或者其它不相关的类型。 所以在这里List<Integer>和List<Float>在List<? extends Number>覆盖的范围之内,也就意味着printNumList能接受这两个列表作为参数。与此同时,List<Double>也符合要求,因为Double也继承至Number, 换句话说继承至Number的所有类型在这里都适用。

 

然而,上界通配符有它的缺陷, 凡是类型为<? Extends T>的泛型对象,都无法被设置除Null以外的值。

 

    public static void printNumList(List<? extends Number> numberList) {
        numberList.add(2.1);
        for(Number item : numberList) {
            System.out.println(item);
        }
    }

 

不好意思,这段代码并不能正常编译,错误出在


numberList.add(2.1);


我们无法往类型为List<? extends Number>的列表中添加除Null以外的值,其中包括这里的2.1,这似乎有点令人费解,可编译器这么规定是有原因的。

 

我们知道printNumList方法的参数可以接受List<Integer>、List<Float>、List<Double>等类型的值,可在方法的内部并无法知晓这个numberList参数确切的类型,对于此参数我们只能确定它的类型范围,列表持有的对象是继承至Number类型,但具体是什么就不得而知了。

 

我们假设编译器不做这个限制,有如下代码


 public class Main {
    public static void main(String[] arg) throws InterruptedException {
        List<Integer> intList = new ArrayList();
        intList.add(1);
        intList.add(2);
        intList.add(3);
        intList.add(4);
        intList.add(5);
        addNumToList(intList);
        for (Integer item : intList) {
            System.out.println(item);
        }
    }
 
    public static void addNumToList(List<? extends Number> numberList) {
        Float val = new Float(2.2) ;
        numberList.add(val);
    }
 }


addNumToList方法会向作为参数传递的列表中添加一个Float类型的值, 外部代码调用这个方法,并传递一个Integer列表作为参数,当方法执行完毕,程序开始遍历列表打印列表中的值时, 代码将抛出一个ClassCastException, 因为列表中存在一个Float类型参数, 而Float类型无法被转换成Integer类型,所以程序报错, 泛型所带来类型安全的好处随之消失, 而设计泛型的初衷就是保证类型的安全。 换言之,不加这个限制代码就会变得像不使用泛型一样 


public class Main{
  public static void main(String[] arg) throws InterruptedException {
        List intList = new ArrayList();
        intList.add(1);
        intList.add(2);
        intList.add(3);
        intList.add(4);
        intList.add(5);
        addNumToList(intList);
        for (Object item : intList) {
            Integer intVal = (Integer)item;
            System.out.println(intVal);
        }
    }
 
    public static void addNumToList(List numberList) {
        Float val = new Float(2.2) ;
        numberList.add(val);
    }
 }

 

这段代码会抛出运行时错误

 

Exception in thread "main" java.lang.ClassCastException: java.lang.Float cannot be cast to java.lang.Integer

 

而原因就是没使用泛型。

 

因此, 使用泛型上界通配符这一特性时,就不要想着改变这个泛型对象所持有的值,因为Java从编译器层面就杜绝了这种操作。假如你既有使用泛型上界通配符的需求,又有修改泛型对象的需求,那只能说明你的代码设计存在逻辑问题,至少在Java的世界中是这样的。

 

2、下界通配符 <? super T>


既然有上界通配符,那自然也有下界通配符,下界通配符重用super关键字,语法如下

 

<? super T>

 

具体一点的写法

 

 List<? super String> dest

 

这里问号「?」的作用与上界通配符相同,代表泛型所持有对象的类型。 而super关键字与extends关键字作用相反,表示泛型持有的类型只能是目标类型的超类或者本身

 

public static void distinct(List<String> src, List<? super String> dest) {
        for(String item : src) {
            if(!dest.contains(item)) {
                dest.add(item);
            }
        }
    }

 

以这个方法为例, 第二个参数desc使用了下界通配符, 那么传递给这个参数的列表只能是String列表或者String所继承的类型的列表, 比如List<CharSequence>甚至List<Object>。和上界通配符相似,下界通配符表示的也是一个范围,范围的下界就是super关键字右边的类型。

 

下界通配符也有应用场景, 比如这个distinct方法,对第一个参数的列表中的值去重并塞进第二个参数的列表。 方法的第一个参数没有争议,是一个String列表。 第二个参数是一个下界通配符类型列表,在方法体内部无法知道List持所持有类型的具体类型,而只知道它的类型范围,以String为地板,因此它肯定是作为String的超类的存在, 这也就说明了在这个方法里可以往这个List中插入String类型的值,如果String可以被继承,那么也可以插入继承至String类型的值,我们可以确定这是符合Java语法规则,因为Java的超类总是可以被超类的子类所代替。


 public class Main{
   public static void main(String[] arg) throws InterruptedException {
        List<String> strList = Arrays.asList("1","3","2","3");
        List<String> strListResult = new ArrayList<>();
        distinct(strList, strListResult);
        System.out.println(strListResult);
    }
 
    public static void distinct(List<String> src, List<? super String> dest) {
        for(String item : src) {
            if(!dest.contains(item)) {
                dest.add(item);
            }
        }
    }
 }


在这里我们对类型为List<String>列表去重,并将结果置如另一个List<String>。当然List<String>并非唯一选择,List<CharSequence>、List<Serializable>、List<Object>都可以作为第二个参数的类型,只要是String的被继承者便都符合要求。因此,下界通配符能带来强大的灵活性。

 

然而, 下界通配符也有和上界通配符一样的局限性。因为编译器只知道下界通配符的地板类型,在distinct方法中是String, 而不知道它的具体类型, 所以自然也就无法访问它。就拿 <? super String>通配符来说,我们无法确定问号「?」所代表的类型到底是String、CharSequence或者其它什么类型,因为String的被继承者太多了。 硬要对它进行访问,也只能把他它当成一个Object类型,在Java的世界里, 所有的类型都是Object的之类, 把它当成Object在语法上总是正确的。

 

我们在上面提到过,上界通配符泛型类型能读不能写,那么下界通配符泛型类型则是相反的能写不能读。

 

3、无界通配符 <?>


除了这两种通配符以外,还有一种通配符叫做无界通配符。

 

List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);
        intList.add(3);
        intList.add(4);
        intList.add(5);
 
        List<?> unknownList = intList;

 

上面代码中的unknownList就是无解通配符泛型列表。对于unknownList这个变量,虽然指向一个Integer类型的列表,但是编译器无法确定它的类型,所以对于这个列表,既无法通过列表持有的类型访问它,也无法设置它的值,它似乎就像把脑袋缩进壳里的乌龟,让人毫无办法。

 

Java的设计者把一种没有价值的特性纳入Java规范显然是不可能的,这说明无通配符泛型有存在的意义


 public class Main{
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1,2,3,4,5);
        printList(intList);
 
        List<String> strList = Arrays.asList("a","b","c","d","d");
        printList(strList);
    }
 
    public static void printList(List<?> list) {
        for(Object item : list) {
            System.out.println(item);
        }
    }
 }


printList可以打印任意类型的泛型列表,因为List<?>中的问号「?」可以匹配任意类型,虽然在方法内部,无法知道这个被匹配类型的具体类型。

 

细心的人会质疑,要实现这种效果无界通配符显得多此一举,直接把参数类型设为List也可以达到相同的效果, 比如说这样

 

    public static void printList(List list) {
        for(Object item : list) {
            System.out.println(item);
        }
    }

 

然而,这样做就缺乏类型安全的保护了,比如说下面的代码会抛出运行时ClassCastException


public class Main{
    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);
        intList.add(3);
        intList.add(4);
        intList.add(5);
 
        printList(intList);
 
        Integer sum = 0;
        for(Integer item : intList) {
            sum += item;
        }
        System.out.println(sum);
    }
 
    public static void printList(List list) {
        for(Object item : list) {
            System.out.println(item);
        }
        list.add("a");
    }
 }


因为在printList方法中无类型列表被写入了一个字符串, 而在函数外部,这个列表是一个Integer列表, 代码骗过了Java编译器,却骗不过Java虚拟机,于是代码读取列表转换类型时就报错了。  

 

如果我们把printList方法的参数改成无界通配符类型泛型列表,那么在方法的内部根本无法向列表中插入除NULL以外的任意值, 这种不严谨的运行时错误在编译期就能被杜绝,这便是无界通配符重要作用之一。这种特性是无界泛型通配符的一种隐喻:当代码中使用无界通配符泛型类型,意味着代码的主人希望将此泛型对象的数据隐藏起来并且不被破坏。以List<?>来说, 其实这个类型已经做了自说明,它除了告诉别人它是一个列表以外,其它信息概不透露。


作者:陈大侠
日期:2018-01-30

留言(0条)

我要发表留言

您的大名 选填
电子邮箱 选填

欢迎关注微信公众号 「带你撸出一手好代码」

首页    GitHub 知乎 豆瓣 博客园