何时在Java Generics中使用通配符?
这是来自HeadFirst Java :(第575页)
这个:
public void takeThing(ArrayList list)
与此相同:
public void takeThing(ArrayList list)
所以这是我的问题:如果它们完全相同,我们为什么不写
public void takeThing(ArrayList list)
要么
public void takeThing(ArrayList list)
此外,何时使用?有用? 使用generics或类声明而不是方法声明中的T(如上所述)? 有什么好处?
两者之间的巨大差异
public void takeThing(ArrayList list)
和
public void takeThing(ArrayList extends Animal> list)
在前一种方法中,您可以将方法中的“T”称为给定的具体类。 在第二种方法中,你不能这样做。
这里有一个更复杂的例子来说明这一点:
// here i can return the concrete type that was passed in public Map getNamesMap(ArrayList list) { Map names = new HashMap(); for (T animal : list) { names.put(animal, animal.getName()); // i assume there is a getName method } return names; } // here i have to use general Animal public Map getNamesMap(ArrayList extends Animal> list) { Map names = new HashMap(); for (Animal animal : list) { names.put(animal, animal.getName()); // i assume there is a getName method } return names; }
使用第一种方法,如果您传入一个猫列表,您将获得一个以猫为主的地图。 第二种方法总是返回带有常规Animal键的Map。
顺便说一下,这是无效的java语法:
public extends Animal> void takeThing(ArrayList> list)
使用这种forms的generics方法声明,您必须使用有效的java标识符而不是“?”。
编辑:
“?extends Type”forms仅适用于变量或参数类型声明。 在通用方法declration中,它必须是“Identifier extends Type”,因为您可以在方法中引用“Identifier”。
通配符是关于generics的共同/反转方差。 我将通过提供一些例子来试图弄清楚这意味着什么。
基本上它与以下事实有关:对于类型S和T,其中S是T的子类型,genericsG
不是G
的有效子类型。
List someNumbers = new ArrayList(); // compile error
你可以用外卡来解决这个问题
List extends Number> someNumbers = new ArrayList(); // this works
请注意,您不能将任何内容放入此类列表中
someNumbers.add(2L); //compile error
甚至(对许多开发人员来说更令人惊讶):
List extends Long> someLongs = new ArrayList(); someLongs.add(2L); // compile error !!!
我认为SO不是详细讨论它的正确位置。 我将尝试找到一些更详细解释这一点的文章和论文。
将类型绑定到类型参数可能更强大,具体取决于方法应该执行的操作。 我不确定takeThing
应该做什么,但想象一般我们有一个带有这些类型签名之一的方法:
public void foo(ArrayList list); //or public void foo(ArrayList extends Animal> list);
以下是您只能使用第一个类型签名的具体示例:
public void foo(ArrayList list) { list.add(list.remove(0)); // (cycle front element to the back) }
在这种情况下,需要T
通知类型检查器从列表中删除的元素是要添加到列表的OK元素。
您不能使用通配符执行此操作,因为由于通配符尚未绑定到类型参数,因此不会跟踪其上下文(嗯,通过“捕获”跟踪它,但它无法利用)。 您可以在我给出的另一个答案中获得有关此内容的更多信息: generics的generics如何工作?
如果你写? extends T
? extends T
你说“任何T或更具体的东西”。 例如: List
只能包含Shape
,而List extends Shape>
List extends Shape>
可以有Shape
s, Circle
s, Rectangle
s等。
如果你写? super T
? super T
你说“任何T或更普遍的东西”。 这种情况较少使用,但有用例。 一个典型的例子是回调:如果你想将一个Rectangle
传回一个回调,你可以使用Callback super Rectangle>
Callback super Rectangle>
,因为Callback
也能处理Rectangle
。
这是维基百科的相关文章 。
如果takeThing
方法需要向list
参数添加元素,则通配符版本将无法编译。
有趣的情况是,当您不添加到列表中时,两个版本似乎都可以编译和工作。
在这种情况下,如果要在列表中允许不同类型的动物(更灵活)和参数版本(当您需要列表中的固定动物类型:T类型)时,您将编写通配符版本。
例如, java.util.Collection
声明:
interface Collection { ... public boolean containsAll(Collection> c); ... }
假设您有以下代码:
Collection
如果java.util.Collection
将是:
interface Collection { ... public boolean containsAll(Collection c); ... }
上述测试代码无法编译, Collection
API的灵活性也会降低。
值得注意的是, containsAll
的后一个定义具有在编译时捕获更多错误的优点,例如:
Collection c = Arrays.asList("1", "2"); Collection i = Arrays.asList(1, 2, 3); i.containsAll(c); //does not compile, the integer collection can't contain strings
但是错过了有效的测试,使用Collection
Javagenerics通配符的使用受GET-PUT原则(也称为IN-OUT原则)的约束。 这表明: 当您只从结构中获取值时使用“扩展”通配符,当您仅将值放入结构时使用“超级”通配符,并且在执行这两者时不使用通配符。 这不适用于方法的返回类型。 不要使用通配符作为返回类型 。 见下面的例子:
public static void copyContainerDataValues(Container extends T> source, Container super T> destinationtion){ destination.put(source.get()); }