我们是否允许在实例化期间使用通配符

时间:2013-09-04 19:48:03

标签: java generics wildcard

为什么这些声明在Java中无效?

  1. List<Number> test = new ArrayList<? extends Number>();

  2. List test = new ArrayList<? extends Number>();

  3. 我们不允许在实例化期间使用通配符。如果通配符仅用于将它们传递给方法?

    List<Object> test = new ArrayList<Integer>();是非法的,因为泛型不协变正确吗?

7 个答案:

答案 0 :(得分:3)

?通配符表示“未知”而不是“任何”。实例化一个未知内容的新容器没有任何意义,你会把它放在那里?它真的无法用于任何事情!

所以声明new ArrayList<? extends Number>()意味着“某些特定的东西可以扩展数字,但我不知道是什么。”它意味着“任何扩展数字的东西。”
您为其分配的List<Number>将允许将Double和Integer添加到其中,但List<? extends Number>的实际内容可能是Float! (或其他任何东西。) 如果通配符作为“任何”工作,请考虑此代码中会发生什么:

    List<Integer> listInteger = new ArrayList<Integer>();
    listInteger.add(Integer.valueOf(1));
    List<? extends Number> listWildCard = listInteger;
    listWildCard.add(Double.valueOf(1.0)); //This does not compile
    Integer integer = listInteger.get(1);//because this would throw a ClassCastException

关于你的第二个例子的脚注: 使用原始类型调用声明没有类型参数的参数化类型。这被认为是编程错误。语法是合法的,因此在java 5之前编写的代码仍然可以编译。如果你的场景与java之前的版本不兼容,那就不要这样做了。

答案 1 :(得分:3)

要理解为什么不允许创建通配符参数化类型的对象,必须首先了解通配符参数化类型的用法。

为什么选择通配符?

正如您已经知道Java泛型是不变的。所以List<Number>不是List<Integer>的超类,即使它们的类型参数是协变的。那么,如果你想在泛型中也有这样的行为,比如同一个引用指向不同的对象呢?那个多态的东西,正如你所说的那样。如果您希望单个List引用引用IntegerFloatDouble列表,该怎么办?

拯救的通配符:

使用通配符,您可以实现上述行为。因此,List<? extends Number>可以引用List<Integer>List<Double>等。因此,以下声明有效:

List<? extends Number> numbers = new ArrayList<Integer>();
numbers = new ArrayList<Double>();
numbers = new ArrayList<Float>();
numbers = new ArrayList<String>();  // This is still not valid (you know why)

那么我们在这里改变了什么?只是numbers的引用类型。请注意,Java中引入了泛型,用于执行更强大的编译时检查。因此,主要是编译器的工作是确定参数化类型的声明是否符合规则。如果没有通配符,编译器会显示List<Number>引用List<Integer>的错误。

因此,通配符只是将行为等协方差引入泛型的一种方法。通过使用通配符,可以增加灵活性,或者可以说减少编译器强制执行的限制。 List<? extends Number>引用告诉编译器该列表可以引用Number的列表或Number 的任何子类型(当然,也有较低的有界通配符。但这不是这里的要点。它们之间的差异已经在SO上的许多其他答案中讨论过了。)

您将在方法参数中看到的通配符参数化类型的主要用途,您希望为单个方法参数传递泛型类型的不同实例:

// Compiler sees that this method can take List of any subtype of Number
public void print(List<? extends Number> numbers) {
    // print numbers
}

但是在运行时,为了创建一个对象,你必须给出一个具体的类型。通配符 - 有界或无界,不是具体类型。 ? extends Number可能意味着Number的子类型。那么在创建List时,您期望创建哪种类型的List<? extends Number>? 您可以将此案例视为无法实例化interface的原因。因为它们不仅仅是具体的。

有一种解决方法。真的?

虽然这是非法的,但您会惊讶地发现有一种解决方法,如 - Java Generics FAQs中所述。但我真的不认为你会需要它。

答案 2 :(得分:2)

实例化参数化类时,参数必须是某种已知的具体类型。即使使用?,它也可以是参数化类,但出于推理的原因,它必须是具体的。例如。这是一个有效的声明:new ArrayList<List<?>>();

这里的技巧是在签名的参数中使用type参数的方法要求参数的类型是下限。也就是说,您传入的任何参数都可以强制转换为参数类型。例如:

public void fillUp(List<? super T> param)

fillUp方法接受一个集合并用T类型对象填充它。 param列表必须能够处理T对象,因此声明列表可以包含T的祖先类型,T可以安全地转换为类型。如果T不是具体类型,例如? extends Number,那么就不可能准确定义T的所有祖先。

答案 3 :(得分:1)

这不是一个有效的声明,因为它不是一个已知的类型。你没有在这里指定一个完整的类型。 new ArrayList<Number>可以通过子类型接受任何扩展Number的内容,因此您对? extends Foo的使用不是有效需求。

List<Number>可以接受IntegerLong等。除了? super FooList之外,没有办法等同于List<Object>,因为它在语义上毫无意义{{1}}有一个奇怪的人为限制。

答案 4 :(得分:1)

您当前的定义不正确,泛型类型在两侧应该相同或者应该具有继承关系。

答案 5 :(得分:1)

Java泛型不是协变的。例如,请参阅Brian Goetz撰写的文章Java theory and practice: Generics gotchas。你的第一个例子有两个问题。首先,当您实例化一个类型时,必须完全指定它(包括任何类型参数)。其次,类型参数必须与左侧完全匹配。

关于类型协方差(或缺乏协方差),这也是不合法的:

List<Number> test = new ArrayList<Integer>();

尽管Integer延伸Number。这也解释了为什么第二个例子是非法的。原始类型与将类型参数绑定到Object或多或少相同,因此它类似于:

List<Object> test = new ArrayList<Integer>();

再次失败,因为泛型不是协变的。

至于为什么必须完全指定类型参数,Java Language Specification, §8.1.2解释了这个概念:

  

泛型类声明定义了一组参数化类型(第4.5节),每个类型参数通过类型参数调用一个。

您只能实例化实际类型。只要泛型类型的类型参数未绑定,泛型类型本身就是不完整的。您需要告诉编译器正在实例化哪个特定的参数化类型(在泛型类定义的集合中)。

至于为什么泛型不是协变的,这是为了防止以下类型的错误:

List<Integer> iTest = new ArrayList<Integer>();
List<Number> test = iTest;
test.add(Double.valueOf(2.5));
Integer foo = iTest.get(0); // Oops!

答案 6 :(得分:0)

不要对Java继承概念和Java通用概念的通配符(例如?here)syntex感到困惑。两者都不相同,并且任何继承规则都不适用于java泛型概念。

因此Number? extends Number

不同

请注意,Java Generic通配符旨在告诉编译器有关预期的对象使用情况。在运行时,它根本不存在!

如果你在java中看到泛型只是一种防止你犯错误的工具,你就不应该理解这个概念。