Java的类型参数通配符到底是什么意思?Foo和Foo<?>之间真正的区别是什么?

8

对于通用接口:

public interface Foo<T> {
    void f(T t); 
} 

两个领域之间的区别:
public class Bar {
    Foo foo1; 
    Foo<?> foo2; 
}

foo2 是一种通用类型,而 foo 不是。由于 ? 代表了任何类型的通配符,而每个类型都是 Object 的子类型,因此我希望 Foo<?>Foo<Object> 在语义和语法上应该是等同的。

然而,请看下面的例子:

public class Puzzler {
    void f() {
        Integer i = null; 
        Foo<?> foo1 = null;
        foo1.foo(i); // ERROR 
        Foo foo2 = null; 
        foo2.foo(i); // OKAY
        Foo<Integer> foo3 = null; 
        foo3.foo(i); // OKAY 
        Foo<Object> foo4 = null; 
        foo4.foo(i); // OKAY
    }

    private interface Foo<T> {
        void foo(T t);
    } 
}

所以Foo<?>Foo<Object>在语法上并不是相同的。

这是怎么回事?我对理解这个问题很困惑。


假设这些是编译错误? - calebds
@andrewcooke 这段代码是有效的;然而,由于对象将存储在Foo<?>变量中,它不会知道String参数。因此,如果Foo有任何接受类型为T的参数的方法,唯一允许的值将是null,如果它有任何返回T的方法,它将被视为Object - Taymon
抱歉,在看到你的回复之前我删除了我的评论。现在有些困惑了,capture 什么时候起作用? - andrew cooke
哦,好的,只有与自身完全匹配才能捕获。现在我看到了编译器错误在一些测试代码中,这很明显。 - andrew cooke
5个回答

6
Foo<?>Foo<? extends Object>在语义上是相同的:它们都是具有特定类型参数的Foo,但唯一已知的关于“something”的事情是它是Object的某个子类(这并没有说太多,因为所有类都是Object的子类)。另一方面,Foo<Object>是具有特定类型参数ObjectFoo。虽然一切都与Object兼容,但不是一切都与?兼容,其中?扩展了Object
这里有一个示例说明为什么Foo<?>应该生成错误:
public class StringFoo implements Foo<String> {
    void foo(String t) { . . . }
}

现在将你的示例更改为以下内容:

Foo<?> foo1 = new StringFoo();

由于i是一个整数,编译器不应该允许foo1.foo(i)编译。

请注意:

Foo<Object> foo4 = new StringFoo();

根据匹配参数化类型的规则,ObjectString被证明是不同的类型,因此也无法编译。

Foo(没有任何类型参数的原始类型)通常被认为是编程错误。然而,根据Java语言规范(§4.8),编译器接受这种代码以避免破坏非泛型遗留代码。

由于类型擦除,这些都对生成的字节码没有任何影响。也就是说,这些只有在编译时才会有区别。


编译器不能将原始类型视为错误,请参见JLS - Daniel Lubarov
@Daniel - 在Eclipse中,您可以设置编译器将使用原始类型视为错误。您还可以设置大多数编译器将所有警告视为错误,编译器肯定会警告使用原始类型。 - Ted Hopp
具体来说:如果您将Foo用作_原始类型_(即没有类型参数),这将(a)生成编译时警告(可以忽略),(b)导致编译器对所有泛型类型使用Object,并且(c)防止编译器在其他泛型的上下文中检查类型参数的正确性。例如,Foo<String> f = new Foo<Object>();是一个编译时错误,但Foo<String> f = new Foo();将编译(带有警告),然后在运行时如果类型不正确会引发ClassCastException。这是不可取的,因此不要使用原始类型。 - Taymon

1
考虑以下类型:
  • List<Object>
  • List<CharSequence>
  • List<String>

尽管 StringCharSequence 的子类型,而 CharSequence 又是 Object 的子类型,但这些 List 类型之间并没有任何子类型-超类型关系。(有趣的是,String[]CharSequence[] 的子类型,而 CharSequence[] 又是 Object[] 的子类型,但这是由于历史原因。)

假设我们想编写一个打印 List 的方法。如果我们这样做:

void print(List<Object> list) {...}

这段代码无法直接打印一个 List<String>(除非使用hack),因为 List<String> 不是 List<Object>。但是通过使用通配符,我们可以写成:

void print(List<?> list) {...}

并将其传递给任何列表。

通配符可以具有上限和下限,以增加灵活性。假设我们想打印一个仅包含CharSequence的列表。如果我们执行以下操作:

void print(List<CharSequence> list) {...}

然后我们遇到了同样的问题--我们只能传递一个List<CharSequence>,而我们的List<String>不是List<CharSequence>。但如果我们改为

void print(List<? extends CharSequence> list) {...}

然后我们可以传递一个List<String>,以及一个List<StringBuilder>等等。


0

Foo<?>中的通配符表示,在当前范围内,您不知道或不关心'Foo'的类型。

Foo<?>Foo<? extends Object>是相同的(前者是后者的简写)。Foo<Object>则不同。

一个具体的例子

您可以将任何类型的List分配给 List<?>,例如:

List<?> list1 = new ArrayList<String>();
List<?> list2 = new ArrayList<Object>();
List<?> list3 = new ArrayList<CharSequence>();

如果您有一个List<?>,则可以调用size(),因为您不需要知道列表是什么类型以找出其大小。并且您可以调用get(i),因为我们知道该列表包含某种Object,因此编译器将处理它,就好像get返回和Object一样。
但是您无法调用add(o),因为您不知道(编译器也不知道)正在处理什么类型的列表。
在上面的示例中,您不想允许list1.add(new Object());因为那应该是一个String列表

通配符的原因是,这样您就可以做到这样的事情:

public static boolean containsNull(List<?> list)
{
    for(Object o : list )
    {
       if( o == null ) return true;
    }
    return false;
}

那段代码可以在任何你想要的列表上工作,比如 List<String>List<Object>List<Integer> 等等。

如果签名是 public static boolean containsNull(List<Object> list),那么你只能传递 List<Object>List<String> 将不起作用。


0

由于类型擦除的原因,任何与泛型相关的内容都只存在于编译时期;我想这就是你所说的语法上的区别吧。
我认为你最初的评估是正确的,真正的区别在于<T>使其成为了一个泛型类型的变量,而普通的Foo则不是。


0

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