Collections.UnmodifiableMap.UnmodifiableEntrySet为什么要重写stream()方法?

3
在查看源代码时,我发现Collections.UnmodifiableMap.UnmodifiableEntrySet中覆盖了stream()方法。但是代码似乎与Collection.stream()完全相同,只是在Collections.UnmodifiableMap.UnmodifiableEntrySet.stream()中返回的类型更加具体,即Stream<Entry<K,V>>而不仅仅是在Collection.stream()中的Stream<E>
虽然两个类的spliterator()方法不同,但即使未覆盖stream方法,如果对象是UnmodifiableEntrySet类型,则从Collection.stream()调用UnmodifiableEntrySet.spliterator()
那么,有没有任何原因可以解释为什么要覆盖stream方法呢? Collection.java
@Override
default Spliterator<E> spliterator() {
    return Spliterators.spliterator(this, 0);
}
 
default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}

Collections.UnmodifiableMap.UnmodifiableEntrySet.java

@SuppressWarnings("unchecked")
public Spliterator<Entry<K,V>> spliterator() {
    return new UnmodifiableEntrySetSpliterator<>(
        (Spliterator<Map.Entry<K, V>>) c.spliterator());
}

@Override
public Stream<Entry<K,V>> stream() {
    return StreamSupport.stream(spliterator(), false);
}

1
仅仅因为 UnmodifiableEntrySetEntry 的集合而 Collection 是泛型的,我不确定是什么困惑了你。 - Aniket Sahrawat
@AniketSahrawat 是的。我的问题是为什么UnmodifiableEntrySet应该覆盖stream()方法,如果方法体与Collection.stream()完全相同。 - Gautham M
@AniketSahrawat 经过一些调试,我能够找到一个部分解决方案。 - Gautham M
1
@GauthamM - Stream<Entry<K,V>> 不是一个 Stream<E>。另一个例子是 List<String> 不是一个 List<Object> - Arvind Kumar Avinash
@ArvindKumarAvinash https://dev59.com/RL_qa4cB1Zd3GeqPInyT#66198058 这是我的研究结果。我尝试删除了UnmodifiableEntrySetUnmodifiableCollection中重写的stream,这样stream调用将会到Collection中,spliterator调用将会调用UnmodifiableEntrySet中的重写版本。从您的角度来看,我的理解是返回的流将是一个通用流,而不是Entry流。如果是这样,那么我就不能在该流上调用map(Entry::value)。但我确实能做到。请也审查一下我的答案。 - Gautham M
2
我昨天误读了你的问题。如果你看一下stream的实现,你会注意到它已经在7个地方被重写了。相关的重写在UnmodifiableCollection中,然后再次在UnmodifiableEntrySet中。UnmodifiableCollection#stream基本上是将调用委托给传递的任何Collection。如果在UnmodifiableEntrySet中没有重写,那么调用将被委托给原始的Collection(可变的)。 - Aniket Sahrawat
2个回答

4
以下Java Doc /程序来自于 openjdk 14 2020-03-17
重写 spliteratorstream 的主要原因是确保 UnmodifiableEntrySet 的输入不被修改。
UnmodifiableEntrySet 的注释中可以看出:
“除了 UnmodifiableSet,我们还需要这个类,因为 Map.Entries 本身允许通过它们的 setValue 操作修改后备 Map。这个类很微妙:必须防范许多可能的攻击。”
首先,UnmodifiableEntrySet 扩展了 UnmodifiableSet,而 UnmodifiableSet 又扩展了 UnmodifiableCollection。 在 UnmodifiableCollection 中,代理模式用于避免修改后备 Collection c,大多数方法只是调用后备 Collection 方法,例如 spliteratorstream
    @Override
    public Spliterator<E> spliterator() {
        return (Spliterator<E>)c.spliterator();
    }

    @SuppressWarnings("unchecked")
    @Override
    public Stream<E> stream() {
        return (Stream<E>)c.stream();
    }

因此,如果UnmodifiableEntrySet没有覆盖那些方法,其行为将遵循UnmodifiableCollection实现,并且可以通过Entry#setValue公开和修改后端条目。
因此,spliteratorstream方法被重写,并引入了UnmodifiableEntrySetSpliterator来使用UnmodifiableEntry包装所有访问后端条目,确保条目无法被修改。
为什么UnmodifiableCollection要覆盖stream?
似乎没有必要在UnmodifiableCollection中覆盖stream,因为我们可以只使用Collection中的默认实现(通过spliterator创建流)。 但作者决定使用支持Collection c stream方法的后备stream方法覆盖stream,其中一个可能的原因是支持Collection的基础stream方法可能出于性能原因而被覆盖,例如Collections.CopiesList,或其spliterator方法不满足根据Collection#stream的要求。
此方法应在spliterator()方法无法返回不可变,并发或延迟绑定的spliterator时进行覆盖。 (有关详细信息,请参见spliterator()。)

1
@Gautham M,我同意这并不是真正必需的。但是作者决定默认使用支持Collection c stream方法,我认为其中一个原因是c可能会覆盖stream方法以提高性能,例如Collections.CopiesList - samabcde
1
@GauthamM 我相信samabcde的答案是正确的,实际上需要覆盖UnmodifiableEntrySet.stream()。我不确定为什么你的例子没有起作用。我修改了我的JDK构建以删除该覆盖,流包含可变条目,从而允许修改不可修改的映射。 - Stuart Marks
3
@GauthamM 哦,我现在理解你的问题了。也许可以不重写UnmodifiableCollection.stream,但这可能会在维护过程中使代码变得脆弱。包装器集合的一般规则是,它们应该覆盖 _所有内容_,以便可以验证它们保护其不变量,而无需检查整个类层次结构。 - Stuart Marks
@samabcde 没错。您能否将第二条评论中的详细信息也添加到答案中呢? - Gautham M
@StuartMarks 除此之外,samabcde提到的其他类中覆盖stream以提高性能,也使得在UnmodifiableCollection中覆盖它成为必要。 - Gautham M
显示剩余2条评论

0

Collections.java 的内容复制到一个新类 CollectionsCopy.java 中后,我尝试从 UnmodifiableEntrySet 中删除 stream 方法,然后进行了一些调试,并成功得出了答案。

UnmodifiableEntrySet 扩展了 UnmodifiableSetUnmodifiableSet 又扩展了 UnmodifiableCollection,而 UnmodifiableCollection 实现了 Collection 接口。由于 UnmodifiableCollection 有一个字段 final Collection<? extends E> c;,因此它必须重写 stream() 方法如下:

public Stream<E> stream() {
    return (Stream<E>)c.stream();
}

UnmodifiableEntrySet 的构造函数中,Set<? extends Map.Entry<? extends K, ? extends V>> 对象被强制转换为原始的 Set 类型,并传递给 super 构造函数。
super((Set)s);

代码附带的注释是:
需要转换为原始类型以解决类型系统中的限制。
因此,当我从我的 CollectionsCopy.java 中删除了 UnmodifiableEntrySet 中的 stream() 时,由于原始类型转换,被调用的 spliterator 方法是 Set 中的方法而不是 UnmodifiableEntrySet 中的方法。这是因为 UnmodifiableCollection 中的字段 c 将是 Set 而不是 UnmodifiableEntrySet,当从 UnmodifiableCollection.stream() 调用 c.stream() 时,它将调用 Set.spliterator()。因此,有必要让 UnmodifiableEntrySet 覆盖 UnmodifiableCollection 的 stream() 实现。
另外,我尝试删除 UnmodifiableCollection 中覆盖的 stream()。这次,它按预期工作,因为 spliterator 是从 UnmodifiableEntrySet 中调用的。

编辑: 根据一些评论,建议返回的流从Collection不会是一个Entry流,而只是一个Stream。但在这种情况下,我不应该能够在流上调用Entry类的任何方法。但我确实能够在该流上调用map(Entry::getValue)。因此,返回的流确实是Entry类型的流。


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