通配符 vs. 泛型方法

14

以下两种方法在打印范围内的所有元素时有实际区别吗?

public static void printA(Iterable<?> range)
{
    for (Object o : range)
    {
        System.out.println(o);
    }
}

public static <T> void printB(Iterable<T> range)
{
    for (T x : range)
    {
        System.out.println(x);
    }
}

显然,printB涉及到一个额外的Object类型的检查转换(见第16行),这对我来说似乎相当愚蠢——毕竟所有东西不都是Object吗?

public static void printA(java.lang.Iterable);
  Code:
   0:   aload_0
   1:   invokeinterface #18,  1; //InterfaceMethod java/lang/Iterable.iterator:()Ljava/util/Iterator;
   6:   astore_2
   7:   goto    24
   10:  aload_2
   11:  invokeinterface #24,  1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
   16:  astore_1
   17:  getstatic   #30; //Field java/lang/System.out:Ljava/io/PrintStream;
   20:  aload_1
   21:  invokevirtual   #36; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
   24:  aload_2
   25:  invokeinterface #42,  1; //InterfaceMethod java/util/Iterator.hasNext:()Z
   30:  ifne    10
   33:  return

public static void printB(java.lang.Iterable);
  Code:
   0:   aload_0
   1:   invokeinterface #18,  1; //InterfaceMethod java/lang/Iterable.iterator:()Ljava/util/Iterator;
   6:   astore_2
   7:   goto    27
   10:  aload_2
   11:  invokeinterface #24,  1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
   16:  checkcast   #3; //class java/lang/Object
   19:  astore_1
   20:  getstatic   #30; //Field java/lang/System.out:Ljava/io/PrintStream;
   23:  aload_1
   24:  invokevirtual   #36; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
   27:  aload_2
   28:  invokeinterface #42,  1; //InterfaceMethod java/util/Iterator.hasNext:()Z
   33:  ifne    10
   36:  return
3个回答

7
在你的示例中,泛型类型仅在签名的一个位置使用。在这种情况下,对于调用者来说,类型T与通配符相比没有任何优势。在你的示例中,类型对于方法的实现者也没有任何优势。
我发现通配符版本更容易让调用者理解,因为它明确地表示“我根本不关心类型”。
在你的示例中,checkcast确实是多余的。如果T被限定,例如T extends Number,则需要检查Number的转换,因为局部变量x将是Number类型,但Iterator.next()方法仍然返回Object类型。似乎Java编译器不会费心去优化转换。JIT可能会在运行时进行优化。
更新:
如果泛型类型在多个位置使用,例如cletus的答案中,你只能使用泛型类型T,否则编译器看不到参数类型/返回类型之间的联系(对于编译器来说,任意两个通配符都是不同的)。
一个边界情况是当签名中只有一个位置的类型,但实现需要它是一个泛型类型而不是通配符。想象一下void swap(List list,int a,int b)方法。它需要从列表中取出元素并将其放回。如果我没记错的话,《Effective Java》建议使用带有通配符的公共方法和带有包含实际实现的类型的内部助手方法。这样,用户就会得到一个简单的API,而实现者仍然具有类型安全性。
public void swap(List<?> list, int a, int b){
    swapHelper(list, a, b);
}
private <T> void swapHelper(List<T> list, int a, int b){
    ...
}

我同意:在这种情况下,通配符版本更好。 - Jorn

2
第二种方式更加灵活。一个更好的例子是:它描述了类型信息,是否对你有用取决于函数的功能。
当你想要从方法中返回一些东西时,第二种方式就显示了它的有用性:
public static <T> List<T> reverse(List<T> list) {
  for (int i=0; i<n/2; i++) {
    T value = list.get(i);
    list.set(i, list.get(list.size() - i - 1));
    list.set(list.size() - i = 1, value);
  }
  return list;
}

也许复制数组是一个更好的例子,但重点在于你不能真正地用 List<?> 做以上操作。


我不理解这个。据我所知,这两种形式在完全相同的上下文中可以使用,其中一种并不比另一种提供更多或更少的类型安全性。我有什么遗漏吗? - Marcelo Cantos
1
如果你使用 ? 版本,调用方法将会得到一个 List,需要将该 List 强制转换为 List<Whatever>。这有点不安全,因为编译器无法检查实现是否确实产生了 List<Whatever>。协定(带 ?)指出应返回一个 List,尽管它不是 List<T>。T 版本可以让编译器检查返回类型,使得调用方法更加安全。 - extraneon
是的,但只要T在参数列表中的一个位置上出现(就像OP的例子一样),它就可以被转换为通配符。这个例子不同(T出现在两个位置,包括返回值中)。 - newacct
@extraneon:OP的两种方法都返回void。@cletus似乎在更一般的情况下讨论区别。 - Marcelo Cantos

0

并不会。生成的字节码几乎完全相同。


啊,是的。所以 <T> 版本实际上检查内容是否为正确类型,因此如果 Iterable<T> 包含不是 T 的内容,则可能会失败。我已经稍微缓和了我的回答。 - Marcelo Cantos
@Marcello:不,它不会检查Iterable<T>中是否包含不是T的内容。它只是检查是否包含无法转换为Object的内容。但是Java中的泛型实现太愚蠢了,所以我认为这只是另一个剩余问题... - Foxfire
谢谢Foxfire。Java的泛型永远让我惊叹不已。 - Marcelo Cantos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接