Java泛型 - 通配符以及原始类型(Raw Type)

字数 2623阅读 2751

参考 & 推荐

推荐阅读:


如何理解List<? extends Number>

根据定义, List<? extends Number> list指的是list引用可以指向声明为List<继承于Number>的实例(不一定要是直接父类, 祖先有Number即可).
比如说以下都是合法的,

List<? extends Number> listOfNumbers = new ArrayList<Number>();
List<? extends Number> listOfIntegers = new ArrayList<Integer>();
List<? extends Number> listOfDoubles = new ArrayList<Double>();

但是我们却不能向上面任何一个容器加入数据.

List<? extends Number> listOfIntegers = new ArrayList<Integer>();
listOfIntegers.add(100);  // 错误, 不允许添加

用呆杰的话来理解就是,
现在给了你一个List<? extends Number>的引用listOfNumber, 他可能是任何继承于Number的List<>. 如果允许往其中加入数据的话很显然是不安全的, 比如说调用list.add(1.4), 但是list实际上指向的是List<Integer>类型, 这样很显然是不允许的.
List<? extends Number>用处是什么? 一个常见的用例就是作为函数参数类型, 因为虽然我们不能对List<? extends Number>的引用进行写操作, 但是我们可以读内容. 因为能传进来的List的泛型类型都是继承于Number类的, 所以总是能将其元素安全地转换为Number类. 比如下面这个例子:

private static double sumList(List<? extends Number> list) {
    return list.stream()
            .mapToDouble(Number::doubleValue) // returns DoubleStream
            .sum();
}

public static void main(String[] args) {
    List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
    List<Double> doubles = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
    List<BigDecimal> bigDecimals = Arrays.asList(
        new BigDecimal("1.0"),
        new BigDecimal("2.0"),
        new BigDecimal("3.0"),
        new BigDecimal("4.0"),
        new BigDecimal("5.0")
    );

    System.out.printf("ints sum is        %s%n", sumList(ints));
    System.out.printf("doubles sum is     %s%n", sumList(doubles));
    System.out.printf("bigdecimals sum is %s%n", sumList(bigDecimals));
}

可能我们会有疑问, 为什么不直接把参数类型定义为List<Number>呢?为什么非得加上? extends看起来如此复杂的声明? 答案是, List<Number>List<Integer>其实没有任何关系, 并没有List<Integer>List<Number>的子类的意思. 其实, 因为泛型擦除的原因, 这两个类最终都是List. 从下面的例子可以看出List<People>List<Man>并没有什么关系. 所以Java中的泛型是不协变的, 即A是B的父类, 但是List<A>和List<B>并没有关系.
更多协变内容: Treant - Java中的逆变与协变
下面代码表明了Java的泛型不是协变的.

    class People{

    }

    class Man extends People{

    }

    class Boy extends Man{

    }

    public void test(){
        List<People> peopleList = new ArrayList<People>();
        List<Man> manList = new ArrayList<Man>();
        peopleList = (List<People>) manList; // 错误, 不能转换类型
    }

原文中给出的例子:

List<String> strings = new ArrayList<>();
String s = "abc";
Object o = s;      // allowed
// strings.add(o); // not allowed

// List<Object> moreObjects = strings; // also not allowed, but pretend it was
// moreObjects.add(new Date());
// String s = moreObjects.get(0); // uh oh
// 感觉按照下面的解释, 这里应该是 String s = strings.get(0);

Since String is a subclass of Object, you can assign a String reference to an Object reference. You can’t however, add an Object reference to a List<String>, which feels strange. The problem is that List<String> is NOT a subclass of List<Object>. When declaring a type, the only instances you can add to it are of the declared type. That’s it. No sub- or superclass instances allowed. We say that the parameterized type is invariant.

The commented out section shows why List<String> is not a subclass of List<Object>. Say you could assign a list of strings to a reference to a list of objects. Then, using the list of objects reference, you could add something that wasn’t a string to the list, which would cause a cast exception when you tried to retrieve it using the original reference to the list of strings. The compiler wouldn’t know any better.

这段话主要说明了List<TypeA>List<TypeB>是没有什么关系的. 如果List<String>能转型为List<Object>那么我们就可以往List<String>里面加入其他类型的对象, 这显然是不正确的, 所以泛型类并不是协变的.


如何理解 <? super>

List<? super Number> list 表明list引用可以指向元素类型为Number或者Number的超类的List, 比如说List<Number>List<Object>.
实例:

public void numsUpTo(Integer num, List<? super Integer> output) {
    IntStream.rangeClosed(1, num)
        .forEach(output::add);
}

ArrayList<Integer> integerList = new ArrayList<>();
ArrayList<Number> numberList = new ArrayList<>();
ArrayList<Object> objectList = new ArrayList<>();

numsUpTo(5, integerList);
numsUpTo(5, numberList);
numsUpTo(5, objectList);

因为<? super Integer>所以, 往容器加Integer是绝对安全的, 因为实际的List要么是Integer要么是Integer的父类, 所以Integer引用一定能转型为Integer或者Integer的父类引用.

实例2(Collections类的max方法):

public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp) {
        if (comp==null)
            return (T)max((Collection) coll);

        Iterator<? extends T> i = coll.iterator();
        T candidate = i.next();

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

注意Comparator<? super T> comp部分, 在Comparator的泛型参数中使用了super, 表明可以使用T的父类的比较方法.
注意对比以下几个方法声明:

public static <T> T max(Collection<? extends T> collection, Comparator<T> comparator){
        return null;
}

public static <T> T max2(Collection<T> collection, Comparator<T> comparator){
        return null;
}

然后有Father和Son类:

static class Father{

}

static class Son extends Father{

}

测试:

public static void main(String[] args) {
    List<Son> sons = new ArrayList<Son>();
    Collections.max(sons, new Comparator<Father>(){
        @Override
        public int compare(Father o1, Father o2) {
            return 0;
        }
    });

    max(sons, new Comparator<Father>(){
        @Override
        public int compare(Father o1, Father o2) {
            return 0;
        }
    });

//    不能这样调用max2
//    max2(sons, new Comparator<Father>(){
//        @Override
//        public int compare(Father o1, Father o2) {
//            return 0;
//        }
//    });
}

其中max2的调用是错误的, 因为max2的参数表明该容器存放的类型必须实现了跟自己比较的Comparator.
但是为什么max(Collection<? extends T> collection, Comparator<T> comparator)Comparator的参数没用super但是例子中调用却是合法的呢? 这是因为对于调用max(sons, new Comparator<Father>(){...}), Java推断了类型参数<T>Father, 而Collection<? extends Father>表明是可以传入存放Son类型的容器的.


如何合理使用通配符

PECS - Producer - Extends, Consumer - Super, 这个词来来源Effective Java一书.

  • Producer
    这里生产者的意思是, 你要从某个参数中获取某个类型的数据, 那么声明这个参数类型为<? extends T>. 比如说List<? extends Number> list, 表明list是一个生产者, 你可以从list中取出Number对象.
  • Consumer
    这里消费者的意思是, 这个参数将消费(使用)到某个类型的数据, 那么应该将参数声明为<? super T>. 比如说Collection<? super E> coll, 表明coll可以消费E类型的数据.
  • 即要消费又要生产
    那么就不使用通配符.

java 官方文档也有关于使用通配符的建议.

下面是一些例子, 多数来自Effective Java (2nd Edition).

实例

static <E> E reduce(List<E> list, Function<E> f, E iniVal);  // #1

list仅仅用于produce类型为E的数据, 所以符合producer的角色, 所以应该将其声明为List<? extends E>. 而Function<E> f既要消费E又会产生E, 所以直接使用具体类型. 修改后的声明如下:

static <E> E reduce(List<? extends E> list, Function<E> f, E iniVal); // #2

那么上面两者有什么区别呢? 对于#1, 当Function<E> fFunction<Number>的时候, 对于List<Integer> listOfIntegers来说, 是传不进去的, 只能是List<Number>. 但是#2, 因为listList<? extends E>, 所以此处可以传入listOfIntegers.

  1. 类型推导
public static <E> Set<E>  union(Set<E> s1, Set<E> s2);

s1和s2都是producer, 所以修改为以下声明

public static <E> Set<E>  union(Set<? extends E> s1, Set<? extends E> s2);

注意返回值仍然是Set<E>, 而不是Set<? extedns E>. 如果改成后者, 那么用户代码也必须使用通配符, 这是一个不好的决定.
类型推导的规则十分复杂, 在[JLS, 15.12.2.7-8]中有整整16页描述. 虽然大多数情况下, 用户无需指定类型参数, 但是对于有些情况, 则必须由用户指定边界的类型到底是什么.

Set<Integer> integers = ...;
Set<Double> doubles = ...;
Set<Number> numbers = union(integers, doubles);

感觉上Java应该推断E为Number, 但是在不指定具体类型参数的时候, 却会报错.

注意: 在我个人实验这段代码的时候, 编译器已经正确推导出了类型.(Java 1.8), 因为Effective Java (2nd Edition)出版于2008年, 所以应该是老版本编译器的问题.

显式指定类型参数:
注意以下几点:

  • class后面的类型参数是无法被静态方法使用的, 静态方法必须自己重新定义类型参数
    这个和class的类型参数不能用于静态方法一样, 因为静态属性和方法都是整个类共有的, 如果有其他地方传入了两种不同的类型, 那么静态属性或者方法不可能同时拥有两种类型, 所以这是不被允许的. 可见我的另一篇文章Java 泛型使用限制.
  • 显式指定类型参数的静态泛型方法的调用格式:
    ClassName.<Type...>methodName();
    <>后和方法名之间不用再加.
public class TypeInference {

    public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2){
        Set<E> result = new HashSet<>();
        result.addAll(s1);
        result.addAll(s2);
        return result;
    }

    public static void main(String[] args) {
        Set<Integer> integerSet = new HashSet<>();
        Set<Double> doubleSet = new HashSet<>();
        Set<Number> numberSet = union(integerSet, doubleSet);
        // 显式指定
        numberSet = TypeInference.<Number>union(integerSet, doubleSet);
    }
}
  1. max方法的声明
public static <T extends Comparable<T>> T max(List<T> list);  // #1

修改过后

public static <T extends Comparable<? super T>> T max(List<? extends T> list);  // #2

那么#2的优点在哪呢? 首先对于Comparable<? super T>表明, 可以用其父类的比较函数来比较子类. 对于list参数, 是对PECS的应用, 但是我个人认为list在这个语境下即使被定义为List<T> list也能有同样的效果.

  1. List<?> 和 List<E>
public static <E> void swap(List<E> list,   int i,  int j); // #1
public static void swap(List<?> list,   int i,  int j);  // #2

对于#1, list能够get也能add, 对于#2只能取出Object, 而且只有null能作为add的参数.
基于#2的交换代码:

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

我们会发现, 编译器不会通过这段代码, 看起来很违背直觉. 从同一个列表拿出的元素竟然不能放回去. 这是因为<?>, 编译知道list中的元素是某个具体类型, 但是因为是?, 所以并不知道具体是哪个类型. 所以从listget方法中拿出的数据, 只能是Object引用, 这样才安全. 对于set()方法, 除了null, 编译器不会允许我们放入任何其他东西, 因为编译器无法判断我们要添加的东西到底是不是?的那个类型, 所以就会阻止我们这么做.
而使用#1的声明, 这一操作就可以执行了, 因为编译器知道list中的元素可以安全的转换为E, 而add方法由于现在有了E, 也知道, 可以安全的放入E对象, 所以swap就可以正常工作.

书上提到<?>会比<E>看起来是更好的API声明, 像swap函数, 对外的声明仍然是#2形式, 然后内部实现采用#1的私有函数来做. 这里也涉及到一个概念叫capture, 有些编译错误中会有capture, 实际是指的是编译器为不确定的?类型定义了一个名字而已. 详细可见capture.


<?> 与 Raw Type

stackoverflow上有一篇讨论raw type的提问, 里面讲到了Raw Type和<?>的区别.
what-is-a-raw-type-and-why-shouldnt-we-use-it
使用了<?>的话, compiler会进行类型检查, 所以不能够通过一个List<?>的引用, 往List实例中添加任何元素(null除外, 因为null可以赋值给任何引用对象), 因为根本无法确定List<?>到底指向了什么类型的List, 所以无法保证类型安全, 所以不能通过这种引用添加元素.

static void appendNewObject(List<?> list) {
    list.add(new Object()); // compilation error!
}

但是, 如果参数是List这种Raw Type, 那么添加任何元素都是可以的:

List list = new ArrayList<Integer>();
list.add(0);
list.add("what");

上面这段代码是可以运行的, 但是compiler会给出警告.
在引入泛型以后, 使用Raw Type是不被推荐的, 使用Raw Type只是为了兼容性问题!
例外情况, 因为Java泛型擦除的关系, List<String>.class是错误的, 因为Java泛型没有生成新的class, 所以当需要引用List这个class的时候, 必须使用List.class, 同理使用instanceof操作符的时候, 也只能用o instanceof Set而不能够o instanceof Set<String>.

List, List<?>, List<Object> 区别

    public static void testFunction(List<Integer> integerList){
        // do nothing
    }

    public static void main(String[] args) {
        List rawList = new ArrayList();
        List<?> wildcardList = new ArrayList<>();
        List<Object> objectList = new ArrayList<>();

        // 编译器不会阻止我们添加任何对象, 但是会给出一个警告
        rawList.add("we can add anything to rawList");

        // 编译器直接拒绝这个操作
        // wildcardList.add("we cannot add anything, because the compiler don't know what the exact type for ? is");

        // 可以添加任何对象
        objectList.add("we can add anything too, because everything is derived from Object");

        // 同样是warning, 但是允许传入, 这很不安全, 但是raw type就是可以这样做
        // 所以raw type可作为任何List<AnyType>的参数
        testFunction(rawList);

        // 编译器不允许
        // testFunction(wildcardList);

        // 也是不允许的
        // testFunction(objectList);

        // 所以使用RawType确实是很危险的, 因为编译器只会给警告, 而不会阻止我们做一些潜在危险的事情
    }

推荐阅读更多精彩内容