在 Java 泛型是不变的?
In Java Generics are invariant?
编译以下代码失败:
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
喜欢:
Swap.java:5: set(int,capture#282 of ?) in List<capture#282 of ?> cannot be applied to (int,Object)
list.set(i, list.set(j, list.get(i)));
但是如果我这样做:
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
private static <E> void swapHelper(List<E> list, int i, int j) { list.set(i, list.set(j, list.get(i)));
}
它运行良好。
但我在这里有一个基本的疑问。据说泛型是不变的,所以 List<String>
不是 List<Object>
的子类型,对吗?
如果是这样的话,那上面的方法怎么能把List<?>
传给List<E>
呢?这是如何工作的?
答案是通配符。
List<?>
与 List<Object>
不同
而 java 假定两者都是 Object 的集合,前者将匹配任何其他 List<T>
的类型,而后者将不匹配除 List<Object>
之外的任何 List<T>
的类型。
示例:
public void doSomething(List<Object> list);
此函数将仅接受 List<Object>
作为参数。然而这个:
public void doSomething(List<?> list);
将接受任何 List<T>
作为参数。
这在与 generic constraints 一起使用时非常有用。例如,如果您想编写一个操作数字(Integer
s、Float
s 等)的函数,您可以:
public void doSomethingToNumbers(List<? extends Number> numbers) { ... }
因为您正在使用wildcard捕获。
In some cases, the compiler infers the type of a wildcard. For
example, a list may be defined as List but, when evaluating an
expression, the compiler infers a particular type from the code. This
scenario is known as wildcard capture.
多亏了辅助方法,编译器在调用中使用推理来确定捕获变量 T。
List<?>
表示 "list of an unknown type"。你可以为它传递一个 List<String>
,不是因为它是一个子类(它不是),而是因为 "unknown type" 应该接受你扔给它的任何东西。
因为类型未知,你不能用它执行操作,需要知道元素的类型(如 set
或 add
等)。
List<E>
是泛型。它与 List<?>
不同,尽管它看起来有点相似。
正如您所指出的,一个区别是您可以对其执行 set
或 add
之类的操作,因为元素的类型现在已知。
作为函数参数的通配符不是很有用(<E> void foo(List<E> l)
与 void foo(List<?> l)
差别不大)。它们还绕过了所有编译时类型检查,从而违背了泛型的目的,确实应该避免。
通配符更常见(且危害较小)的用法是在 return 类型中:List<? extends DataIterface> getData()
表示 getData
return 是一些实现的对象的列表a DataInterface
,但不会告诉你它们的具体类型。这通常在 API 设计中完成,以将实现细节与接口隔离开来。通常的约定是,您可以将对象列表 return 由 API 编辑到其他 API 方法,它们将接受并处理它们。这个概念叫做existential types
。
此外,请注意上面示例中的 getData
可以 return 不同类型的列表,具体取决于某些条件,即在调用方域之外,这与声明为 return List<E>
,在这种情况下,调用者必须以某种方式指定预期的类型。
编译以下代码失败:
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
喜欢:
Swap.java:5: set(int,capture#282 of ?) in List<capture#282 of ?> cannot be applied to (int,Object)
list.set(i, list.set(j, list.get(i)));
但是如果我这样做:
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
private static <E> void swapHelper(List<E> list, int i, int j) { list.set(i, list.set(j, list.get(i)));
}
它运行良好。
但我在这里有一个基本的疑问。据说泛型是不变的,所以 List<String>
不是 List<Object>
的子类型,对吗?
如果是这样的话,那上面的方法怎么能把List<?>
传给List<E>
呢?这是如何工作的?
答案是通配符。
List<?>
与 List<Object>
不同
而 java 假定两者都是 Object 的集合,前者将匹配任何其他 List<T>
的类型,而后者将不匹配除 List<Object>
之外的任何 List<T>
的类型。
示例:
public void doSomething(List<Object> list);
此函数将仅接受 List<Object>
作为参数。然而这个:
public void doSomething(List<?> list);
将接受任何 List<T>
作为参数。
这在与 generic constraints 一起使用时非常有用。例如,如果您想编写一个操作数字(Integer
s、Float
s 等)的函数,您可以:
public void doSomethingToNumbers(List<? extends Number> numbers) { ... }
因为您正在使用wildcard捕获。
In some cases, the compiler infers the type of a wildcard. For example, a list may be defined as List but, when evaluating an expression, the compiler infers a particular type from the code. This scenario is known as wildcard capture.
多亏了辅助方法,编译器在调用中使用推理来确定捕获变量 T。
List<?>
表示 "list of an unknown type"。你可以为它传递一个 List<String>
,不是因为它是一个子类(它不是),而是因为 "unknown type" 应该接受你扔给它的任何东西。
因为类型未知,你不能用它执行操作,需要知道元素的类型(如 set
或 add
等)。
List<E>
是泛型。它与 List<?>
不同,尽管它看起来有点相似。
正如您所指出的,一个区别是您可以对其执行 set
或 add
之类的操作,因为元素的类型现在已知。
作为函数参数的通配符不是很有用(<E> void foo(List<E> l)
与 void foo(List<?> l)
差别不大)。它们还绕过了所有编译时类型检查,从而违背了泛型的目的,确实应该避免。
通配符更常见(且危害较小)的用法是在 return 类型中:List<? extends DataIterface> getData()
表示 getData
return 是一些实现的对象的列表a DataInterface
,但不会告诉你它们的具体类型。这通常在 API 设计中完成,以将实现细节与接口隔离开来。通常的约定是,您可以将对象列表 return 由 API 编辑到其他 API 方法,它们将接受并处理它们。这个概念叫做existential types
。
此外,请注意上面示例中的 getData
可以 return 不同类型的列表,具体取决于某些条件,即在调用方域之外,这与声明为 return List<E>
,在这种情况下,调用者必须以某种方式指定预期的类型。