无界通配符

96
呆呆李宇杰
2017.09.18 23:40* 字数 1654

无界通配符

无界通配符<?> 看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。而事实上,编译器初看起来是支持这种判断的。

public class UnBoundedWildcards {
    static List list1;
    static List<?> list2;
    static List<? extends Object> list3;

    static void assign1(List list) {
        list1 = list;
        list2 = list;

        // warning: unchecked conversion
        // Found: List, Require: List<? extends Object>
        list3 = list;
    }

    static void assign2(List<?> list) {
        list1 = list;
        list2 = list;
        list3 = list;
    }

    static void assign3(List<? extends Object> list) {
        list1 = list;
        list2 = list;
        list3 = list;
    }


    public static void main(String[] args) {
        assign1(new ArrayList());
        assign2(new ArrayList());
        // warning: unchecked conversion
        // Found: ArrayList, Require: List<? extends Object>
        assign3(new ArrayList());

        assign1(new ArrayList<String>());
        assign2(new ArrayList<String>());
        assign3(new ArrayList<String>());

        List<?> wildList = new ArrayList();
        assign1(wildList);
        assign2(wildList);
        assign3(wildList);
    }
}

编译器很少关心使用的是原生类型还是<?>,而在这种情况下,<?>可以被认为是一种装饰,但是它仍旧是很有价值的,因为,实际上,它是在声明:“想用Java的泛型来编写代码,但是我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。”


下面的示例展示了无界通配符的一个重要的应用。当你处理多个泛型参数的时,有时允许一个参数可以是任何的类型,同时为其他参数确定某种特定类型的能力会显得尤为重要。

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

    static void assign1(Map map) {
        map1 = map;
    }

    static void assign2(Map<?, ?> map) {
        map2 = map;
    }

    static void assign3(Map<String, ?> map) {
        map3 = map;
    }

    public static void main(String[] args) {
        assign1(new HashMap());
        assign2(new HashMap());
        // warning
        // Unchecked assignment: 'java.util.HashMap' to 'java.util.Map<java.lang.String,?>'
        assign3(new HashMap());
        
        assign1(new HashMap<String, Integer>());
        assign2(new HashMap<String, Integer>());
        assign3(new HashMap<String, Integer>());
    }
}

但是,当拥有的全都是无界通配符的时候,就像在Map<?,?>中看到的那样,编译器看起来无法将其与原生Map区分开了。而另外UnBoundedWildcards.java中也展示了编译器处理List<?>List<? extends Object>时是不同的。
而令人困惑的是,编译器并非总是关注像ListList<?>之间的差异,因此它们看起来就像是相同的事物。事实上,由于泛型参数将擦除到它的第一个边界,因此List<?>看起来就等价于List<Object>,而List实际上也是List<Object>List实际上是表示“持有任何Object类型的原生List”,而List<?>表示“具有某种特定类型”的非原生List,并且我们不知道哪个类型是什么。
那么问题来了,编译器什么时候才会去关注原生类型和涉及无界通配符的类型之间的差异呢?下面的示例使用了之前定义的Holder<T>类,它包含接受Holder作为参数的各种方法,但是它们具有不同的形式,作为原生类型,具有具体的类型参数以及具有无界通配符参数。

public class Wildcards {

    static void rawArgs(Holder holder, Object arg) {
        // unchecked warning
        // holder.set(arg);
        // same warning
        // holder.set(new Wildcards());

        // compile error, don't have any T
        // T t = holder.get();

        // No warning, but type information has been lost
        Object obj = holder.get();
    }


    // 和rawArgs相似,但是其中的warning会变成编译错误
    static void unboundedArg(Holder<?> holder, Object arg) {
        // Compile Error set(capture of ?) but accept Object
        // holder.set(arg);
        // same error
        // holder.set(new Wildcards());

        // compile error, don't have any T
        // T t = holder.get();

        // No warning, but type information has been lost
        Object obj = holder.get();
    }

    static <T> T exact1(Holder<T> holder) {
        T t = holder.get();
        return t;
    }

    static <T> T exact2(Holder<T> holder, T arg) {
        holder.set(arg);
        T t = holder.get();
        return t;
    }

    static <T> T wildSubtype(Holder<? extends T> holder, T arg) {
        // Error
        // set (capture<? extends T>) in Holder cannot be applied to (T)
        // holder.set(arg);
        T t = holder.get();
        return t;
    }

    static <T> void wildSupertype(Holder<? super T> holder, T arg) {
        holder.set(arg);
        // Error
        // Required: T Found: capture<? super T>
        // T t = holder.get();

        // No warning, but type information has been lost
        Object obj = holder.get();
    }

    public static void main(String[] args) {
        Holder raw = new Holder<Long>();
        // Or
        raw = new Holder();
        Holder<Long> qualified = new Holder<>();
        Holder<?> unbounded = new Holder<Long>();
        Holder<? extends Long> bounded = new Holder<Long>();
        Long lng = 1L;

        rawArgs(raw, lng);
        rawArgs(qualified, lng);
        rawArgs(unbounded, lng);
        rawArgs(bounded, lng);

        unboundedArg(raw, lng);
        unboundedArg(qualified, lng);
        unboundedArg(unbounded, lng);
        unboundedArg(bounded, lng);

        // unchecked warning
        Object r1 = exact1(raw);
        Long r2 = exact1(qualified);
        Object r3 = exact1(unbounded);
        Long r4 = exact1(bounded);

        // unchecked warning
        Long r5 = exact2(raw, lng);
        Long r6 = exact2(qualified, lng);
        // Error 参数错误
        // Long r7 = exact2(unbounded, lng);
        // Error 参数错误
        // Long r8 = exact2(bounded, lng);

        // unchecked warning
        Long r9 = wildSubtype(raw, lng);
        Long r10 = wildSubtype(qualified, lng);
        Object r11 = wildSubtype(unbounded, lng);
        Long r12 = wildSubtype(bounded, lng);

        // unchecked warning
        wildSupertype(raw, lng);
        wildSupertype(qualified,lng);
        // Error 参数错误
        // wildSupertype(unbounded,lng);
        // wildSupertype(bounded,lng);
    }
}

rawArgs()中,编译器知道Holder是一种泛型类型,因此即使它在这里被表示为一个原生类型,编译器仍旧知道向set()传递一个Object是不安全的。由于它是原生类型,可以将任何类型的对象传递给set(),而这个对象将向上转型为Object。因此,无论何时,只要使用了原生类型,都会放弃编译器检查。而对get()的调用说明了相同的问题:没有T类型的对象,因此结果只能是一个Object
人们很自然地会开始考虑原生HolderHolder<?>是大致相同的事物。但是uboundedArg()强调它们是不同的——它揭示了相同的问题,但是它将这些问题作为错误而不是警告报告,因为原生Holder将持有任何类型的组合,而Holder<?>将持有某种具体类型的同构集合,因此不能只是向其中传递Object
exact1()exact2()中,都使用了确切的泛型参数——没有任何的通配符。而exact2()exact1()多了一个额外的参数。
wildSubType()中,在Holder类型上的限制被放松为持有任何扩展至T对象的Holder,这还是意味着如果TFruit,那么holder可以是Holder<Apple>。而为了防止将Orange放置到Holder<Apple>中,对set()的调用(或者对任何接受这个类型参数为参数的方法的调用)都是不允许的。但是,你仍然知道任何来自Holder<? extends Fruit>的对象至少是Fruit,因此get()(或者任何将产生具有这个类型参数的返回值的方法)都是允许的。
wildSupertype()展示了超类型通配符,这个方法展示了和wildSubtype()相反的行为:holder可以是持有任何T的基类型的容器,所以,set可以接受T,因为可以工作于基类的对象都可以多态地作于导出类(这里指的是T)。但是,尝试着调用get()是没有用的,因为由holder持有的类型是任何超类型,唯一安全的是Object
这个示例还展示了对于unbounded()中使用无界通配符能够做不能上吗做什么所做出的限制。对于迁移兼容性rawArgs()将接受所有Holder的不同变体,而不会产生警告。unboundedArg()方法也可以接受相同的所有类型,尽管如前所述,它在方法体内部处理这些类型的防暑并不相同。
如果向接受“确切”泛型类型(没有通配符)的方法传递一个原生Holder引用,就会得到一个警告,因为确切的参数期望得到在原生类型中并不存在的信息。如果向exact1()传递一个一个无界引用,就不会有任何可以确定返回类型的信息。
而可以看到,exact2()具有最多的限制,因为它希望精确地得到一个Holder<T>,以及一个具有类型T的参数,正因为如此,它将产生错误或者警告,除非提供确切的参数。虽然有时这样做很好,但是如果它过于受限,那么就可以使用通配符,这取决于是否想从泛型参数中返回类型确定的返回值(就像在wildSubtype中看到的那样),或者是否想要向泛型参数传递类型确定的参数(就像在wildSupertype()中看到那样)。
因此,使用确切类型来替代通配符的好处是,可以用泛型参数来做更多的事,但是使用通配符使得必须接受范围更宽的参数化类型作为参数。因此必须权衡利弊,找到适合的方法。

Java编程思想笔记
Web note ad 1