简体   繁体   English

在理解Java中的通用集合时遇到问题

[英]Problem in understanding generic collections in Java

Can someone please explain why the following code is not allowed: 有人可以解释为什么不允许以下代码:

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

But I can do this: 但是我可以这样做:

Number[] a = new Integer[10];

I am learning generics and don't understand this. 我正在学习泛型,但不理解这一点。 Why does the type on the left be exactly the type from the right and child types are not allowed? 为什么左边的类型恰好是右边的类型,不允许子类型?

TIA! TIA!

It's because arrays support covariance in a simple but less type-safe way. 这是因为数组以简单但类型安全性较低的方式支持协方差。 You can use covariance in the first case by writing: 在第一种情况下,您可以通过以下方式使用协方差:

List<? extends Number> l = new ArrayList<Integer>();

Here's the reason it isn't allowed... suppose your original line worked. 这是不允许的原因……假设您的原始行有效。 Then you could write: 然后,您可以编写:

ArrayList<Integer> integers = new ArrayList<Integer>();
// Invalid code - fortunately
List<Number> numbers = integers;
numbers.add(new BigDecimal(5.5));
Integer x = integer.get(0); // Bang!

Arrays "know" their real types, unlike generic collections - so the equivalent array example would fail differently: 与通用集合不同,数组“知道”它们的实类型-因此,等效数组示例将以不同的方式失败:

// All valid code... just not as type-safe as it looks
Integer[] integers = new Integer[1];
Number[] numbers = integers;
numbers[0] = new BigDecimal(5.5);

This is a common problem for people trying to understand Generics in Java. 对于试图理解Java泛型的人们来说,这是一个普遍的问题。 For a detailed explanation, read the wiki article about covariance and contravariance . 有关详细说明,请阅读有关协方差和相反方差的Wiki文章。 As your code sample demonstrates, Java's generic classes are neither covariant nor contravariant. 如您的代码示例所示,Java的泛型类既不是协变也不是协变。

Here's a simpler, more intuitive explanation. 这是一个更简单,更直观的解释。 Let's say 比方说

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

was a valid declaration. 是有效的声明。 Then, through polymorphism, you could give l to a function declared as 然后,通过多态,可以将l声明为

public void foo(List<Number> list) {
    l.add(new Double(42.0));
}

According to polymorphism, foo(l) should be a perfectly valid call ( Double is a Number , after all), but you'd be adding a Double to an ArrayList<Integer> , which understandable violates type safety. 根据多态性, foo(l)应该是一个完全有效的调用(毕竟Double是一个Number ),但是您要向ArrayList<Integer>添加Double ,这可以理解,这违反了类型安全性。

The reasons for this are based on how Java implements generics. 原因是基于Java如何实现泛型。

An Arrays Example 数组示例

With arrays you can do this (arrays are covariant as others have explained) 使用数组可以做到这一点(数组是协变的,正如其他人解释的那样)

Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;

But, what would happen if you try to do this? 但是,如果尝试这样做会发生什么?

Number[0] = 3.14; //attempt of heap pollution

This last line would compile just fine, but if you run this code, you could get an ArrayStoreException . 最后一行可以正常编译,但是如果运行此代码,则可能会得到ArrayStoreException Because you're trying to put a double into an integer array (regardless of being accessed through a number reference). 因为您尝试将双精度型放入整数数组中(无论通过数字引用进行访问)。

This means that you can fool the compiler, but you cannot fool the runtime type system. 这意味着您可以欺骗编译器,但不能欺骗运行时类型系统。 And this is so because arrays are what we call reifiable types . 之所以这样,是因为数组是我们所谓的可更新类型 This means that at runtime Java knows that this array was actually instantiated as an array of integers which simply happens to be accessed through a reference of type Number[] . 这意味着在运行时Java知道此数组实际上是作为整数数组实例化的,而该数组恰好是通过Number[]类型的引用进行访问的。

So, as you can see, one thing is the actual type of the object, an another thing is the type of the reference that you use to access it, right? 因此,正如您所看到的,一件事是对象的实际类型,另一件事是用于访问它的引用的类型,对吗?

The Problem with Java Generics Java泛型的问题

Now, the problem with Java generic types is that the type information is discarded by the compiler and it is not available at run time. 现在,Java泛型类型的问题在于类型信息被编译器丢弃,并且在运行时不可用。 This process is called type erasure . 此过程称为类型擦除 There are good reason for implementing generics like this in Java, but that's a long story, and it has to do with binary compatibility with pre-existing code. 在Java中实现这样的泛型是有充分的理由的,但这是一个很长的故事,它与预先存在的代码的二进制兼容性有关。

But the important point here is that since, at runtime there is no type information, there is no way to ensure that we are not committing heap pollution. 但是这里的重点是,由于在运行时没有类型信息,因此无法确保我们不会造成堆污染。

For instance, 例如,

List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);

List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution

If the Java compiler does not stop you from doing this, the runtime type system cannot stop you either, because there is no way, at runtime, to determine that this list was supposed to be a list of integers only. 如果Java编译器没有阻止您执行此操作,则运行时类型系统也不会阻止您执行此操作,因为在运行时无法确定此列表仅应为整数列表。 The Java runtime would let you put whatever you want into this list, when it should only contain integers, because when it was created, it was declared as a list of integers. Java运行时允许您将只包含整数的所有内容放入此列表中,因为在创建时将其声明为整数列表。

As such, the designers of Java made sure that you cannot fool the compiler. 因此,Java的设计人员确保您不能欺骗编译器。 If you cannot fool the compiler (as we can do with arrays) you cannot fool the runtime type system either. 如果您不能欺骗编译器(就像我们对数组所做的那样),那么您也不能欺骗运行时类型系统。

As such, we say that generic types are non-reifiable . 因此,我们说泛型是不可更改的

Evidently, this would hamper polymorphism. 显然,这会妨碍多态性。 Consider the following example: 考虑以下示例:

static long sum(Number[] numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

Now you could use it like this: 现在您可以像这样使用它:

Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};

System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));

But if you attempt to implement the same code with generic collections, you will not succeed: 但是,如果您尝试用通用集合实现相同的代码,则不会成功:

static long sum(List<Number> numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

You would get compiler erros if you try to... 如果您尝试...,将会得到编译器错误。

List<Integer> myInts = asList(1,2,3,4,5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);

System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error

The solution is to learn to use two powerful features of Java generics known as covariance and contravariance. 解决方案是学习使用Java泛型的两个强大功能,即协方差和逆方差。

Covariance 协方差

With covariance you can read items from a structure, but you cannot write anything into it. 使用协方差,您可以从结构中读取项目,但不能在其中写入任何内容。 All these are valid declarations. 所有这些都是有效的声明。

List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>()
List<? extends Number> myNums = new ArrayList<Double>()

And you can read from myNums : 您可以从myNums读取:

Number n = myNums.get(0); 

Because you can be sure that whatever the actual list contains, it can be upcasted to a Number (after all anything that extends Number is a Number, right?) 因为您可以确定实际列表中包含的内容,都可以将其向上转换为Number(所有扩展Number的东西都是Number,对吗?)

However, you are not allowed to put anything into a covariant structure. 但是,不允许您将任何内容放入协变结构中。

myNumst.add(45L); //compiler error

This would not be allowed, because Java cannot guarantee what is the actual type of the object in the generic structure. 这是不允许的,因为Java无法保证泛型结构中对象的实际类型是什么。 It can be anything that extends Number, but the compiler cannot be sure. 它可以是扩展Number的任何内容,但是编译器不能确定。 So you can read, but not write. 因此您可以阅读,但不能书写。

Contravariance 逆变

With contravariance you can do the opposite. 有了相反性,您可以做相反的事情。 You can put things into a generic structure, but you cannot read out from it. 您可以将事物放入通用结构中,但不能从中读出。

List<Object> myObjs = new List<Object();
myObjs.add("Luke");
myObjs.add("Obi-wan");

List<? super Number> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);

In this case, the actual nature of the object is a List of Objects, and through contravariance, you can put Numbers into it, basically because all numbers have Object as their common ancestor. 在这种情况下,对象的实际性质是“对象列表”,并且可以通过逆变将“数字”放入其中,这基本上是因为所有数字都以“对象”作为其共同祖先。 As such, all Numbers are objects, and therefore this is valid. 因此,所有数字都是对象,因此这是有效的。

However, you cannot safely read anything from this contravariant structure assuming that you will get a number. 但是,假设您将得到一个数字,那么您将无法安全地从此反结构中读取任何内容。

Number myNum = myNums.get(0); //compiler-error

As you can see, if the compiler allowed you to write this line, you would get a ClassCastException at runtime. 如您所见,如果编译器允许您编写此行,则在运行时将收到ClassCastException。

Get/Put Principle 获取/放置原理

As such, use covariance when you only intend to take generic values out of a structure, use contravariance when you only intend to put generic values into a structure and use the exact generic type when you intend to do both. 因此,在仅打算将通用值从结构中取出时,请使用协方差;在仅打算将通用值放入结构中时,请使用逆方差;当您打算同时使用两者时,请使用确切的通用类型。

The best example I have is the following that copies any kind of numbers from one list into another list. 我最好的例子是将以下任何一种数字从一个列表复制到另一个列表。 It only gets items from the source, and it only puts items in the destiny. 它仅从来源获取项目,并且仅项目置于目的地。

public static void copy(List<? extends Number> source, List<? super Number> destiny) {
    for(Number number : source) {
        destiny.add(number);
    }
}

Thanks to the powers of covariance and contravariance this works for a case like this: 得益于协方差和逆方差的强大功能,它可以在以下情况下工作:

List<Integer> myInts = asList(1,2,3,4);
List<Integer> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();

copy(myInts, myObjs);
copy(myDoubles, myObjs);

ArrayList<Integer> is a subclass of List<? extends Number> ArrayList<Integer>List<? extends Number>的子类List<? extends Number> List<? extends Number> not a subclass of List<Number> . List<? extends Number>而不是List<Number>的子类。 This is because generics are invariant in that for any two distinct types Type1 and Type2 , List<Type1> is not a subtype neither a supertype of List<Type2> , therefore a bounded wildcard type is used, as in List<? extends Number> 这是因为泛型是不变的 ,因为对于任何两个不同类型Type1Type2List<Type1>都不是List<Type2>的子类型,也不是List<Type2>的超类型,因此使用有界通配符类型 ,如List<? extends Number> List<? extends Number> , to deal with these sort of situations. List<? extends Number> ,以处理此类情况。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM