【问题标题】:Why doesn't Java Map extend Collection?为什么 Java Map 不扩展 Collection?
【发布时间】:2011-02-08 18:11:25
【问题描述】:

Map<?,?> 不是Collection<?>,这让我很惊讶。

我认为如果这样声明它会很有意义:

public interface Map<K,V> extends Collection<Map.Entry<K,V>>

毕竟Map&lt;K,V&gt;Map.Entry&lt;K,V&gt; 的集合,不是吗?

那么是否有充分的理由不这样做?


感谢 Cletus 提供最权威的答案,但我仍然想知道为什么,如果您已经可以将 Map&lt;K,V&gt; 视为 Set&lt;Map.Entries&lt;K,V&gt;&gt;(通过 entrySet()),它不仅仅扩展了该接口。

如果MapCollection,那么元素是什么?唯一合理的答案是“键值对”

没错,interface Map&lt;K,V&gt; extends Set&lt;Map.Entry&lt;K,V&gt;&gt; 会很棒!

但这提供了一个非常有限的(并且不是特别有用)Map 抽象。

但如果是这样,那么为什么接口指定了entrySet?它一定是有用的(而且我认为很容易为这个立场争论!)。

您不能询问给定键映射到什么值,也不能在不知道它映射到什么值的情况下删除给定键的条目。

我并不是说这就是Map 的全部内容!它可以并且应该保留所有其他方法(entrySet 除外,它现在是多余的)!

【问题讨论】:

  • 事件集> 实际上
  • 它只是没有。它基于一种意见(如设计常见问题解答中所述),而不是一个合乎逻辑的结论。如需另一种方法,请查看 C++ STL (sgi.com/tech/stl/table_of_contents.html) 中的容器设计,该设计基于 Stepanov 彻底而出色的分析。
  • OP 改变了他们的问题,我不确定 SO 规则是否允许。对于不耐烦的人,在@einpoklum 的回答中对后一个问题(为什么它不能从Set&lt;Map.Entry&lt;K,V&gt;&gt; 继承)有一个简洁的回答。但是对于最初的问题(如标题),我还没有在这里找到任何好的答案,除了常见问题解答中引用的by design

标签: java oop collections


【解决方案1】:

来自Java Collections API Design FAQ

为什么 Map 不扩展 Collection?

这是设计使然。我们觉得 映射不是集合和 集合不是映射。因此,它 扩展 Map 没有什么意义 Collection 接口(或副 反之亦然)。

如果地图是一个集合,那么什么是 元素?唯一合理的答案 是“键值对”,但这 提供了一个非常有限的(而不是 特别有用)地图抽象。 你不能问给定键的值是什么 映射到,也不能删除条目 对于给定的密钥,不知道是什么 它映射到的值。

集合可以扩展 地图,但这提出了一个问题: 什么是钥匙?真的没有 令人满意的答案,并强迫一个 导致界面不自然。

地图可以被视为集合( 键、值或对),以及这个事实 体现在三个“收藏 查看操作”在地图上(keySet, entrySet 和值)。虽然它是,在 原则上,可以将列表视为 a Map 将索引映射到元素, 这有一个讨厌的属性 从列表中删除一个元素 更改与每个关联的密钥 被删除元素之前的元素。 这就是为什么我们没有地图视图 对列表进行操作。

更新:我认为这句话回答了大部分问题。值得强调的是,条目集合不是特别有用的抽象。例如:

Set<Map.Entry<String,String>>

允许:

set.add(entry("hello", "world"));
set.add(entry("hello", "world 2"));

(假设一个 entry() 方法创建了一个 Map.Entry 实例)

Maps 需要唯一的键,所以这会违反这一点。或者,如果您在 Set 的条目上施加唯一键,那么在一般意义上,它并不是真正的 Set。这是一个Set,有更多限制。

您可以说Map.Entryequals()/hashCode() 关系纯粹是关键,但即使这样也有问题。更重要的是,它真的增加了任何价值吗?一旦您开始研究极端案例,您可能会发现这种抽象会崩溃。

值得注意的是,HashSet 实际上是作为HashMap 实现的,而不是相反。这纯粹是一个实现细节,但仍然很有趣。

entrySet() 存在的主要原因是为了简化遍历,这样您就不必遍历键然后查找键。不要将其视为Map 应该是Set 条目的初步证据(恕我直言)。

【讨论】:

  • 这次更新很有说服力,但确实entrySet()返回的Set视图确实支持remove,但不支持add(可能会抛出UnsupportedException)。所以我明白你的观点,但我也看到了 OP 的观点..(我自己的观点与我自己的回答中所述..)
  • Zwei 提出了一个很好的观点。由add 引起的所有这些复杂情况都可以像entrySet() 视图那样处理:允许某些操作而不允许其他操作。一个自然的反应当然是“什么样的Set不支持add?” ——好吧,如果entrySet() 可以接受这种行为,那为什么this 不能接受呢?话虽如此,我基本上已经确信为什么这不是我曾经认为的那么好,但我仍然认为它值得进一步辩论,如果只是为了丰富我自己对什么是良好的 API/OOP 设计。
  • FAQ 中引用的第一段很有趣:“我们觉得……因此……没有什么意义”。我不觉得“感觉”和“感觉”形成一个结论;o)
  • 可以收集来扩展地图?不确定,为什么作者认为Collection 扩展Map
  • @aderchox 如果你有一个Set&lt;Pair&lt;K,V&gt;&gt;,你不能问它是否包含特定的K,因为contains的合约要求如果特定对象是返回true在集合内,它必须是 Pair 的实例,具有相同的 K V 才能匹配。这同样适用于remove的合约。这正是entrySet() 返回的集合的行为,其containsremove 方法根据Map.Entry&lt;K,V&gt; 元素进行操作。这是一个与Map 本身完全不同的API,它有containsKeyremove(key) 等,这就是这一切的意义所在..
【解决方案2】:

cletus的答案很好,但我想添加一个语义的方法。将两者结合起来是没有意义的,考虑一下您通过集合接口添加键值对并且键已经存在的情况。 Map-interface 只允许一个值与键关联。但是,如果您使用相同的键自动删除现有条目,则集合在添加后的大小与之前相同 - 对于集合来说非常意外。

【讨论】:

  • 这就是Set 的工作方式,Set 确实 实现了Collection
【解决方案3】:

没错,interface Map<K,V> extends Set<Map.Entry<K,V>> 会很棒!

实际上,如果是implements Map&lt;K,V&gt;, Set&lt;Map.Entry&lt;K,V&gt;&gt;,那我倾向于同意.. 这似乎更自然。但这不是很好,对吧?假设我们有HashMap implements Map&lt;K,V&gt;, Set&lt;Map.Entry&lt;K,V&gt;LinkedHashMap implements Map&lt;K,V&gt;, Set&lt;Map.Entry&lt;K,V&gt; 等等……这一切都很好,但是如果你有entrySet(),没有人会忘记实现那个方法,而且你可以确定你可以获得任何 Map 的 entrySet,而如果你希望实现者实现了这两个接口,你就不会......

我不想拥有interface Map&lt;K,V&gt; extends Set&lt;Map.Entry&lt;K,V&gt;&gt; 的原因很简单,因为会有更多的方法。毕竟,它们是不同的东西,对吧?同样非常实用的是,如果我在 IDE 中点击map.,我不想看到.remove(Object obj).remove(Map.Entry&lt;K,V&gt; entry),因为我不能做hit ctrl+space, r, return 并完成它。

【讨论】:

  • 在我看来,如果有人可以在语义上声称“A Map is-a Set on Map.Entry's”,那么人们就会费心实现相关方法;如果一个人不能做出这样的陈述,那么一个人就不会 - 即它应该是关于麻烦的。
【解决方案4】:

我猜为什么是主观的。

在 C# 中,我认为 Dictionary 扩展或至少实现了一个集合:

public class Dictionary<TKey, TValue> : IDictionary<TKey, TValue>, 
    ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, 
    IDictionary, ICollection, IEnumerable, ISerializable, IDeserializationCallback

在 Pharo Smalltak 中也是如此:

Collection subclass: #Set
Set subclass: #Dictionary

但有些方法存在不对称性。例如,collect: 将采用关联(相当于条目),而 do: 采用值。他们提供了另一种方法keysAndValuesDo: 来按条目迭代字典。 Add: 接受关联,但 remove: 已被“抑制”:

remove: anObject
self shouldNotImplement 

所以它绝对是可行的,但会导致有关类层次结构的其他一些问题。

更好的是主观的。

【讨论】:

    【解决方案5】:

    虽然您已经获得了一些相当直接地涵盖了您的问题的答案,但我认为退后一步,更笼统地看待这个问题可能会很有用。也就是,不专门看 Java 库是怎么写的,而是看为什么会这样写。

    这里的问题是继承只对一种类型的共性进行建模。如果你挑出两件看起来都“像收藏”的东西,你大概能挑出 8 到 10 件它们有共同点的东西。如果您挑选出一对不同的“类似收藏”的东西,它们也会有 8 或 10 个共同点——但它们不会像第一对一样相同的 8 或 10 个东西.

    如果您查看十几个不同的“类似收藏”的东西,几乎每一个都可能与至少一个其他的有 8 或 10 个共同特征 - 但如果您查看共享的内容在每一个中,你几乎一无所有。

    这是一种继承(尤其是单继承)不能很好建模的情况。在哪些是真正的集合和哪些不是真正的集合之间没有明确的分界线——但如果你想定义一个有意义的 Collection 类,你就会坚持将其中的一些排除在外。如果您只留下其中的几个,您的 Collection 类将只能提供相当稀疏的接口。如果您省略更多内容,您将能够为其提供更丰富的界面。

    有些人也选择基本上说:“这种类型的集合支持操作 X,但你不能使用它,通过从定义 X 的基类派生,但尝试使用派生类的 X失败(例如,通过抛出异常)。

    这仍然留下了一个问题:几乎无论您遗漏了哪些以及添加了哪些,您都必须在哪些类在哪些类之间划清界限。无论你在哪里画出这条线,你都会在一些相当相似的事物之间留下一个清晰的、相当人为的划分。

    【讨论】:

      【解决方案6】:

      Java 集合已损坏。缺少一个接口,即 Relation 接口。因此,Map 扩展了 Relation 扩展了 Set。关系(也称为多映射)具有唯一的名称-值对。映射(又名“函数”)具有唯一的名称(或键),它们当然映射到值。序列扩展 Maps(其中每个键是一个大于 0 的整数)。 Bags(或多集)扩展 Maps(其中每个键是一个元素,每个值是该元素在包中出现的次数)。

      这种结构将允许一系列“集合”的交集、联合等。因此,层次结构应该是:

                                      Set
      
                                       |
      
                                    Relation
      
                                       |
      
                                      Map
      
                                      / \
      
                                   Bag Sequence
      

      Sun/Oracle/Java ppl - 下次请做好。谢谢。

      【讨论】:

      • 我想详细了解这些虚构接口的 API。不确定我是否想要一个键是整数的序列(又名列表);这听起来像是对性能的巨大打击。
      • 没错,一个序列就是一个列表——因此序列扩展了列表。我不知道你是否可以访问这个,但它可能会很有趣portal.acm.org/citation.cfm?id=1838687.1838705
      • @SoftwareMonkey 这里是另一个链接。最后一个链接是指向 API 的 javadoc 的链接。 zedlib.sourceforge.net
      • @SoftwareMonkey 我毫不掩饰地承认设计是我在这里的主要关注点。我假设 Oracle/Sun 的专家可以优化它。
      • 我倾向于不同意。如果您不能始终将域的非成员元素添加到您的集合中(如@cletus 回答中的示例),它如何保持“Map is-a Set”。
      【解决方案7】:

      Map&lt;K,V&gt; 不应扩展 Set&lt;Map.Entry&lt;K,V&gt;&gt;,因为:

      • 不能将具有相同密钥的不同Map.Entrys 添加到相同Map,但是
      • 可以使用相同的密钥将不同的Map.Entrys 添加到相同的Set&lt;Map.Entry&gt;

      【讨论】:

      • ...这是@cletus 答案第二部分的精髓的简洁陈述。
      • 阅读此答案后,我理解了@cletus 的答案,哈哈。谢谢:)
      【解决方案8】:

      如果您查看各自的数据结构,您可以很容易地猜到为什么Map 不是Collection 的一部分。每个Collection 存储一个值,而Map 存储键值对。因此Collection 接口中的方法与Map 接口不兼容。例如在Collection 我们有add(Object o)Map 中的这种实现是什么。在Map 中有这样的方法是没有意义的。相反,我们在Map 中有一个put(key,value) 方法。

      addAll()remove()removeAll() 方法的参数相同。所以主要原因是MapCollection存储数据的方式不同。 此外,如果您还记得Collection 接口实现了Iterable 接口,即任何具有.iterator() 方法的接口都应该返回一个迭代器,它必须允许我们迭代存储在Collection 中的值。现在,对于Map,这种方法会返回什么?键迭代器还是值迭代器?这也没有意义。

      我们可以通过多种方式迭代Map 中的键和值存储,这就是它成为Collection 框架的一部分的方式。

      【讨论】:

      • There are ways in which we can iterate over keys and values stores in a Map and that is how it is a part of Collection framework. 你能给出一个代码示例吗?
      • 这很好解释。我现在明白了。谢谢!
      • add(Object o) 的实现是添加一个Entry&lt;ClassA, ClassB&gt; 对象。 Map 可以被认为是 Collection of Tuples
      【解决方案9】:

      简单明了。 Collection 是一个只需要一个 Object 的接口,而 Map 需要两个。

      Collection(Object o);
      Map<Object,Object>
      

      【讨论】:

      • Nice Trinad,你的答案总是简单而简短。
      猜你喜欢
      • 2011-08-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-10-24
      • 2019-11-07
      • 2015-06-17
      • 2010-12-10
      相关资源
      最近更新 更多