为什么这些声明在 Java 中无效?
List<Number> test = new ArrayList<? extends Number>();
List test = new ArrayList<? extends Number>();
我们不允许在实例化过程中使用通配符吗?如果通配符仅用于将它们传递给方法?
并且List<Object> test = new ArrayList<Integer>();
是非法的,因为泛型不是协变正确的?
通配符的?
意思是“未知”而不是“任何”。实例化一个包含未知内容的新容器没有任何意义,你会在里面放什么?它真的不能用于任何事情!
所以声明的new ArrayList<? extends Number>()
意思是“一些扩展数字的特定事物,但我不知道是什么”。这并不 意味着“任何扩展数字的东西”。
您分配给它的List<Number>
将允许将 Double 和 Integer 添加到其中,但 a 的实际内容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 之前编写的代码仍然可以编译。如果您的方案与 pre-java 5 不向后兼容,请不要这样做。
要了解为什么不允许创建通配符参数化类型的对象,您必须首先了解通配符参数化类型的用途。
如您所知,Java 泛型是不变的。所以 aList<Number>
不是 的超类List<Integer>
,即使它们的类型参数是协变的。那么,如果你也想要泛型中的这种行为,比如让相同的引用指向不同的对象呢?那个多态的东西,正如你所说的那样。如果您想要一个List
引用来引用 、 或 的列表Integer
怎么Float
办Double
?
使用通配符,您可以实现上述行为。因此, aList<? extends Number>
可以引用 a 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
(当然,也有下界通配符。但这不是重点。它们之间的差异已经在许多其他答案中讨论过) .
您会在方法参数中看到通配符参数化类型的主要用途,您希望在其中为单个方法参数传递泛型类型的不同实例:
// 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中所述。但我真的不认为你会需要那个。
当您实例化参数化类时,参数必须是某种已知的具体类型。它甚至可以是参数化的类,?
但出于推理原因,它必须是具体的。例如,这是一个有效的声明:new ArrayList<List<?>>();
这里的技巧是在其签名的参数中使用类型参数的方法要求参数的类型是下限。也就是说,您传入的任何参数都可以转换为参数类型。例子:
public void fillUp(List<? super T> param)
该方法接受一个集合并用类型对象fillUp
填充它。T
该param
列表必须能够处理T
对象,因此声明该列表可以包含 的祖先类型T
,T
可以安全地转换为该类型。如果T
不是具体类型,例如? extends Number
,那么就不可能准确定义 的所有祖先T
。
这不是一个有效的声明,因为它不是已知类型。您没有在此处指定完整类型。new ArrayList<Number>
可以接受通过子类型扩展的任何内容,Number
因此您的使用? extends Foo
不是有效的需要。
List<Number>
可以接受Integer
,Long
等。没有办法做等效的事情,因为超出或带有奇怪的人为限制? super Foo
在语义上毫无意义。List
List<Object>
您当前的定义不正确,泛型类型应在双方相同或应具有继承关系。
Java 泛型不是协变的。例如,请参阅 Brian Goetz 的文章Java 理论与实践:泛型陷阱。您的第一个示例有两个问题。首先,当您实例化一个类型时,必须完全指定它(包括任何类型参数)。其次,类型参数必须与左侧完全匹配。
关于类型协方差(或缺乏协方差),这也是不合法的:
List<Number> test = new ArrayList<Integer>();
尽管事实上Integer
延伸Number
。这也解释了为什么第二个例子是非法的。原始类型与将类型参数绑定到 或多或少相同Object
,因此它类似于:
List<Object> test = new ArrayList<Integer>();
再次失败,因为泛型不是协变的。
至于为什么必须完全指定类型参数,Java 语言规范第 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!
不要被Java 继承概念和Java 泛型概念的通配符(例如这里的?)语法所迷惑。两者都不相同,并且没有任何继承规则适用于 java 泛型概念。
因此Number
不一样? extends Number
请注意,Java Generic 通配符旨在告诉编译器预期的对象用途。在运行时,它根本不存在!
如果您将 Java 中的泛型视为防止您犯错的工具,那么您在理解这个概念时应该不会出错。