二十、泛型(5)

本章概要

  • 边界
  • 通配符
    • 编译器有多聪明
    • 逆变
    • 无界通配符
    • 捕获转换

边界

边界(bounds)在本章的前面进行了简要介绍。边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在绑定的类型中调用方法。

由于擦除会删除类型信息,因此唯一可用于无限制泛型参数的方法是那些 Object 可用的方法。但是,如果将该参数限制为某类型的子集,则可以调用该子集中的方法。为了应用约束,Java 泛型使用了 extends 关键字。

重要的是要理解,当用于限定泛型类型时,extends 的含义与通常的意义截然不同。此示例展示边界的基础应用:

java 复制代码
interface HasColor {
    java.awt.Color getColor();
}

class WithColor<T extends HasColor> {
    T item;

    WithColor(T item) {
        this.item = item;
    }

    T getItem() {
        return item;
    }

    // The bound allows you to call a method:
    java.awt.Color color() {
        return item.getColor();
    }
}

class Coord {
    public int x, y, z;
}

// This fails. Class must be first, then interfaces:
// class WithColorCoord<T extends HasColor & Coord> {

// Multiple bounds:
class WithColorCoord<T extends Coord & HasColor> {
    T item;

    WithColorCoord(T item) {
        this.item = item;
    }

    T getItem() {
        return item;
    }

    java.awt.Color color() {
        return item.getColor();
    }

    int getX() {
        return item.x;
    }

    int getY() {
        return item.y;
    }

    int getZ() {
        return item.z;
    }
}

interface Weight {
    int weight();
}

// As with inheritance, you can have only one
// concrete class but multiple interfaces:
class Solid<T extends Coord & HasColor & Weight> {
    T item;

    Solid(T item) {
        this.item = item;
    }

    T getItem() {
        return item;
    }

    java.awt.Color color() {
        return item.getColor();
    }

    int getX() {
        return item.x;
    }

    int getY() {
        return item.y;
    }

    int getZ() {
        return item.z;
    }

    int weight() {
        return item.weight();
    }
}

class Bounded
        extends Coord implements HasColor, Weight {
    @Override
    public java.awt.Color getColor() {
        return null;
    }

    @Override
    public int weight() {
        return 0;
    }
}

public class BasicBounds {
    public static void main(String[] args) {
        Solid<Bounded> solid = new Solid<>(new Bounded());
        solid.color();
        solid.getY();
        solid.weight();
    }
}

你可能会观察到 BasicBounds.java 中似乎包含一些冗余,它们可以通过继承来消除。在这里,每个继承级别还添加了边界约束:

java 复制代码
class HoldItem<T> {
    T item;

    HoldItem(T item) {
        this.item = item;
    }

    T getItem() {
        return item;
    }
}

class WithColor2<T extends HasColor>
        extends HoldItem<T> {
    WithColor2(T item) {
        super(item);
    }

    java.awt.Color color() {
        return item.getColor();
    }
}

class WithColorCoord2<T extends Coord & HasColor>
        extends WithColor2<T> {
    WithColorCoord2(T item) {
        super(item);
    }

    int getX() {
        return item.x;
    }

    int getY() {
        return item.y;
    }

    int getZ() {
        return item.z;
    }
}

class Solid2<T extends Coord & HasColor & Weight>
        extends WithColorCoord2<T> {
    Solid2(T item) {
        super(item);
    }

    int weight() {
        return item.weight();
    }
}

public class InheritBounds {
    public static void main(String[] args) {
        Solid2<Bounded> solid2 = new Solid2<>(new Bounded());
        solid2.color();
        solid2.getY();
        solid2.weight();
    }
}

HoldItem 拥有一个对象,因此此行为将继承到 WithColor2 中,这也需要其参数符合 HasColorWithColorCoord2Solid2 进一步扩展了层次结构,并在每个级别添加了边界。现在,这些方法已被继承,并且在每个类中不再重复。

这是一个具有更多层次的示例:

java 复制代码
import java.util.List;

interface SuperPower {
}

interface XRayVision extends SuperPower {
    void seeThroughWalls();
}

interface SuperHearing extends SuperPower {
    void hearSubtleNoises();
}

interface SuperSmell extends SuperPower {
    void trackBySmell();
}

class SuperHero<POWER extends SuperPower> {
    POWER power;

    SuperHero(POWER power) {
        this.power = power;
    }

    POWER getPower() {
        return power;
    }
}

class SuperSleuth<POWER extends XRayVision>
        extends SuperHero<POWER> {
    SuperSleuth(POWER power) {
        super(power);
    }

    void see() {
        power.seeThroughWalls();
    }
}

class
CanineHero<POWER extends SuperHearing & SuperSmell>
        extends SuperHero<POWER> {
    CanineHero(POWER power) {
        super(power);
    }

    void hear() {
        power.hearSubtleNoises();
    }

    void smell() {
        power.trackBySmell();
    }
}

class SuperHearSmell
        implements SuperHearing, SuperSmell {
    @Override
    public void hearSubtleNoises() {
    }

    @Override
    public void trackBySmell() {
    }
}

class DogPerson extends CanineHero<SuperHearSmell> {
    DogPerson() {
        super(new SuperHearSmell());
    }
}

public class EpicBattle {
    // Bounds in generic methods:
    static <POWER extends SuperHearing>
    void useSuperHearing(SuperHero<POWER> hero) {
        hero.getPower().hearSubtleNoises();
    }

    static <POWER extends SuperHearing & SuperSmell>
    void superFind(SuperHero<POWER> hero) {
        hero.getPower().hearSubtleNoises();
        hero.getPower().trackBySmell();
    }

    public static void main(String[] args) {
        DogPerson dogPerson = new DogPerson();
        useSuperHearing(dogPerson);
        superFind(dogPerson);
        // You can do this:
        List<? extends SuperHearing> audioPeople;
        // But you can't do this:
        // List<? extends SuperHearing & SuperSmell> dogPs;
    }
}

接下来将要研究的通配符将会把范围限制在单个类型。

通配符

你已经在 集合章节中看到了一些简单示例使用了通配符------在泛型参数表达式中的问号,在 类型信息 一章中这种示例更多。本节将更深入地探讨这个特性。

我们的起始示例要展示数组的一种特殊行为:你可以将派生类的数组赋值给基类的引用:

java 复制代码
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];
        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);
        }
    }
}

main() 中的第一行创建了 Apple 数组,并赋值给一个 Fruit 数组引用。这是有意义的,因为 Apple 也是一种 Fruit ,因此 Apple 数组应该也是一个 Fruit 数组。

但是,如果实际的数组类型是 Apple[] ,你可以在其中放置 AppleApple 的子类型,这在编译期和运行时都可以工作。但是你也可以在数组中放置 Fruit 对象。这对编译器来说是有意义的,因为它有一个 Fruit[] 引用------它有什么理由不允许将 Fruit 对象或任何从 Fruit 继承出来的对象(比如 Orange ),放置到这个数组中呢?因此在编译期,这是允许的。然而,运行时的数组机制知道它处理的是 Apple[],因此会在向数组中放置异构类型时抛出异常。

向上转型用在这里不合适。你真正在做的是将一个数组赋值给另一个数组。数组的行为是持有其他对象,这里只是因为我们能够向上转型而已,所以很明显,数组对象可以保留有关它们包含的对象类型的规则。看起来就像数组对它们持有的对象是有意识的,因此在编译期检查和运行时检查之间,你不能滥用它们。

数组的这种赋值并不是那么可怕,因为在运行时你可以发现插入了错误的类型。但是泛型的主要目标之一是将这种错误检测移到编译期。所以当我们试图使用泛型集合代替数组时,会发生什么呢?

java 复制代码
import java.util.*;

public class NonCovariantGenerics {
    // Compile Error: incompatible types:
    List<Fruit> flist = new ArrayList<Apple>();
}

尽管你在首次阅读这段代码时会认为"不能将一个 Apple 集合赋值给一个 Fruit 集合"。记住,泛型不仅仅是关于集合,它真正要表达的是"不能把一个涉及 Apple 的泛型赋值给一个涉及 Fruit 的泛型"。如果像在数组中的情况一样,编译器对代码的了解足够多,可以确定所涉及到的集合,那么它可能会留下一些余地。

但是它不知道任何有关这方面的信息,因此它拒绝向上转型。然而实际上这也不是向上转型------ AppleList 不是 FruitListAppleList 将持有 AppleApple 的子类型,FruitList 将持有任何类型的 Fruit 。是的,这包括 Apple ,但是它不是一个 AppleList ,它仍然是 FruitListAppleList 在类型上不等价于 FruitList ,即使 Apple 是一种 Fruit 类型。

真正的问题是我们在讨论的集合类型,而不是集合持有对象的类型。与数组不同,泛型没有内建的协变类型。这是因为数组是完全在语言中定义的,因此可以具有编译期和运行时的内建检查,但是在使用泛型时,编译器和运行时系统不知道你想用类型做什么,以及应该采用什么规则。

但是,有时你想在两个类型间建立某种向上转型关系。通配符可以产生这种关系。

java 复制代码
import java.util.*;

public class GenericsAndCovariance {

    public static void main(String[] args) {
        // Wildcards allow covariance:
        List<? extends Fruit> flist = new ArrayList<>();
        // Compile Error: can't add any type of object:
        // flist.add(new Apple());
        // flist.add(new Fruit());
        // flist.add(new Object());
        flist.add(null); // Legal but uninteresting
        // We know it returns at least Fruit:
        Fruit f = flist.get(0);
    }

}

flist 的类型现在是 List<? extends Fruit>,你可以读作"一个具有任何从 Fruit 继承的类型的列表"。然而,这实际上并不意味着这个 List 将持有任何类型的 Fruit 。通配符引用的是明确的类型,因此它意味着"某种 flist 引用没有指定的具体类型"。因此这个被赋值的 List 必须持有诸如 FruitApple 这样的指定类型,但是为了向上转型为 Fruit,这个类型是什么没人在意。

List 必须持有一种具体的 FruitFruit 的子类型,但是如果你不关心具体的类型是什么,那么你能对这样的 List 做什么呢?如果不知道 List 中持有的对象是什么类型,你怎能保证安全地向其中添加对象呢?就像在 CovariantArrays.java 中向上转型一样,你不能,除非编译器而不是运行时系统可以阻止这种操作的发生。你很快就会发现这个问题。

你可能认为事情开始变得有点走极端了,因为现在你甚至不能向刚刚声明过将持有 Apple 对象的 List 中放入一个 Apple 对象。是的,但编译器并不知道这一点。List<? extends Fruit> 可能合法地指向一个 List<Orange>。一旦执行这种类型的向上转型,你就丢失了向其中传递任何对象的能力,甚至传递 Object 也不行。

另一方面,如果你调用了一个返回 Fruit 的方法,则是安全的,因为你知道这个 List 中的任何对象至少具有 Fruit 类型,因此编译器允许这么做。

编译器有多聪明

现在你可能会猜想自己不能去调用任何接受参数的方法,但是考虑下面的代码:

java 复制代码
import java.util.*;

public class CompilerIntelligence {

    public static void main(String[] args) {
        List<? extends Fruit> flist = Arrays.asList(new Apple());
        Apple a = (Apple) flist.get(0); // No warning
        flist.contains(new Apple()); // Argument is 'Object'
        flist.indexOf(new Apple()); // Argument is 'Object'
    }

}

这里对 contains()indexOf() 的调用接受 Apple 对象作为参数,执行没问题。这是否意味着编译器实际上会检查代码,以查看是否有某个特定的方法修改了它的对象?

通过查看 ArrayList 的文档,我们发现编译器没有那么聪明。尽管 add() 接受一个泛型参数类型的参数,但 contains()indexOf() 接受的参数类型是 Object 。因此当你指定一个 ArrayList<? extends Fruit> 时,add() 的参数就变成了"? extends Fruit "。从这个描述中,编译器无法得知这里需要 Fruit 的哪个具体子类型,因此它不会接受任何类型的 Fruit 。如果你先把 Apple 向上转型为 Fruit ,也没有关系------编译器仅仅会拒绝调用像 add() 这样参数列表中涉及通配符的方法。

contains()indexOf() 的参数类型是 Object ,不涉及通配符,所以编译器允许调用它们。这意味着将由泛型类的设计者来决定哪些调用是"安全的",并使用 Object 类作为它们的参数类型。为了禁止对类型中使用了通配符的方法调用,需要在参数列表中使用类型参数。

下面展示一个简单的 Holder 类:

java 复制代码
import java.util.Objects;

public 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;
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof Holder && Objects.equals(value, ((Holder) o).value);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(value);
    }

    public static void main(String[] args) {
        Holder<Apple> apple = new Holder<>(new Apple());
        Apple d = apple.get();
        apple.set(d);
        // Holder<Fruit> fruit = apple; // Cannot upcast
        Holder<? extends Fruit> fruit = apple; // OK
        Fruit p = fruit.get();
        d = (Apple) fruit.get();
        try {
            Orange c = (Orange) fruit.get(); // No warning
        } catch (Exception e) {
            System.out.println(e);
        }
        // fruit.set(new Apple()); // Cannot call set()
        // fruit.set(new Fruit()); // Cannot call set()
        System.out.println(fruit.equals(d)); // OK
    }
}

Holder 有一个接受 T 类型对象的 set() 方法,一个返回 T 对象的 get() 方法和一个接受 Object 对象的 equals() 方法。正如你所见,如果创建了一个 Holder<Apple>,就不能将其向上转型为 Holder<Fruit>,但是可以向上转型为 Holder<? extends Fruit>。如果调用 get(),只能返回一个 Fruit ------这就是在给定"任何扩展自 Fruit 的对象"这一边界后,它所能知道的一切了。

如果你知道更多的信息,就可以将其转型到某种具体的 Fruit 而不会导致任何警告,但是存在得到 ClassCastException 的风险。set() 方法不能工作在 AppleFruit 上,因为 set() 的参数也是"? extends Fruit",意味着它可以是任何事物,编译器无法验证"任何事物"的类型安全性。

但是,equals() 方法可以正常工作,因为它接受的参数是 Object 而不是 T 类型。因此,编译器只关注传递进来和要返回的对象类型。它不会分析代码,以查看是否执行了任何实际的写入和读取操作。

Java 7 引入了 java.util.Objects 库,使创建 equals()hashCode() 方法变得更加容易,当然还有很多其他功能。

逆变

还可以走另外一条路,即使用超类型通配符。这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定 <?super MyClass> ,或者甚至使用类型参数: <?super T>(尽管你不能对泛型参数给出一个超类型边界;即不能声明 <T super MyClass> )。这使得你可以安全地传递一个类型对象到泛型类型中。因此,有了超类型通配符,就可以向 Collection 写入了:

java 复制代码
import java.util.*;

public class SuperTypeWildcards {
    static void writeTo(List<? super Apple> apples) {
        apples.add(new Apple());
        apples.add(new Jonathan());
        // apples.add(new Fruit()); // Error
    }
}

参数 applesApple 的某种基类型的 List ,这样你就知道向其中添加 AppleApple 的子类型是安全的。但是因为 Apple 是下界,所以你知道向这样的 List 中添加 Fruit 是不安全的,因为这将使这个 List 敞开口子,从而可以向其中添加非 Apple 类型的对象,而这是违反静态类型安全的。

下面的示例复习了一下逆变和通配符的的使用:

java 复制代码
import java.util.*;

public class GenericReading {
    static List<Apple> apples = Arrays.asList(new Apple());
    static List<Fruit> fruit = Arrays.asList(new Fruit());

    static <T> T readExact(List<T> list) {
        return list.get(0);
    }

    // A static method adapts to each call:
    static void f1() {
        Apple a = readExact(apples);
        Fruit f = readExact(fruit);
        f = readExact(apples);
    }

    // A class type is established
    // when the class is instantiated:
    static class Reader<T> {
        T readExact(List<T> list) {
            return list.get(0);
        }
    }

    static void f2() {
        Reader<Fruit> fruitReader = new Reader<>();
        Fruit f = fruitReader.readExact(fruit);
        //- Fruit a = fruitReader.readExact(apples);
        // error: incompatible types: List<Apple>
        // cannot be converted to List<Fruit>
    }

    static class CovariantReader<T> {
        T readCovariant(List<? extends T> list) {
            return list.get(0);
        }
    }

    static void f3() {
        CovariantReader<Fruit> fruitReader = new CovariantReader<>();
        Fruit f = fruitReader.readCovariant(fruit);
        Fruit a = fruitReader.readCovariant(apples);
    }

    public static void main(String[] args) {
        f1();
        f2();
        f3();
    }
}

readExact() 方法使用了精确的类型。如果使用这个没有任何通配符的精确类型,就可以向 List 中写入和读取这个精确类型。另外,对于返回值,静态的泛型方法 readExact() 可以有效地"适应"每个方法调用,并能够从 List<Apple> 中返回一个 Apple ,从 List<Fruit> 中返回一个 Fruit ,就像在 f1() 中看到的那样。因此,如果可以摆脱静态泛型方法,那么在读取时就不需要协变类型了。

然而对于泛型类来说,当你创建这个类的实例时,就要为这个类确定参数。就像在 f2() 中看到的,fruitReader 实例可以从 List<Fruit> 中读取一个 Fruit ,因为这就是它的确切类型。但是 List<Apple> 也应该产生 Fruit 对象,而 fruitReader 不允许这么做。

为了修正这个问题,CovariantReader.readCovariant() 方法将接受 List<?extends T> ,因此,从这个列表中读取一个 T 是安全的(你知道在这个列表中的所有对象至少是一个 T ,并且可能是从 T 导出的某种对象)。在 f3() 中,你可以看到现在可以从 List<Apple> 中读取 Fruit 了。

无界通配符

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

java 复制代码
import java.util.*;

public class UnboundedWildcards1 {
    static List list1;
    static List<?> list2;
    static List<? extends Object> list3;
  
    static void assign1(List list) {
        list1 = list;
        list2 = list;
        //- list3 = list;
        // warning: [unchecked] unchecked conversion
        // list3 = list;
        //         ^
        // required: List<? extends Object>
        // found:    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());
        //- assign3(new ArrayList());
        // warning: [unchecked] unchecked method invocation:
        // method assign3 in class UnboundedWildcards1
        // is applied to given types
        // assign3(new ArrayList());
        //        ^
        // required: List<? extends Object>
        // found: ArrayList
        // warning: [unchecked] unchecked conversion
        // assign3(new ArrayList());
        //         ^
        // required: List<? extends Object>
        // found:    ArrayList
        // 2 warnings
        assign1(new ArrayList<>());
        assign2(new ArrayList<>());
        assign3(new ArrayList<>());
        // Both forms are acceptable as List<?>:
        List<?> wildList = new ArrayList();
        wildList = new ArrayList<>();
        assign1(wildList);
        assign2(wildList);
        assign3(wildList);
    }
}

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

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

java 复制代码
import java.util.*;

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());
        //- assign3(new HashMap());
        // warning: [unchecked] unchecked method invocation:
        // method assign3 in class UnboundedWildcards2
        // is applied to given types
        //     assign3(new HashMap());
        //            ^
        //   required: Map<String,?>
        //   found: HashMap
        // warning: [unchecked] unchecked conversion
        //     assign3(new HashMap());
        //             ^
        //   required: Map<String,?>
        //   found:    HashMap
        // 2 warnings
        assign1(new HashMap<>());
        assign2(new HashMap<>());
        assign3(new HashMap<>());
    }
}

但是,当你拥有的全都是无界通配符时,就像在 Map<?,?> 中看到的那样,编译器看起来就无法将其与原生 Map 区分开了。另外, UnboundedWildcards1.java 展示了编译器处理 List<?>List<? extends Object> 是不同的。

令人困惑的是,编译器并非总是关注像 ListList<?> 之间的这种差异,因此它们看起来就像是相同的事物。事实上,因为泛型参数擦除到它的第一个边界,因此 List<?> 看起来等价于 List<Object> ,而 List 实际上也是 List<Object> ------除非这些语句都不为真。List 实际上表示"持有任何 Object 类型的原生 List ",而 List<?> 表示"具有某种特定类型的非原生 List ,只是我们不知道类型是什么。"

编译器何时才会关注原生类型和涉及无界通配符的类型之间的差异呢?下面的示例使用了前面定义的 Holder<T> 类,它包含接受 Holder 作为参数的各种方法,但是它们具有不同的形式:作为原生类型,具有具体的类型参数以及具有无界通配符参数:

java 复制代码
public class Wildcards {
    // Raw argument:
    static void rawArgs(Holder holder, Object arg) {
        //- holder.set(arg);
        // warning: [unchecked] unchecked call to set(T)
        // as a member of the raw type Holder
        //     holder.set(arg);
        //               ^
        //   where T is a type-variable:
        //     T extends Object declared in class Holder
        // 1 warning

        // Can't do this; don't have any 'T':
        // T t = holder.get();

        // OK, but type information is lost:
        Object obj = holder.get();
    }

    // Like rawArgs(), but errors instead of warnings:
    static void unboundedArg(Holder<?> holder, Object arg) {
        //- holder.set(arg);
        // error: method set in class Holder<T>
        // cannot be applied to given types;
        //     holder.set(arg);
        //           ^
        //   required: CAP#1
        //   found: Object
        //   reason: argument mismatch;
        //     Object cannot be converted to CAP#1
        //   where T is a type-variable:
        //     T extends Object declared in class Holder
        //   where CAP#1 is a fresh type-variable:
        //     CAP#1 extends Object from capture of ?
        // 1 error

        // Can't do this; don't have any 'T':
        // T t = holder.get();

        // OK, but type information is lost:
        Object obj = holder.get();
    }

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

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

    static <T> T wildSubtype(Holder<? extends T> holder, T arg) {
        //- holder.set(arg);
        // error: method set in class Holder<T#2>
        // cannot be applied to given types;
        //     holder.set(arg);
        //           ^
        //   required: CAP#1
        //   found: T#1
        //   reason: argument mismatch;
        //     T#1 cannot be converted to CAP#1
        //   where T#1,T#2 are type-variables:
        //     T#1 extends Object declared in method
        //     <T#1>wildSubtype(Holder<? extends T#1>,T#1)
        //     T#2 extends Object declared in class Holder
        //   where CAP#1 is a fresh type-variable:
        //     CAP#1 extends T#1 from
        //       capture of ? extends T#1
        // 1 error
        return holder.get();
    }

    static <T> void wildSupertype(Holder<? super T> holder, T arg) {
        holder.set(arg);
        //- T t = holder.get();
        // error: incompatible types:
        // CAP#1 cannot be converted to T
        //     T t = holder.get();
        //                     ^
        //   where T is a type-variable:
        //     T extends Object declared in method
        //       <T>wildSupertype(Holder<? super T>,T)
        //   where CAP#1 is a fresh type-variable:
        //     CAP#1 extends Object super:
        //       T from capture of ? super T
        // 1 error

        // OK, but type information is lost:
        Object obj = holder.get();
    }

    public static void main(String[] args) {
        Holder raw = new Holder<>();
        // Or:
        raw = new Holder();
        Holder<Long> qualified = new Holder<>();
        Holder<?> unbounded = new Holder<>();
        Holder<? extends Long> bounded = new Holder<>();
        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);

        //- Object r1 = exact1(raw);
        // warning: [unchecked] unchecked method invocation:
        // method exact1 in class Wildcards is applied
        // to given types
        //      Object r1 = exact1(raw);
        //                        ^
        //   required: Holder<T>
        //   found: Holder
        //   where T is a type-variable:
        //     T extends Object declared in
        //     method <T>exact1(Holder<T>)
        // warning: [unchecked] unchecked conversion
        //      Object r1 = exact1(raw);
        //                         ^
        //   required: Holder<T>
        //   found:    Holder
        //   where T is a type-variable:
        //     T extends Object declared in
        //     method <T>exact1(Holder<T>)
        // 2 warnings

        Long r2 = exact1(qualified);
        Object r3 = exact1(unbounded); // Must return Object
        Long r4 = exact1(bounded);

        //- Long r5 = exact2(raw, lng);
        // warning: [unchecked] unchecked method invocation:
        // method exact2 in class Wildcards is
        // applied to given types
        //     Long r5 = exact2(raw, lng);
        //                     ^
        //   required: Holder<T>,T
        //   found: Holder,Long
        //   where T is a type-variable:
        //     T extends Object declared in
        //       method <T>exact2(Holder<T>,T)
        // warning: [unchecked] unchecked conversion
        //     Long r5 = exact2(raw, lng);
        //                      ^
        //   required: Holder<T>
        //   found:    Holder
        //   where T is a type-variable:
        //     T extends Object declared in
        //       method <T>exact2(Holder<T>,T)
        // 2 warnings

        Long r6 = exact2(qualified, lng);

        //- Long r7 = exact2(unbounded, lng);
        // error: method exact2 in class Wildcards
        // cannot be applied to given types;
        //     Long r7 = exact2(unbounded, lng);
        //               ^
        //   required: Holder<T>,T
        //   found: Holder<CAP#1>,Long
        //   reason: inference variable T has
        //     incompatible bounds
        //     equality constraints: CAP#1
        //     lower bounds: Long
        //   where T is a type-variable:
        //     T extends Object declared in
        //       method <T>exact2(Holder<T>,T)
        //   where CAP#1 is a fresh type-variable:
        //     CAP#1 extends Object from capture of ?
        // 1 error

        //- Long r8 = exact2(bounded, lng);
        // error: method exact2 in class Wildcards
        // cannot be applied to given types;
        //      Long r8 = exact2(bounded, lng);
        //                ^
        //   required: Holder<T>,T
        //   found: Holder<CAP#1>,Long
        //   reason: inference variable T
        //     has incompatible bounds
        //     equality constraints: CAP#1
        //     lower bounds: Long
        //   where T is a type-variable:
        //     T extends Object declared in
        //       method <T>exact2(Holder<T>,T)
        //   where CAP#1 is a fresh type-variable:
        //     CAP#1 extends Long from
        //       capture of ? extends Long
        // 1 error

        //- Long r9 = wildSubtype(raw, lng);
        // warning: [unchecked] unchecked method invocation:
        // method wildSubtype in class Wildcards
        // is applied to given types
        //     Long r9 = wildSubtype(raw, lng);
        //                          ^
        //   required: Holder<? extends T>,T
        //   found: Holder,Long
        //   where T is a type-variable:
        //     T extends Object declared in
        //     method <T>wildSubtype(Holder<? extends T>,T)
        // warning: [unchecked] unchecked conversion
        //     Long r9 = wildSubtype(raw, lng);
        //                           ^
        //   required: Holder<? extends T>
        //   found:    Holder
        //   where T is a type-variable:
        //     T extends Object declared in
        //     method <T>wildSubtype(Holder<? extends T>,T)
        // 2 warnings

        Long r10 = wildSubtype(qualified, lng);
        // OK, but can only return Object:
        Object r11 = wildSubtype(unbounded, lng);
        Long r12 = wildSubtype(bounded, lng);

        //- wildSupertype(raw, lng);
        // warning: [unchecked] unchecked method invocation:
        //   method wildSupertype in class Wildcards
        //   is applied to given types
        //     wildSupertype(raw, lng);
        //                  ^
        //   required: Holder<? super T>,T
        //   found: Holder,Long
        //   where T is a type-variable:
        //     T extends Object declared in
        //     method <T>wildSupertype(Holder<? super T>,T)
        // warning: [unchecked] unchecked conversion
        //     wildSupertype(raw, lng);
        //                   ^
        //   required: Holder<? super T>
        //   found:    Holder
        //   where T is a type-variable:
        //     T extends Object declared in
        //     method <T>wildSupertype(Holder<? super T>,T)
        // 2 warnings

        wildSupertype(qualified, lng);

        //- wildSupertype(unbounded, lng);
        // error: method wildSupertype in class Wildcards
        // cannot be applied to given types;
        //     wildSupertype(unbounded, lng);
        //     ^
        //   required: Holder<? super T>,T
        //   found: Holder<CAP#1>,Long
        //   reason: cannot infer type-variable(s) T
        //     (argument mismatch; Holder<CAP#1>
        //     cannot be converted to Holder<? super T>)
        //   where T is a type-variable:
        //     T extends Object declared in
        //     method <T>wildSupertype(Holder<? super T>,T)
        //   where CAP#1 is a fresh type-variable:
        //     CAP#1 extends Object from capture of ?
        // 1 error

        //- wildSupertype(bounded, lng);
        // error: method wildSupertype in class Wildcards
        // cannot be applied to given types;
        //     wildSupertype(bounded, lng);
        //     ^
        //   required: Holder<? super T>,T
        //   found: Holder<CAP#1>,Long
        //   reason: cannot infer type-variable(s) T
        //     (argument mismatch; Holder<CAP#1>
        //     cannot be converted to Holder<? super T>)
        //   where T is a type-variable:
        //     T extends Object declared in
        //     method <T>wildSupertype(Holder<? super T>,T)
        //   where CAP#1 is a fresh type-variable:
        //     CAP#1 extends Long from capture of
        //     ? extends Long
        // 1 error
    }
}

rawArgs() 中,编译器知道 Holder 是一个泛型类型,因此即使它在这里被表示成一个原生类型,编译器仍旧知道向 set() 传递一个 Object 是不安全的。由于它是原生类型,你可以将任何类型的对象传递给 set() ,而这个对象将被向上转型为 Object 。因此无论何时,只要使用了原生类型,都会放弃编译期检查。对 get() 的调用说明了相同的问题:没有任何 T 类型的对象,因此结果只能是一个 Object

人们很自然地会开始考虑原生 HolderHolder<?> 是大致相同的事物。但是 unboundedArg() 强调它们是不同的------它揭示了相同的问题,但是它将这些问题作为错误而不是警告报告,因为原生 Holder 将持有任何类型的组合,而 Holder<?> 将持有具有某种具体类型的同构集合,因此不能只是向其中传递 Object

exact1()exact2() 中,你可以看到使用了确切的泛型参数------没有任何通配符。你将看到,exact2()exact1() 具有不同的限制,因为它有额外的参数。

wildSubtype() 中,在 Holder 类型上的限制被放松为包括持有任何扩展自 T 的对象的 Holder 。这还是意味着如果 T 是 Fruit ,那么 holder 可以是 Holder<Apple> ,这是合法的。为了防止将 Orange 放置到 Holder<Apple> 中,对 set() 的调用(或者对任何接受这个类型参数为参数的方法的调用)都是不允许的。但是,你仍旧知道任何来自 Holder<?extends Fruit> 的对象至少是 Fruit ,因此 get() (或者任何将产生具有这个类型参数的返回值的方法)都是允许的。

wildSupertype() 展示了超类型通配符,这个方法展示了与 wildSubtype() 相反的行为:holder 可以是持有任何 T 的基类型的容器。因此, set() 可以接受 T ,因为任何可以工作于基类的对象都可以多态地作用于导出类(这里就是 T )。但是,尝试着调用 get() 是没有用的,因为由 holder 持有的类型可以是任何超类型,因此唯一安全的类型就是 Object

这个示例还展示了对于在 unbounded() 中使用无界通配符能够做什么不能做什么所做出的限制:因为你没有 T ,所以你不能将 set()get() 作用于 T 上。

main() 方法中你看到了某些方法在接受某些类型的参数时没有报错和警告。为了迁移兼容性,rawArgs() 将接受所有 Holder 的不同变体,而不会产生警告。unboundedArg() 方法也可以接受相同的所有类型,尽管如前所述,它在方法体内部处理这些类型的方式并不相同。

如果向接受"确切"泛型类型(没有通配符)的方法传递一个原生 Holder 引用,就会得到一个警告,因为确切的参数期望得到在原生类型中并不存在的信息。如果向 exact1() 传递一个无界引用,就不会有任何可以确定返回类型的类型信息。

可以看到,exact2() 具有最多的限制,因为它希望精确地得到一个 Holder<T> ,以及一个具有类型 T 的参数,正由于此,它将产生错误或警告,除非提供确切的参数。有时,这样做很好,但是如果它过于受限,那么就可以使用通配符,这取决于是否想要从泛型参数中返回类型确定的返回值(就像在 wildSubtype() 中看到的那样),或者是否想要向泛型参数传递类型确定的参数(就像在 wildSupertype() 中看到的那样)。

因此,使用确切类型来替代通配符类型的好处是,可以用泛型参数来做更多的事,但是使用通配符使得你必须接受范围更宽的参数化类型作为参数。因此,必须逐个情况地权衡利弊,找到更适合你的需求的方法。

捕获转换

有一种特殊情况需要使用 <?> 而不是原生类型。如果向一个使用 <?> 的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法。下面的示例演示了这种技术,它被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型。这里,有关警告的注释只有在 @SuppressWarnings 注解被移除之后才能起作用:

java 复制代码
public class CaptureConversion {
    static <T> void f1(Holder<T> holder) {
        T t = holder.get();
        System.out.println(t.getClass().getSimpleName());
    }

    static void f2(Holder<?> holder) {
        f1(holder); // Call with captured type
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Holder raw = new Holder<>(1);
        f1(raw);
        // warning: [unchecked] unchecked method invocation:
        // method f1 in class CaptureConversion
        // is applied to given types
        //     f1(raw);
        //       ^
        //   required: Holder<T>
        //   found: Holder
        //   where T is a type-variable:
        //     T extends Object declared in
        //     method <T>f1(Holder<T>)
        // warning: [unchecked] unchecked conversion
        //     f1(raw);
        //        ^
        //   required: Holder<T>
        //   found:    Holder
        //   where T is a type-variable:
        //     T extends Object declared in
        //     method <T>f1(Holder<T>)
        // 2 warnings
        f2(raw); // No warnings

        Holder rawBasic = new Holder();
        rawBasic.set(new Object());
        // warning: [unchecked] unchecked call to set(T)
        // as a member of the raw type Holder
        //     rawBasic.set(new Object());
        //                 ^
        //   where T is a type-variable:
        //     T extends Object declared in class Holder
        // 1 warning
        f2(rawBasic); // No warnings

        // Upcast to Holder<?>, still figures it out:
        Holder<?> wildcarded = new Holder<>(1.0);
        f2(wildcarded);
    }
}

f1() 中的类型参数都是确切的,没有通配符或边界。在 f2() 中,Holder 参数是一个无界通配符,因此它看起来是未知的。但是,在 f2() 中调用了 f1(),而 f1() 需要一个已知参数。这里所发生的是:在调用 f2() 的过程中捕获了参数类型,并在调用 f1() 时使用了这种类型。

你可能想知道这项技术是否可以用于写入,但是这要求在传递 Holder<?> 时同时传递一个具体类型。捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。注意,不能从 f2() 中返回 T ,因为 T 对于 f2() 来说是未知的。捕获转换十分有趣,但是非常受限。

相关推荐
怒放de生命20102 个月前
cerbot 实现通配符子域名证书+续期
证书·ssl·通配符·cerbot
攸攸太上3 个月前
Java通配符的作用
java·学习·通配符
网络研究院3 个月前
无限边界:现代整合安全如何保护云
网络·云计算·云安全·零信任·远程·边界·工作
zzzzzzzz'4 个月前
浅谈数据结构
java·数据结构·装箱·泛型·通配符·拆箱·上界下界
whoispo7 个月前
通过QT自动生成的gitignore文件来说明用法
开发语言·qt·gitignore·通配符·忽略规则
微小冷9 个月前
C#协变与逆变:解锁高级编程技巧,轻松提升代码性能
开发语言·c#·逆变·协变·里氏替换
闻缺陷则喜何志丹1 年前
【动态规划】C++算法:44 通配符匹配
c++·算法·leetcode·动态规划·字符串·dp·通配符
此星光明1 年前
全球国家行政区划边界(中国科学院地理科学与资源研究所)
大数据·javascript·人工智能·云计算·pie·矢量·边界
周湘zx1 年前
Linux 常用通配符
linux·运维·通配符