为什么这些声明在Java中无效?
List<Number> test = new ArrayList<? extends Number>();
List test = new ArrayList<? extends Number>();
我们不允许在实例化期间使用通配符。如果通配符仅用于将它们传递给方法?
和List<Object> test = new ArrayList<Integer>();
是非法的,因为泛型不协变正确吗?
答案 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
引用引用Integer
,Float
或Double
列表,该怎么办?
使用通配符,您可以实现上述行为。因此,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>
可以接受Integer
,Long
等。除了? super Foo
或List
之外,没有办法等同于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中看到泛型只是一种防止你犯错误的工具,你就不应该理解这个概念。