Java 泛型 - ArrayList 初始化

21

众所周知,ArrayList的初始化应该像这样

ArrayList<A> a = new ArrayList<A>();
ArrayList<Integer> a = new ArrayList<Number>(); // compile-time error

那么,为什么Java允许这些呢?

1. ArrayList<? extends Object> a1 = new ArrayList<Object>();
2. ArrayList<?> a2 = new ArrayList<Integer>();
然后,如果它们是正确的,为什么不允许这些?
1. a1.add(3);
2. a2.add(3);

编译器错误信息为:类型ArrayList中的方法add(int, capture#1-of ? extends Object)对于参数(int)不适用。

更具一般性的描述。

  1. a1.add(null e);
  2. a2.add(? e);

我已经了解了这个问题,但很高兴能从你这里听到。谢谢。

另一个有趣的点是:

 ArrayList<ArrayList<?>> a = new ArrayList<ArrayList<?>>(); // correct
 ArrayList<?> a = new ArrayList<?>(); // wrong. I know it's reason but I have some 
question in my mind that mentioned above 

你能做类似于 a1.add(new Integer(3)); 这样的事情吗? - Gabe
@Gabe 你只能执行a1.add(null)。a1.add(3) 应该被转换为Integer对象,但编译器只看到这个数字2。.add(? e) 只允许添加(null)。 - user467871
6个回答

14

您无法将 List<Number> 分配给类型为 List<Integer> 的引用,因为 List<Number> 允许其他类型的数字而不仅仅是 Integer。 如果允许这样做,那么以下内容将被允许:

List<Number> numbers = new ArrayList<Number>();
numbers.add(1.1); // add a double
List<Integer> ints = numbers;
Integer fail = ints.get(0); // ClassCastException!
类型 List<Integer> 保证了它所包含的任何内容都将是一个 Integer,这就是为什么你可以从中获取一个 Integer 而不需要进行强制类型转换的原因。如果编译器允许将另一种类型的列表(比如 Number)分配给 List<Integer>,那么这个保证就会被打破。
List<Integer> 分配给诸如 List<?>List<? extends Number> 类型的引用是合法的,因为 ? 表示“某个未知的子类型”,其中在只有 ? 的情况下,该类型是 Object,而在 ? extends Number 的情况下,该类型是 Number
由于 ? 表示您不知道列表将接受哪种特定类型的对象,因此除 null 外,不能向其添加任何其他对象。但是,您可以从中检索任何对象,这是使用 ? extends X 有界通配符类型的目的。请注意,对于 ? super X 有界通配符类型,则相反,List<? super Integer> 是“某个未知类型的列表,它至少是 Integer 的超类型”。虽然您不知道它是哪种类型的 List(可能是 List<Integer>List<Number>List<Object>),但您确实知道无论它是什么,都可以将 Integer 添加到其中。
最后,new ArrayList<?>() 是不合法的,因为当您创建一个像 ArrayList 这样的参数化类的实例时,您必须给出一个具体的类型参数。在您的示例中,您可以使用任何类型(比如 ObjectFoo),因为您永远无法向其中添加除 null 之外的任何内容,因为您直接将其分配给了一个 ArrayList<?> 引用。

9
关键在于引用和实例之间的区别,以及引用可以承诺什么,实例真正能做什么。
ArrayList<A> a = new ArrayList<A>();

这里的a是指特定类型的实例,确切地说,是A类型的数组列表。更明确地说,a是一个引用,指向一个可以接受A并能生成A的数组列表。而new ArrayList<A>()则是A类型的数组列表的一个实例,即一个可以接受A并能生成A的数组列表。

ArrayList<Integer> a = new ArrayList<Number>(); 

在这里,a是一个引用,确切地说是一个Integer数组列表,即只能接受Integer并生成Integer的数组列表。它不能指向Number的数组列表。那个Number的数组列表无法满足ArrayList<Integer> a的所有承诺(即一个Number的数组列表可能会产生不是Integer的对象,即使此时为空)。
ArrayList<Number> a = new ArrayList<Integer>(); 

在这里,a的声明表示,a将指向一个完全由Number组成的数组列表,也就是说,它将接受Number并生成Number。它不能指向Integer的数组列表,因为a的类型声明表示,a可以接受任何Number,但是Integer的数组列表不能接受任何Number,它只能接受Integer

ArrayList<? extends Object> a= new ArrayList<Object>();

这里的 a 是对一个类型族的(泛型)引用,而不是对特定类型的引用。它可以指向该族中的任何列表。然而,这种灵活的引用也意味着它不能保证与非泛型引用相同的所有功能。在本例中,a 是一个引用到将产生 Objects 的数组列表。但是,与特定类型的列表引用不同,这个 a 引用不能接受任何 Object。(即 a 可以指向的类型族的成员不能都接受任何 Object,比如一个由 Integer 组成的数组列表只能接受 Integer)。
ArrayList<? super Integer> a = new ArrayList<Number>();

再次强调,a是一组类型的引用(而不是单个特定类型)。由于通配符使用了super,因此此列表引用可以接受Integer,但不能生成Integer。换句话说,我们知道a可以指向的类型族中的任何成员都可以接受Integer,但并非该族的每个成员都能生成Integer

PECS - 生产者extends,消费者super - 这个助记符帮助你记住使用extends表示泛型类型可以生成具体类型(但不能接受它)。使用super表示泛型类型可以消费(接受)具体类型(但不能生成它)。

ArrayList<ArrayList<?>> a

一个数组列表,它持有对于任何属于数组列表类型家族的列表的引用。
= new ArrayList<ArrayList<?>>(); // correct

一个数组列表实例,它持有对任何属于一组数组列表类型的列表的引用。

ArrayList<?> a

任何数组列表的引用(数组列表类型家族的成员之一)。
= new ArrayList<?>()

ArrayList<?> 是指数组列表类型家族中的任意类型,但你只能实例化一个特定类型。


另请参见如何向 List<? extends Number> 数据结构中添加元素?


2
您有奇怪的期望。如果您提供导致这些期望的论点链,我们可能会发现其中的错误。但现在,我只能简要介绍一下泛型,希望能触及您可能误解的几个要点。 ArrayList<? extends Object> 是一个类型参数已知为 Object 或其子类型的 ArrayList。(是的,在类型边界中的 extends 有着其他意义,而不仅仅是直接子类)。由于只有引用类型可以作为类型参数,因此这实际上等同于 ArrayList<?>
也就是说,您可以将 ArrayList<String> 放入使用 ArrayList<?> 声明的变量中。这就是为什么 a1.add(3) 是编译时错误的原因。a1 的声明类型允许 a1 成为一个 ArrayList<String>,而在其中不能添加任何 Integer
很明显,ArrayList<?> 并不是非常有用,因为你只能向其中插入 null。这可能是 Java 规范 禁止 它的原因:

如果在类实例创建表达式中使用的任何类型参数都是通配符类型参数,则会在编译时出错。

相比之下,ArrayList<ArrayList<?>> 是一种功能性数据类型。你可以添加各种 ArrayList 到其中,并检索它们。而且由于 ArrayList<?> 只包含但不是通配符类型,上述规则不适用。

1
很多与多态有关。当你分配时
X = new Y();

X 可以比 Y 更“不具体”,但反过来则不行。X 只是你用来访问 Y 的句柄,而 Y 才是真正实例化的东西。

在这里出现错误是因为 Integer 是一个数字,但 Number 并不是一个整数。

ArrayList<Integer> a = new ArrayList<Number>(); // compile-time error

因此,您调用的 X 的任何方法都必须对 Y 有效。由于 X 更为一般,因此可能共享 Y 的某些方法,但并非全部。尽管如此,任何给定的参数必须对 Y 有效。
在您使用 add 的示例中,int(小写i)不是有效的 Object 或 Integer。
ArrayList<?> a = new ArrayList<?>();

这不好,因为你实际上不能实例化一个包含 ? 的数组列表。你可以声明一个这样的列表,然后几乎任何东西都可以在 new ArrayList<Whatever>(); 中跟随。


0
?视为表示"未知"。因此,"ArrayList<? extends Object>"的意思是"一个未知类型,它(或只要)扩展Object"。因此,需要说明的是,arrayList.add(3)会将你知道的东西放入一个未知中。即'遗忘'。

-1
ArrayList<Integer> a = new ArrayList<Number>(); 

代码无法工作的原因是,Number 是 Integer 的超类并不意味着 List<Number>List<Integer> 的超类。泛型在编译时被移除,在运行时不存在,因此集合的父子关系不能被实现:元素类型的信息被简单地移除了。

ArrayList<? extends Object> a1 = new ArrayList<Object>();
a1.add(3);

我无法解释为什么它不起作用。这真的很奇怪,但这是事实。真正的语法<? extends Object>主要用于方法的返回值。即使在这个例子中,Object o = a1.get(0)也是有效的。

ArrayList<?> a = new ArrayList<?>()

这不起作用,因为您无法实例化未知类型的列表...


真正的语法 <? extends Object> 主要用于方法的返回值。方法几乎不应该返回带有通配符类型参数的泛型类型。通配符类型参数主要用于方法的参数。 - ColinD

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