二十、泛型(5)
本章概要
- 边界
- 通配符
- 编译器有多聪明
- 逆变
- 无界通配符
- 捕获转换
边界
边界(bounds)在本章的前面进行了简要介绍。边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在绑定的类型中调用方法。
由于擦除会删除类型信息,因此唯一可用于无限制泛型参数的方法是那些 Object 可用的方法。但是,如果将该参数限制为某类型的子集,则可以调用该子集中的方法。为了应用约束,Java 泛型使用了 extends
关键字。
重要的是要理解,当用于限定泛型类型时,extends
的含义与通常的意义截然不同。此示例展示边界的基础应用:
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 Boundedextends Coord implements HasColor, Weight {@Overridepublic java.awt.Color getColor() {return null;}@Overridepublic 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 中似乎包含一些冗余,它们可以通过继承来消除。在这里,每个继承级别还添加了边界约束:
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 中,这也需要其参数符合 HasColor。 WithColorCoord2 和 Solid2 进一步扩展了层次结构,并在每个级别添加了边界。现在,这些方法已被继承,并且在每个类中不再重复。
这是一个具有更多层次的示例:
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 SuperHearSmellimplements SuperHearing, SuperSmell {@Overridepublic void hearSubtleNoises() {}@Overridepublic 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;}
}
接下来将要研究的通配符将会把范围限制在单个类型。
通配符
你已经在 集合
章节中看到了一些简单示例使用了通配符——在泛型参数表达式中的问号,在 类型信息
一章中这种示例更多。本节将更深入地探讨这个特性。
我们的起始示例要展示数组的一种特殊行为:你可以将派生类的数组赋值给基类的引用:
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(); // OKfruit[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[],你可以在其中放置 Apple 或 Apple 的子类型,这在编译期和运行时都可以工作。但是你也可以在数组中放置 Fruit 对象。这对编译器来说是有意义的,因为它有一个 Fruit[] 引用——它有什么理由不允许将 Fruit 对象或任何从 Fruit 继承出来的对象(比如 Orange),放置到这个数组中呢?因此在编译期,这是允许的。然而,运行时的数组机制知道它处理的是 Apple[],因此会在向数组中放置异构类型时抛出异常。
向上转型用在这里不合适。你真正在做的是将一个数组赋值给另一个数组。数组的行为是持有其他对象,这里只是因为我们能够向上转型而已,所以很明显,数组对象可以保留有关它们包含的对象类型的规则。看起来就像数组对它们持有的对象是有意识的,因此在编译期检查和运行时检查之间,你不能滥用它们。
数组的这种赋值并不是那么可怕,因为在运行时你可以发现插入了错误的类型。但是泛型的主要目标之一是将这种错误检测移到编译期。所以当我们试图使用泛型集合代替数组时,会发生什么呢?
import java.util.*;public class NonCovariantGenerics {// Compile Error: incompatible types:List<Fruit> flist = new ArrayList<Apple>();
}
尽管你在首次阅读这段代码时会认为“不能将一个 Apple 集合赋值给一个 Fruit 集合”。记住,泛型不仅仅是关于集合,它真正要表达的是“不能把一个涉及 Apple 的泛型赋值给一个涉及 Fruit 的泛型”。如果像在数组中的情况一样,编译器对代码的了解足够多,可以确定所涉及到的集合,那么它可能会留下一些余地。
但是它不知道任何有关这方面的信息,因此它拒绝向上转型。然而实际上这也不是向上转型—— Apple 的 List 不是 Fruit 的 List。Apple 的 List 将持有 Apple 和 Apple 的子类型,Fruit 的 List 将持有任何类型的 Fruit。是的,这包括 Apple,但是它不是一个 Apple 的 List,它仍然是 Fruit 的 List。Apple 的 List 在类型上不等价于 Fruit 的 List,即使 Apple 是一种 Fruit 类型。
真正的问题是我们在讨论的集合类型,而不是集合持有对象的类型。与数组不同,泛型没有内建的协变类型。这是因为数组是完全在语言中定义的,因此可以具有编译期和运行时的内建检查,但是在使用泛型时,编译器和运行时系统不知道你想用类型做什么,以及应该采用什么规则。
但是,有时你想在两个类型间建立某种向上转型关系。通配符可以产生这种关系。
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 必须持有诸如 Fruit 或 Apple 这样的指定类型,但是为了向上转型为 Fruit,这个类型是什么没人在意。
List 必须持有一种具体的 Fruit 或 Fruit 的子类型,但是如果你不关心具体的类型是什么,那么你能对这样的 List 做什么呢?如果不知道 List 中持有的对象是什么类型,你怎能保证安全地向其中添加对象呢?就像在 CovariantArrays.java 中向上转型一样,你不能,除非编译器而不是运行时系统可以阻止这种操作的发生。你很快就会发现这个问题。
你可能认为事情开始变得有点走极端了,因为现在你甚至不能向刚刚声明过将持有 Apple 对象的 List 中放入一个 Apple 对象。是的,但编译器并不知道这一点。List<? extends Fruit>
可能合法地指向一个 List<Orange>
。一旦执行这种类型的向上转型,你就丢失了向其中传递任何对象的能力,甚至传递 Object 也不行。
另一方面,如果你调用了一个返回 Fruit 的方法,则是安全的,因为你知道这个 List 中的任何对象至少具有 Fruit 类型,因此编译器允许这么做。
编译器有多聪明
现在你可能会猜想自己不能去调用任何接受参数的方法,但是考虑下面的代码:
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 warningflist.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 类:
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;}@Overridepublic boolean equals(Object o) {return o instanceof Holder && Objects.equals(value, ((Holder) o).value);}@Overridepublic 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 upcastHolder<? extends Fruit> fruit = apple; // OKFruit 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()
方法不能工作在 Apple 和 Fruit 上,因为 set()
的参数也是"? extends Fruit",意味着它可以是任何事物,编译器无法验证“任何事物”的类型安全性。
但是,equals()
方法可以正常工作,因为它接受的参数是 Object 而不是 T 类型。因此,编译器只关注传递进来和要返回的对象类型。它不会分析代码,以查看是否执行了任何实际的写入和读取操作。
Java 7 引入了 java.util.Objects 库,使创建 equals()
和 hashCode()
方法变得更加容易,当然还有很多其他功能。
逆变
还可以走另外一条路,即使用超类型通配符。这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定 <?super MyClass>
,或者甚至使用类型参数: <?super T>
(尽管你不能对泛型参数给出一个超类型边界;即不能声明 <T super MyClass>
)。这使得你可以安全地传递一个类型对象到泛型类型中。因此,有了超类型通配符,就可以向 Collection 写入了:
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}
}
参数 apples 是 Apple 的某种基类型的 List,这样你就知道向其中添加 Apple 或 Apple 的子类型是安全的。但是因为 Apple 是下界,所以你知道向这样的 List 中添加 Fruit 是不安全的,因为这将使这个 List 敞开口子,从而可以向其中添加非 Apple 类型的对象,而这是违反静态类型安全的。
下面的示例复习了一下逆变和通配符的的使用:
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 了。
无界通配符
无界通配符 <?>
看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。事实上,编译器初看起来是支持这种判断的:
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 warningsassign1(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 的泛型来编写这段代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。”
第二个示例展示了无界通配符的一个重要应用。当你在处理多个泛型参数时,有时允许一个参数可以是任何类型,同时为其他参数确定某种特定类型的这种能力会显得很重要:
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 warningsassign1(new HashMap<>());assign2(new HashMap<>());assign3(new HashMap<>());}
}
但是,当你拥有的全都是无界通配符时,就像在 Map<?,?>
中看到的那样,编译器看起来就无法将其与原生 Map 区分开了。另外, UnboundedWildcards1.java 展示了编译器处理 List<?>
和 List<? extends Object>
是不同的。
令人困惑的是,编译器并非总是关注像 List
和 List<?>
之间的这种差异,因此它们看起来就像是相同的事物。事实上,因为泛型参数擦除到它的第一个边界,因此 List<?>
看起来等价于 List<Object>
,而 List 实际上也是 List<Object>
——除非这些语句都不为真。List 实际上表示“持有任何 Object 类型的原生 List ”,而 List<?>
表示“具有某种特定类型的非原生 List ,只是我们不知道类型是什么。”
编译器何时才会关注原生类型和涉及无界通配符的类型之间的差异呢?下面的示例使用了前面定义的 Holder<T>
类,它包含接受 Holder 作为参数的各种方法,但是它们具有不同的形式:作为原生类型,具有具体的类型参数以及具有无界通配符参数:
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 errorreturn 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 warningsLong r2 = exact1(qualified);Object r3 = exact1(unbounded); // Must return ObjectLong 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 warningsLong 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 warningsLong 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 warningswildSupertype(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。
人们很自然地会开始考虑原生 Holder
与 Holder<?>
是大致相同的事物。但是 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
注解被移除之后才能起作用:
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 warningsf2(raw); // No warningsHolder 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 warningf2(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()
来说是未知的。捕获转换十分有趣,但是非常受限。
相关文章:

二十、泛型(5)
本章概要 边界通配符 编译器有多聪明逆变无界通配符捕获转换 边界 边界(bounds)在本章的前面进行了简要介绍。边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在…...

Oracle递归查询树形数据
实际生活有很多树形结构的数据,比如公司分为多个部门、部门下分为多个组,组下分为多个员工;省市县的归属;页面菜单栏等等。 如果想查询某个节点的父节点或者子节点,一般通过表自身连接完成,但如果该节点的子…...

pycharm pro v2023.2.4(Python开发)
PyCharm是一种Python集成开发环境(IDE),PyCharm提供了强大的功能,包括语法突出显示、智能代码完成、代码检查、自动重构和调试等特性,这些都可以帮助Python开发人员更加高效地编写代码。 PyCharm Pro是PyCharm的高级版…...

武汉某母婴用品公司 - 集简云连接ERP和营销系统,实现库存管理的自动化
品牌介绍与关怀理念 武汉某母婴用品公司是一家专注于高端孕婴童护理用品的企业,积极响应和关怀孕产人群,全方位提供从待产用品到产后护理用品,再到婴童洗护用品和初生婴儿用品等一系列全面的母婴产品。我们的使命是满足客户的需求࿰…...

AD9371 Crossbar
AD9371 系列快速入口 AD9371ZCU102 移植到 ZCU106 : AD9371 官方例程构建及单音信号收发 ad9371_tx_jesd -->util_ad9371_xcvr接口映射: AD9371 官方例程之 tx_jesd 与 xcvr接口映射 AD9371 官方例程 时钟间的关系与生成 : AD9371 官方…...
设计模式 -- 责任链模式(Chain of Responsibility Pattern)
责任链模式:和上一篇策略模式一样也是行为型模式。创建一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它…...

开源:特殊的垄断
免责声明:本博客旨在分享我对开源策略的理解和体会,不代表任何组织或机构的立场或观点,也不构成任何商业或投资的建议或担保。本博客的内容可能存在错误或遗漏,也可能随着时间的推移而变得过时或不适用。请在使用或依赖本博客的内…...
linux安装oracle client解决cx_Oracle.DatabaseError: DPI-1047
背景: 需要在linux上调用python,连接oracle数据,在执行脚本是出现如下错误: 由于未安装client,所有找不到对应的文件 [root@xx wxArticle]# python main.py Traceback (most recent call last):File "main.py", line 16, in <module>oracle_db = cx_Or…...

工业园区一般用多大规格的电表?
随着我国经济的快速发展,工业园区在各地区如雨后春笋般崛起。作为电力系统的重要组成部分,电表的选择与应用对于工业园区的稳定运行至关重要。那么,工业园区一般用的是多大规格的电表呢?下面,小编就来给大家揭秘一下&a…...

Debian12换镜像源
0 背景 用docker运行了一个node容器,发现连vim也没有,所以打算安一个vim 1 查看操作系统 find / -name *release* #查看release信息2 更换镜像源 2.1 从网上找个国内镜像源 确定好操作系统版本后,从网上搜一下对应的数据源。这里提供一个…...

deeplog中输出某个 event 的概率
1 实现之后效果 # import DeepLog and Preprocessor import numpy as np from deeplog import DeepLog import torch# Create DeepLog object deeplog DeepLog(input_size 10, # Number of different events to expecthidden_size 64 , # Hidden dimension, we suggest 64…...
流媒体中relay和forword的区别
在流媒体中,"relay"(中继)和 "forward"(转发)是两种常见的传输方式,用于将流媒体从一个源传递到目标接收器。它们的区别如下: Relay(中继)ÿ…...
git创建本地分支并提交到远程
创建本地分支 git branch local_branch_name切换到本地分支 git checkout local_branch_name提交代码 git add . git commit -m "commit_message"添加远程信息 git remote add origin remote_git_address提交到远程 git push origin local_branch_name:remote_b…...

Selenium定位元素的方法css和xpath的区别
selenium是一种自动化测试工具,它可以通过不同的定位方式来识别网页上的元素,如id、name、class、tag、link text、partial link text、css和xpath。 如果对软件测试、接口、自动化、性能测试、测试开发、面试经验交流。感兴趣可以加裙485187702&#x…...
【目标检测】SSD损失函数详解
文章目录 定位损失 L l o c L_{loc} Lloc偏移值的计算smooth L1 loss 置信率损失 L c o n f L_{conf} Lconf 最近看看这个古早的目标检测网络,看了好多文章,感觉对损失函数的部分讲得都是不很清楚得样子,所以自己捋一下。 首先&#x…...

C【整数正序分解】
// 整数正序分解 #include <stdio.h> #include <stdlib.h>int main() {int x;scanf("%d", &x);// 13425/10000->1(int一个d)// 13425%10000->3425(这是x)// 10000/10-.1000(这是mask)int mask 1;int t x;while (t > 9){t / 10;mask * 10;…...

基于springboot实现高校党务平台管理系统【项目源码】计算机毕业设计
基于springboot实现高校党务平台管理系统演示 Java技术 Java是由Sun公司推出的一门跨平台的面向对象的程序设计语言。因为Java 技术具有卓越的通用性、高效性、健壮的安全性和平台移植性的特点,而且Java是开源的,拥有全世界最大的开发者专业社群&#x…...

Day24力扣打卡
打卡记录 寻找峰值(二分法) class Solution { public:int findPeakElement(vector<int> &nums) {int left -1, right nums.size() - 1; // 开区间 (-1, n-1)while (left 1 < right) { // 开区间不为空int mid left (right - left) / …...

5G-A 商用加速,赋能工业互联网
2019 年 6 月,中国工业和信息化部发放 5G 商用牌照。同年 10 月,三大运营商公布 5G 商用套餐,11 月 1 日正式上线 5G 商用套餐,标志中国正式进入 5G 商用新纪元。今年是 5G 商用的第五年,在当前数字经济蓬勃发展的催化…...

代码随想录day2
目录 vscode 自定义代码模板Reference vscode 自定义代码模板 select User snippets from Settings on the bottom left corner. select a certain language for example: cpp create your own snippets 格式如下,防着写 第一行"cpp template",模板…...

C++_核心编程_多态案例二-制作饮品
#include <iostream> #include <string> using namespace std;/*制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料 利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶*//*基类*/ class AbstractDr…...
rknn优化教程(二)
文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK,开始写第二篇的内容了。这篇博客主要能写一下: 如何给一些三方库按照xmake方式进行封装,供调用如何按…...

【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...

shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...

定时器任务——若依源码分析
分析util包下面的工具类schedule utils: ScheduleUtils 是若依中用于与 Quartz 框架交互的工具类,封装了定时任务的 创建、更新、暂停、删除等核心逻辑。 createScheduleJob createScheduleJob 用于将任务注册到 Quartz,先构建任务的 JobD…...

自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...

九天毕昇深度学习平台 | 如何安装库?
pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子: 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...
人工智能--安全大模型训练计划:基于Fine-tuning + LLM Agent
安全大模型训练计划:基于Fine-tuning LLM Agent 1. 构建高质量安全数据集 目标:为安全大模型创建高质量、去偏、符合伦理的训练数据集,涵盖安全相关任务(如有害内容检测、隐私保护、道德推理等)。 1.1 数据收集 描…...
鸿蒙(HarmonyOS5)实现跳一跳小游戏
下面我将介绍如何使用鸿蒙的ArkUI框架,实现一个简单的跳一跳小游戏。 1. 项目结构 src/main/ets/ ├── MainAbility │ ├── pages │ │ ├── Index.ets // 主页面 │ │ └── GamePage.ets // 游戏页面 │ └── model │ …...