为什么 ArrayList<ArrayList<T>> 的隐式转换到 Iterable<Iterable<T>> 是不可能的?

4
我很乐意为您翻译中文。以下是需要翻译的内容:
我想知道为什么Java不能将 ArrayList<ArrayList<T>> 隐式转换为 Iterable<Iterable<T>>。我的问题不是如何显式地实现此操作。
以下的代码为什么会出错:
import java.util.Iterator;
import java.util.ArrayList;

public class Test {
  public Test() {
    ArrayList<ArrayList<Test>> arr = new ArrayList();
    Iterable<Iterable<Test>> casted = arr;
  }
}

编译过程中是否出现以下错误?
Test.java:7: error: incompatible types: ArrayList<ArrayList<Test>> cannot be converted to Iterable<Iterable<Test>>

2
因为你可以执行 casted.add(new LinkedList());,或者添加任何实现了 Iterable 接口的东西,即使 casted 只应该持有 ArrayList<Test> 对象。 - Colonel Thirty Two
不,我不能执行 arr.add(new LinkedList());,因为 arr 仍然是一个 ArrayList<ArrayList<Test>>。 我也不能执行 casted.add(new LinkedList());,因为 Iterable 没有实现 add。 - lovasoa
2
好的,没错。但是如果你转换成 List<List<Test>> 会发生什么呢?它有一个 add 方法。对于 Iterable 的情况,语义上没有问题(因为 Iterable 是只读的),但对于一般情况,它允许你违反内部模板的类型,所以 Java 不允许你这样做。 - Colonel Thirty Two
到目前为止,这些答案提到了如何解决问题,但他们没有解释为什么需要这样解决。 - seh
Java文档实际上提供了非常好的一般性解释:https://docs.oracle.com/javase/tutorial/extra/generics/subtype.html - Alex Fedulov
4个回答

2
“ArrayList是Iterable,你是对的,所以ArrayList>是Iterable>。但是,Iterable>不是Iterable>,因为在这种情况下,你当然无法明确地向Iterable添加任何内容,但可能会出现这样的情况,你可以(使用其他示例)向其中添加Set,这将破坏arr。你可以通过使用有界泛型来解决这个问题。”
Iterable <? extends Iterable<Test>> casted = arr;

所以,当你在casted上运行iterator()时,你会得到Iterator<? extends Iterable<Test>>,并且在每个next()中,你会得到? extends Iterable<Test>,它是Iterable<Test>
你应该阅读一些关于Java中的协变和逆变的内容来理解这个。

@njzk2 不行,编译器会阻止这样做,因为它不能确定(而且这不是真的),? extends Iterable<Test> 的扩展是否确切地是 HashSet - Dmitry Ginzburg
我觉得你的“但是”断言令人困惑。你写道,“在这种情况下,你当然不能显式地向'Iterable'添加任何内容。”没有人试图这样做。也许你的意思是写成“'Iterable <Iterable<Test>>'不是一个'Iterable<ArrayList<Test>>'”。这样更有意义。 - seh
@seh 没错,我会提供一个答案。 - Dmitry Ginzburg

2
由于泛型的工作方式,您必须使用以下代码:
ArrayList<ArrayList<Test>> arr = new ArrayList();
Iterable<? extends Iterable<Test>> casted = arr;

ArrayList<ArrayList> 不兼容于 Iterable<Iterable>,因为泛型是严格的。 ArrayList 不是 Iterable,但它是 ? extends Iterable

然而,这有一些限制。考虑以下情况:

ArrayList<? extends Iterable<Test>> casted = arr;

这个工作原本是可以的。但现在,casted 没有与任何类型绑定。你不能调用 add,因为没有类型是兼容的。

1
让我们看几个类似的例子:
public final class Test {
  public Test() {
    final ArrayList<ArrayList<Test>> a1 = new ArrayList<>();
    final ArrayList<? extends Iterable<Test>> valueTypeRef = a1;
    final Iterable<? extends Iterable<Test>> spinalTypeRef = valueTypeRef;

    final ArrayList<Iterable<Test>> a2 = new ArrayList<>();
    final Iterable<Iterable<Test>> r2 = a2;
  }
}

在第一个示例中,您可以看到我们可以形成的有效引用,首先是将值类型泛化,然后生成容器类型(当谈论序列时,“脊柱”)。第二个示例(a2)展示了如何从Iterable类型的值类型开始,无需麻烦即可形成所需的引用类型。
你的问题归结为为什么valueTypeRef不能是ArrayList>类型,为什么我们需要使用上限通配符。在Java中,类型ArrayList>与类型ArrayList>没有关系,即使ArrayList是Iterable的子类型。形成valueTypeRef寻找其隐式转换的这种关系,但不存在这样的关系,即使您作为程序员可以看到它应该存在。
Java只允许使用通配符(wildcards)来利用子类型关系,如此处在《Java教程》中所述的 Wildcards and Subtyping。我们需要引入一个上界通配符(这里是 ArrayList<? extends Iterable<Test>>),告诉类型系统我们期望新引用值类型是参考对象值类型的子类型。不带通配符的类型参数既不协变也不逆变,即使函数参数和返回类型沿子类型和超类型轴允许正常替换,泛型类型本身(例如 ArrayList<ArrayList<Test>>ArrayList<Iterable<Test>>)也只能通过通配符进行这些替换。

由此产生的类型(即ArrayList<? extends Iterable<Test>>)阻止使用任何会将某个值插入列表中的变异函数,这个值确实是一个Iterable<Test>,但不是ArrayList<Test>。结果引用类型与其指示物协变,大致意思是可以安全地通过读取,但不能通过写入安全地进行操作。


-1

不好的解决方案

如果有人感兴趣,这是我解决问题的方法。但这只是一个权宜之计,并不能解释为什么第一段代码片段无法工作:

import java.util.Iterator;
import java.util.ArrayList;

public class Test {
  public Test() {
    ArrayList<ArrayList<Test>> arr = new ArrayList();
    Iterable<Iterable<Test>> casted = (Iterable<Iterable<Test>>)(Iterable<?>)arr;
  }
}

这是一项不安全的操作。在这种情况下,因为Iterable是只读的,所以不会造成任何伤害,但是这个方法允许你将ArrayList<ArrayList>转换为ArrayList<LinkedList>,从而可能破坏arr

好的解决方案

没有好的解决方案可以将ArrayList<ArrayList>转换为Iterable<Iterable>,原因如上所述。但是,您可以安全(并且隐式地)将ArrayList<ArrayList>转换为Iterable<? extends Iterable>,并像处理Iterable<Iterable>一样处理类型为Iterable<? extends Iterable>的变量。

您也可以隐式地将Iterable<Iterable>转换为Iterable<? extends Iterable>


1
不要这样做!这段代码通常是不安全的,因为你可以添加不是ArrayList的List,破坏了arr的类型安全性,而arr只允许包含ArrayList。这与说“List<String> l1 = null; List<Object> l2 = (List<Object>) (List<?>) l1; l2.add(new Object()); // l1 包含一个Object实例!”一样糟糕。 - bcsb1001
哎呀,我的意思是 l1 = new ArrayList<String>;,而不是 null,但你明白我的意思了。 - bcsb1001

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