小言_互联网的博客

Java泛型 通配符详解

362人阅读  评论(0)

用法简介

  • 通配符?后只允许出现一个边界。
  • 通配符只允许出现在引用中(普通变量引用、形参),一般是用作<? extends 具体类型>或者<? super 具体类型>。相对地,比如通配符不允许出现在泛型定义中(泛型类、泛型接口、泛型方法的< >里),class one<? extends Integer> {}这样是不允许的,类定义时继承泛型类时的< >里也不可以出现。在泛型类或泛型方法的{ }里还有泛型方法的形参上,配合占位符,甚至可以使用? extends T或者? super T这种形式来用作引用。
  • 在new泛型类的时候也不可以使用通配符,比如new ArrayList<?>()。泛型方法的显式类型说明也不可以使用通配符。

数组协变

具体讲通配符之前,有必要先讲一下数组协变。数组协变可以理解为多态,即子类对象数组可以向上转型为父类对象数组的引用。由于java里的数组在初始化后一定会记住元素的类型,虽然数组协变会带来一些问题(下例就会演示),但有了数组的运行时元素插入的类型检查保护,使得造成的问题不会那么严重。所以即使数组可以协变,它也是足够安全的。
从历史原因上讲,是因为JDK1.0的时候没有设计出真正的泛型(像C++的模板,运行时也可以获得类型参数的真正类型),但当时又有着通用数组的需求(比如,底层实现为数组的容器类)。

class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

public class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruit = new Apple[10];
        //Jonathan[] jonathans = (Jonathan[])fruit;//运行时报错ClassCastException: [LApple; cannot be cast to [LJonathan;
        Apple[] apples = (Apple[])fruit;

        fruit[0] = new Apple(); // OK
        fruit[1] = new Jonathan(); // OK

        // Runtime type is Apple[], not Fruit[] or Orange[]:
        try {
            // Compiler allows you to add Fruit:
            fruit[0] = new Fruit(); // ArrayStoreException
        } catch(Exception e) { System.out.println(e); }
        try {
            // Compiler allows you to add Oranges:
            fruit[0] = new Orange(); // ArrayStoreException
        } catch(Exception e) { System.out.println(e); }
    }
} /* Output:
java.lang.ArrayStoreException: Fruit
java.lang.ArrayStoreException: Orange
*///:~

  • Fruit[] fruit = new Apple[10]此句便是数组协变的表现。
  • Jonathan[] jonathans = (Jonathan[])fruit,数组强转为其他数组和正常的强转表现一样,jvm会检测对象的真实类型,从而判断是否可以强转,所以此句报错ClassCastException。Apple[] apples = (Apple[])fruit同理,根据真实类型,此句强转是可以的。
  • 由于fruit这个引用的类型是Fruit[],所以可以数组的各个元素赋值以Fruit或者Fruit的子类;但由于fruit这个引用它引用的对象的真正类型是Apple[],当在赋值的时候,数组运行时的检测会判断赋值进来的对象的类型是否正确,当赋值类型不符合真正类型时,报错ArrayStoreException。

<? extends 类型>获得泛型类的“协变”

对比之前的数组协变,你却发现List<Fruit> flist = new ArrayList<Apple>()这样的行为都无法通过编译。之所以java这么设计,是因为泛型不像数组,泛型没有内建的协变类型。具体地说,类型信息在编译过后就被类型擦除掉了,运行时也就没法检查了(不像数组,有运行时的插入检查)。

为了获得泛型类的“协变”,可以将引用类型设为? extends 类型

        List<? extends Apple> extendsList = new ArrayList<Apple>();
        extendsList = new ArrayList<Jonathan>();
        //extendsList = new ArrayList<Fruit>();//编译报错

通过将引用的泛型设为<? extends Apple>,此时这个引用便可以接受Apple以及子类的容器。

为了获得泛型类的“逆变”,可以将引用类型设为? super 类型

        List<? super Apple> superList = new ArrayList<Apple>();
        superList = new ArrayList<Fruit>();
        //superList = new ArrayList<Jonathan>();//编译报错

这里同理,通过将引用的泛型设为<? super Apple>,此时这个引用便可以接受Apple以及父类的容器。

<? extends 类型><? super 类型>

有得必有失,虽然上一章的例子让引用获得协变和逆变的效果,但这会对泛型类的读写操作产生限制。

  • 对于<? super 类型>,编译器将只允许写操作,不允许读操作。即只可以设值(比如set操作),不可以取值(比如get操作)。
  • 对于<? extends 类型>,编译器将只允许读操作,不允许写操作。即只可以取值,不可以设值。
  • 以上两点都是针对于源码里涉及到了类型参数的函数而言的。比如对于List而言,不允许的写操作有add函数,因为它的函数签名是boolean add(E e);,此时这个形参E就变成了一个涉及了通配符的类型;而不允许的读操作有get函数,因为它的函数签名是E get(int index);,此时这个返回值E就变成了一个涉及了通配符的类型。

此例中被注释的语句均会报编译异常。

import java.util.*;

public class SuperAndExtendsWildcards {
    public static void main(String[] args) {
    
        //List<? super Apple> superList1 = new ArrayList<Jonathan>();
        List<? super Apple> superList2 = new ArrayList<Apple>();//右边可省略
        List<? super Apple> superList3 = new ArrayList<Fruit>();
        List<? super Apple> superList4 = new ArrayList<Object>();

        superList2.add(new Jonathan());
        superList2.add(new Apple());
        //superList2.add(new Fruit());
        Object o1 = superList2.get(0);
        //Fruit o2 = superList2.get(0);

        List<? extends Apple> extendsList1 = new ArrayList<Jonathan>();
        List<? extends Apple> extendsList2 = new ArrayList<Apple>();
        //List<? extends Apple> extendsList3 = new ArrayList<Fruit>();

        //Jonathan a1 = extendsList1.get(0);
        Apple a2 = extendsList1.get(0);
        Fruit a3 = extendsList1.get(0);
        //extendsList1.add(new Apple());

        List<?> onlyWild = new ArrayList<Apple>();
        //onlyWild.add(new Object());//连Object都不可以加进去
        Object o = onlyWild.get(0);
    }
} ///:~
  • 在第一组代码中,这4个superList的引用体现出了List<? super Apple>的逆变作用。这种引用能接受的ArrayList的类型参数具体类型,只能是Apple以及父类。
  • 在第二组代码中,由于superList2的引用类型为List<? super Apple>,这种情况下在读写操作中,唯一能安全进行的就只有写操作了(写操作指形参类型是类型参数E),因为你把something extends Apple类型的对象赋值给形参? super Apple肯定是合法的。所以在代码里,可以且仅可以add类型为Apple或者Apple子类的对象。
  • 在第二组代码中,本来superList2是不可以进行读操作的,因为读取到的数据的类型是? super Apple,类型没有上限你根本不知道该赋值为什么类型,但由于java所有对象都是Object的子类,导致? super Apple也拥有了上限。所以Object o1 = superList2.get(0)可以获得到一个Object引用,但这样的操作也没有什么意义。
  • 在第三组代码中,这3个extendsList的引用体现出了List<? extends Apple>的协变作用。这种引用能接受的ArrayList的类型参数具体类型,只能是Apple以及子类。
  • 在第四组代码中,由于extendsList1的引用类型为List<? extends Apple>,这种情况下在读写操作中,唯一能安全进行的就只有读操作了(读操作指返回值类型是类型参数E),因为你把? extends Apple类型的返回值赋值给引用something super Apple肯定是合法的。所以在代码里,可以且仅可以把get函数返回值赋值给Apple或者Apple父类的引用。
  • 在第四组代码中,extendsList1是不可以进行写操作的,因为读取到的数据的类型是? extends Apple,类型没有下限编译器根本不知道该给形参传什么类型的实参才对,所以禁止了这种行为。
  • 在第五组代码中,List<?>相当于List<? extends Object>,分析过程同上。

所以,要分析读写操作哪个是合法的,实际上应该关注方法的形参或返回值的类型是否为泛型类型参数,我们可以从引用的类型反向推测出来:

  • 引用类型为List<? super Apple>,那么合法行为只能是something extends Apple类型赋值给? super Apple,要做到这样的事,只能是方法的形参类型是类型参数E。因为这样使用者就可以把实参控制为something extends Apple,传递给形参类型为? super Apple的方法以实参(此时E被? super Apple代替)。而形参类型为类型参数的方法,一般都是“写操作”。
  • 引用类型为List<? extends Apple>,那么合法行为只能是? extends Apple类型赋值给something super Apple,要做到这样的事,只能是方法的返回值类型是类型参数E。因为这样使用者就可以把函数返回赋值的引用控制为something super Apple,然后用这个引用来接返回值类型为? extends Apple的返回值(此时E被? extends Apple代替)。而返回值类型为类型参数的方法,一般都是“读操作”。


上图就完美表达了最后说的这两点,箭头代表了赋值关系,左边是返回值赋值给引用,右边是实参赋值给形参。而something则是客户端程序员来控制的。这两个箭头即表达了两种情况的合法操作,左边箭头代表了读操作,右边箭头代表了写操作。

另外,泛型类中的某个方法如果没有涉及到通配符,那么这样的方法是没有限制的,比如ArrayList里的:

	public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

这两个函数由于形参的类型不是E这个类型参数,而是Object类型。所以你可以对LIst<? extends Apple>这样的引用执行这两个函数,虽然这两个函数看起来属于“写操作”。

JDK实际例子

import java.util.Collections;这个工具类里面的copy方法:

    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

这个方法就充分利用通配符的两种情况的合法操作和限制操作,src作为源,dest作为目标,那么从源复制到目标,就需要:源可以执行读操作;目标可以执行写操作。所以,src的引用类型为List<? extends T>,而dest的引用类型为List<? super T>

通配符与泛型方法的类型推断

可以先看一下java8的官方文档TypeInference这里虽然用的例子不够多,但说了两段话解释了类型推断:

Type inference is a Java compiler’s ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation applicable. The inference algorithm determines the types of the arguments and, if available, the type that the result is being assigned, or returned. Finally, the inference algorithm tries to find the most specific type that works with all of the arguments.
Note: It is important to note that the inference algorithm uses only invocation arguments, target types, and possibly an obvious expected return type to infer types. The inference algorithm does not use results from later in the program.

大意就是:编译器会寻找最合适的推断结果来使得此次泛型方法的调用可行。类型推断只能通过invocation arguments调用参数、target types目标类型、an obvious expected return type显式的期待的返回值,这三种东西来类型推断。总之,关于类型推断,当编译器实在找不到合适的类型可以推断时,就会报编译错误。

import java.util.*;

public class GenericWriting {
    static List<Jonathan> jonathan = new ArrayList<Jonathan>();
    static List<Apple> apples = new ArrayList<Apple>();
    static List<Fruit> fruit = new ArrayList<Fruit>();
    //以下函数都是成对出现,为了方便分析。所以被注释掉的语句都是因为编译错误。
    
    static <T> void writeExact(List<T> list, T item) {
        list.add(item);
    }
    static void f1() {
        writeExact(apples, new Apple());
        writeExact(fruit, new Apple());//这里证明了两个形参都进行推断,最后取了共同父类Fruit
        //writeExact(jonathan, new Apple());//编译错误。就算推断为共同父类Apple,由于泛型类没有协变,所以也编译错误
    }
    
    static <T> void
    writeWithWildcard(List<? super T> list, T item) {
        list.add(item);
    }
    static void f2() {
        writeWithWildcard(apples, new Apple());
        writeWithWildcard(fruit, new Apple());
        //writeWithWildcard(jonathan, new Apple());//编译错误。就算推断为jonathan了,但第二个参数发生了逆变,这样不可以。
    }
    
    static <T> T
    returnWithWildcard(List<? extends T> list) {
        return list.get(0);
    }
    static void f3() {
        //可理解为只用了形参进行推断。所以返回值类型为Jonathan,然后赋值向上转型
        Apple a1 = returnWithWildcard(jonathan);
        //Apple a2 = returnWithWildcard(fruit);//编译错误

        List<? extends Apple> tempList3 = apples;
        Apple a5 = returnWithWildcard(tempList3);//理解为T推断为Apple。

        List<? extends Fruit> tempList1 = apples;
        //编译错误。catch到的? extends Fruit,无法赋值给Apple
        //Apple a3 = returnWithWildcard(tempList1);
        Fruit a3 = returnWithWildcard(tempList1);//这样就能编译。理解为T推断为Fruit。

        List<? super Jonathan> tempList2 = apples;
        //编译错误。catch到的? super Jonathan,无法赋值给Apple
        //Apple a4 = returnWithWildcard(tempList2);
        Object a4 = returnWithWildcard(tempList2);//这样就能编译。理解为T推断为Object。
    }  

    static <T extends Jonathan> T
    returnWildAndBoundary_ver1(List<? extends T> list) {
        return list.get(0);
    }
    static void f4() {
        Apple a0 = returnWildAndBoundary_ver1(jonathan);
        //理解T被推断为Apple,但推断出的具体类型不符合边界extends Jonathan
        //Apple a1 = returnWildAndBoundary_ver1(apples);
        List<? extends Jonathan> tempList1 = jonathan;
        Apple a3 = returnWildAndBoundary_ver1(tempList1);//理解T被推断为Jonathan
        List<? super Jonathan> tempList2 = apples;
        //Apple a4 = returnWildAndBoundary_ver1(tempList2);//返回值? super Jonathan在返回之前加了强制类型转换为Jonathan
        //除非T被推断为Object,但Object又不符合边界extends Jonathan。总之编译器找不到合适的类型。
    }
    
    public static void main(String[] args) {
        f1(); f2();
        //Collections.copy(apples,fruit);//编译错误
        Collections.copy(fruit,apples);
    }
} ///:~

介绍几种泛型方法的例子:

  • writeExact函数形参类型分别为List<T>TwriteExact(List<T> list, T item)方法的签名没有涉及到通配符,此时推断两个参数都会依靠到。编译writeExact(apples, new Apple())时,理所应当这里被推断为Apple;编译writeExact(fruit, new Apple())时,这里被推断为共同父类Fruit了,所以编译通过,所以第二个参数就是正常的多态行为了;编译writeExact(jonathan, new Apple())时,这里被推断共同父类Apple了,但是这样导致此时第一个形参的引用类型为List<Apple>,由于没用通配符的泛型类并不能支持协变,所以这里报错了。
  • writeWithWildcard函数形参类型分别为List<?super T>TwriteWithWildcard(List<? super T> list, T item)方法的签名涉及到了通配符,此时根据推断出的具体类型,第一个形参与实参必须符合逆变的效果writeWithWildcard(fruit, new Apple())这里能编译通过,既可以理解推断为了Apple类型,也可以理解为推断为了Fruit类型。writeWithWildcard(jonathan, new Apple())编译通不过是因为第二个实参类型为Apple,所以此时第一个形参的类型实际为List<? super Apple>,这样的引用类型肯定无法接受List<Jonathan>的实参(其实这里是无论推断为Jonathan类型,还是Apple类型,都无法通过编译,编译器也拿你没办法啊)。
  • returnWithWildcard函数返回类型为T,形参类型为List<? extends T>static <T> T returnWithWildcard(List<? extends T> list)方法的签名里只有形参涉及到通配符,虽然形参有通配符,但形参优先于返回值进行类型推断Apple a2 = returnWithWildcard(fruit)编译通不过是因为,该函数根据形参对应的实参,类型推断为了Fruit,所以返回值Fruit赋值给Apple肯定会出错;Apple a5 = returnWithWildcard(tempList3)编译通过,因为类型推断为了Apple,这样返回值赋值便是合理的;Apple a3 = returnWithWildcard(tempList1)不能通过编译,因为通配符之间赋值必须相容,所以T被推断为Fruit,这样返回值赋值会出错。编译器报的错更加直接(见注释),只看赋值的头尾,那么catch到的? extends Fruit,无法赋值给AppleApple a4 = returnWithWildcard(tempList2)编译报错,因为通配符之间赋值必须相容,所以T被推断为Object。关于通配符之间赋值必须相容的知识点,将在后面的《带通配符的引用之间赋值必须相容》章节具体讲解。
  • returnWildAndBoundary_ver1函数与上一点一样,不过这里该函数的类型参数加了边界<T extends Jonathan>,且返回值类型为T。所以在字节码层面讲,该函数返回值之前会加一句checkcast为Jonathan。所以Apple a1 = returnWildAndBoundary_ver1(apples)编译通不过是因为等号右边就已经通不过了,实参推断出的类型Apple无法checkcast为Jonathan。总之,这种情况下,编译器推断出的具体类型还必须符合边界条件<T extends Jonathan>
  • 形参类型分别为List<? super T>List<?entends T>copy(List<? super T> dest, List<? extends T> src)方法的签名涉及到了通配符,且两个形参都涉及到了通配符,此时推断不是那么关键了,关键在于dest的具体类型必须是src的具体类型的父类或者同一类型。所以Collections.copy(apples,fruit)会报错。而Collections.copy(fruit,apples)编译能通过,你既可以理解成推断为Fruit了,也可以理解成推断为Apple了,两种理解都是合理的。
  • 注意super只能配合通配符?使用,泛型方法这么写static <T super Apple> void argumentWithBoundary(List<? extends T> list, T item) {}和泛型类class testSuper<T super Apple> {}这么写,都是会报编译错误的。

总结一下类型推断:

  • 形参或返回值都能进行类型推断。
  • 形参和返回值都是同一个类型参数,优先使用形参进行判断。

“形参或返回值都能进行类型推断”的观点证明可见于本人博客Java 泛型详解:泛型类、泛型接口、泛型方法的章节“返回值的类型参数通过方法返回值赋值的对象确定”。

泛型方法和泛型类中只是使用了类型参数的方法

本章只是强调一下,泛型方法是指返回值前面带尖括号的方法,决不是泛型类中使用了泛型类的类型参数的方法。若已理解,无需看此章。

import java.util.*;

public class GenericReading {
    static <T> T readExact(List<T> list) {//泛型方法
        return list.get(0);
    }
    static List<Apple> apples = Arrays.asList(new Apple());
    static List<Fruit> fruit = Arrays.asList(new Fruit());
    // A static method adapts to each call:
    static void f1() {
        Apple a = readExact(apples);
        //Apple a1 = GenericReading.<Fruit>readExact(apples);
        Fruit f = readExact(fruit);
        f = readExact(apples);//赋给引用发生多态
    }
    // 注意下面的泛型类的readExact方法根本不是泛型方法
    // 它只是一个使用了泛型类的类型参数的方法
    static class Reader<T> {
        T readExact(List<T> list) { return list.get(0); }
    }
    static void f2() {
        Reader<Fruit> fruitReader = new Reader<Fruit>();//泛型类对象建立后,类型参数被Fruit确定
        Fruit f = fruitReader.readExact(fruit);
        // Fruit a = fruitReader.readExact(apples); //编译错误,因为泛型类不能协变,除非使用通配符
    }
    
    static class CovariantReader<T> {
        T readCovariant(List<? extends T> list) {
            return list.get(0);
        }
        //也不是泛型方法,只是形参里有泛型类定义的类型参数,但这里用了通配符,所以可以协变
    }
    static void f3() {
        CovariantReader<Fruit> fruitReader =
                new CovariantReader<Fruit>();
        Fruit f = fruitReader.readCovariant(fruit);
        Fruit a = fruitReader.readCovariant(apples);//实参传递过去时,发生了协变
    }
    public static void main(String[] args) {
        f1(); f2(); f3();
    }
} ///:~

分析过程类似,具体看注释吧。注意静态内部类ReaderCovariantReader里面的方法都不是泛型方法,它们只是泛型类中使用了类型参数的方法。

带通配符的引用之间赋值必须相容

前面提到过,List<?>相当于List<? extends Object>,所以对于所有用到了类型参数的方法(形参类型或返回值类型),其中的合法操作只能是“读操作”,其限制操作则是“写操作”了。带着这一前提,我们再来阅读本章。注意仔细看注释,单句的重要分析都在注释里。

import java.io.Serializable;
import java.util.*;

public class UnboundedWildcards1 {
    static List rawType;
    static List<?> unbounded;
    static List<? extends Long> bounded;
    static List<Long> concrete;
    static void assign1(List rawTypePara) {
        unbounded = rawTypePara;
        bounded = rawTypePara;//unchecked警告
        concrete = rawTypePara;//unchecked警告
    }
    static void assign2(List<?> unboundedPara) {
        rawType = unboundedPara;
        //bounded = unboundedPara;//通配符赋值给通配符时,前者后者范围必须得相容
        //concrete = unboundedPara; //编译错误,通配符赋值给了具体类型
    }
    static void assign3(List<? extends Long> boundedPara) {
        rawType = boundedPara;
        unbounded = boundedPara;//此时范围相容
        //concrete = boundedPara; //编译错误,通配符赋值给了具体类型
    }
    static void assign4(List<Long> concretePara) {
        rawType = concretePara;
        unbounded = concretePara; //与上面的编译错误形成对比,具体类型赋值给涉及了通配符的引用是可以的,
        bounded = concretePara; //只要符合通配符的边界,便可以把具体类型赋值给通配符。unbounded其实也有边界是Object。
    }
    
    static <T> T assign5(List<T> list) {
        return list.get(0);
    }
    static void f5() {
        Object o1 = assign5(rawType);//因为是原生类型,这里只能被推断为Object
        //Long o11 = assign5(rawType);

        //因为是无界通配符,在泛型方法中所有的T被替换为了?
        // 在泛型代码的出口,必须符合赋值的规律。这里只能被赋值给Object
        Object o2 = assign5(unbounded);
        //Long o22 = assign5(unbounded);

        //因为是有界通配符,在泛型方法中所有的T被替换为了? extends Long
        // 在泛型代码的出口,只要符合赋值的规律,就可以赋值。配合本文那个示意图理解。
        Long o3 = assign5(bounded);
        Number o33 = assign5(bounded);
        Serializable o333 = assign5(bounded);
        Comparable o3333 = assign5(bounded);
        Object o33333 = assign5(bounded);

        Holder<Long> l1;
        Holder<? extends Long> l2 = new Holder<>();
        //l1 = l2;//与类型推断无关时,是不可以将带通配符的引用赋值给确切类型的引用的
    }
} ///:~
  • assign1函数里,原生类型List和无界通配符List<?>在互相给对方赋值时(另见assign2函数),不会出现unchecked警告。唯一出现的两个unchecked警告,是因为原生类型List赋值给了类型List<? extends Long>,或者List<Long>。注意,即使你改成:原生类型List赋值给了类型List<? extends Object>,或者List<Object>,也会有警告。
  • assign2与assign3函数说明了,带通配符的引用赋值给带通配符的引用时,二者范围必须得相容,更具体的说,等号左边的范围要大于等于等号右边的范围。带通配符的引用赋值给了具体类型的引用,是不可以的,注意具体类型不包括泛型方法中的形参List<T>,因为这种情况下,T还正在被推断出来。
  • 从assign1到assign4函数中,可见raw type类型和unbounded类型的引用,是可以接其他所有类型的。assign4函数中,bounded = concretePara体现了只要符合通配符的边界,便可以把具体类型赋值给通配符。
  • assign5函数是一个泛型方法,它的类型参数需要被推断出来。f5函数中,Object o1 = assign5(rawType)并不能把具体类型推断出来,因为传入的实参是原生类型,所以泛型代码的出口只能赋值给类型的上限Object;Object o2 = assign5(unbounded),传入的实参是无界通配符,其实跟类型推断无关了,直接理解为所有的T被替换为了?;Long o3 = assign5(bounded),传入的实参是有界通配符,直接理解为所有的T被替换为了? extends Long。
  • 注意assign5函数中的Object o2 = assign5(unbounded)Long o3 = assign5(bounded),其中的实参赋值给形参,不能理解为“带通配符的引用赋值给了具体类型的引用”。

为了更好的学习wildcard,这里先给出几个知识点:

  • 在java中,top type是Object,bottom type是null。(从继承树上来看,最上面肯定是Object)(从赋值上来看,等号左边始终可以放Object,等号右边始终可以放null)
  • 使用了通配符的引用,可以把这种引用看作一个范围,比如List<?>看作从nullObject的范围。而如果通配符带了边界,就只是将这个范围缩小了而已。
  • 两种引用List(raw type)和List<?>(unbounded wildcard)之间互相赋值,编译器不会有任何警告。
  • 带有通配符的引用只能赋值给List(raw type),或者相容的(compatible)带通配符的引用。相对的,不可以赋值给带有具体类型的引用。
  • 根据top type和bottom type,可以分析出遇到通配符(不管带不带边界,也不管带的是什么边界)的泛型代码边界处必然可行的情况,如下图:
import java.util.*;

public class UnboundedWildcards2 {
    static Map map1;
    static Map<?,?> map2;
    static Map<String,?> map3;

    public static void main(String[] args) {
        map1 = new HashMap();
        map2 = new HashMap();
        map3 = new HashMap();// 警告Warning:
        // Unchecked conversion. Found: HashMap
        // Required: Map<String,?>

        map1 = new HashMap<String,Integer>();
        map2 = new HashMap<String,Integer>();
        map3 = new HashMap<String,Integer>();
    }
} ///:~

上例展示了:处理多个类型参数时,有时允许一个参数是通配符,同时其他类型参数被具体类型所指定,而那个为通配符的参数会对与这个参数有关的函数产生限制(本章最开始就讲了)。本例中,Map<?,?>的使用貌似和Map的使用是一样的,起码在赋值上这样。唯一出现的unchecked警告,是因为将原生类型赋值给了Map<String,?>

class Holder<T> {
    private T value;
    public Holder() {}
    public Holder(T val) { value = val; }
    public void set(T val) { value = val; }
    public T get() { return value; }
    public boolean equals(Object obj) {
        return value.equals(obj);
    }
}
public class Wildcards {
    // 原生类型的参数:
    static void rawArgs(Holder holder, Object arg) {
        holder.set(arg); // 警告Warning:
        //   Unchecked call to set(T) as a
        //   member of the raw type Holder
        holder.set(new Wildcards()); // 同样的警告

        // 使用原生类型的Holder,类型参数全替换为Object:
        Object obj = holder.get();
    }
    // 无界通配符的参数:
    static void unboundedArg(Holder<?> holder, Object arg) {
        // holder.set(arg); // 编译错误,因为是写操作
        //   set(capture of ?) in Holder<capture of ?>
        //   cannot be applied to (Object)
        // holder.set(new Wildcards()); // 编译错误
        holder.set(null);//泛型代码入口的唯一可行情况
        // 使用无界通配符的Holder,读操作可赋值给实际的上限Object:
        Object obj = holder.get();//泛型代码出口的唯一可行情况
    }
    static <T> T exact2(Holder<T> holder, T arg) {
        holder.set(arg);
        T t = holder.get();
        return t;
    }
    // 有界extends通配符的参数:
    static <T> T wildSubtype(Holder<? extends T> holder, T arg) {
        // holder.set(arg); // 编译错误,不能写操作
        T t = holder.get();//只可以读操作
        return t;
    }
    // 有界super通配符的参数:
    static <T> void wildSupertype(Holder<? super T> holder, T arg) {
        holder.set(arg);//只可以写操作
        // T t = holder.get();  // 编译错误,不能读操作

        // 本来是不能读操作的,但super有上限Object,所以这是唯一合法的读操作:
        Object obj = holder.get();
    }
    public static void main(String[] args) {
        Holder raw = new Holder<Long>();
        // 上下两行都一样,反正泛型是伪泛型,重要的还是引用的类型
        raw = new Holder();
        Holder<Long> qualified = new Holder<Long>();
        Holder<?> unbounded = new Holder<Long>();
        Holder<? extends Long> bounded = new Holder<Long>();
        Long lng = 1L;

        Long r5 = exact2(raw, lng); // unchecked警告:
        //   类型推断为了Long,所以第一个参数会有unchecked的警告
        Long r6 = exact2(qualified, lng);
        //Long r7 = exact2(unbounded, lng); // 编译错误
        //Long r8 = exact2(bounded, lng); // 编译错误

        Long r9 = wildSubtype(raw, lng); // unchecked警告
        Long r10 = wildSubtype(qualified, lng);
        // 只能返回给Object。因为传递进入的实参类型是无界通配符
        Object r11 = wildSubtype(unbounded, lng);
        Long r12 = wildSubtype(bounded, lng);

        wildSupertype(raw, lng); // unchecked警告
        wildSupertype(qualified, lng);
        //wildSupertype(unbounded, lng); // 编译错误
        //wildSupertype(bounded, lng); // 编译错误
    }
} ///:~

此例中主函数中的函数调用很好的体现了带通配符的引用之间赋值,需要二者的范围相容。注意,exact2、wildSubtype和wildSupertype函数的两个形参都用到了同一个类型参数,类型推断会同时考虑两个实参,如果可以,编译器便能推断出一个合适的具体类型出来。

  • unboundedArg函数里,体现了上面那个示意图,当无界通配符时,只能写入null,读出Object。List<?>的读操作只能读出Object,从这一点可以理解为:List<?>相当于List<? extends Object>(正如本章最开始说的);List<?>的写操作只能写入null。
  • exact2泛型方法里,第一个形参也是具体类型。主函数中4次对此方法的调用,有两次编译报错,这是由于第二个实参已经将类型参数推断为Long了,此时第一个实参赋值给形参,就变成了“带通配符的引用赋值给具体类型Long的引用”,所以就会报错。就算改成Object r7 = exact2(unbounded, new Object())这样,也会报错的。注意,这里要和本章第一个例子中的assign5函数区分开来,因为assign5函数只有一个形参,实参赋值给形参时类型参数还没有被推断出来。
  • wildSubtype泛型方法里,第一个形参有通配符,且边界是<? extends T>。主函数中4次对此方法的调用,都没有报错。Object r11 = wildSubtype(unbounded, lng)这里,只有让T被推断为Object才能让编译通过,第二个形参发生协变自然可以,第一个形参实际上变成了Holder<? extends Object>,然后来接Holder<?>这个实参,只有这样两个通配符的范围才能相容。而Long r12 = wildSubtype(bounded, lng)这里,编译器将T推断为Long就可以使得编译通过了。
  • wildSupertype泛型方法里,第一个形参有通配符,且边界是<? super T>。主函数中4次对此方法的调用,有两次编译报错。wildSupertype(unbounded, lng)这里,由于第二个实参,类型参数必须被推断为Long或者Long的父类了,假设推断为Long,那么第一个形参Holder<? super Long>的范围为从LongObject,而第一个实参Holder<?>的范围则是从nullObject,这样将实参给形参,就会造成范围不相容。推断为Long的父类一样也不相容。wildSupertype(bounded, lng)编译报错也同理。注意,由于bottom type是null,那么wildSupertype(unbounded, null)能通过编译,因为此时第一个形参实际为Holder<? super null>,其范围是从nullObject,这样就能接住实参Holder<?>
  • 有趣的是,对于top type你可以声明List<? extends Object> list1;这样的引用,但对于bottom type你却不可以声明List<? super null> list2;,这样会编译报错。

总结

  • 泛型只存在于编译期中,其实本文大部分例子都是在分析编译器给泛型的静态检查。
  • 通配符使得泛型类获得了协变和逆变的效果。
  • 但通配符也会对读操作或写操作产生限制。

转载:https://blog.csdn.net/anlian523/article/details/100865538
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场