【问题标题】:Are Java's collections interface and class hierarchy ill done?Java 的集合接口和类层次结构做得不好吗?
【发布时间】:2018-10-20 09:14:35
【问题描述】:

我知道在 Java 中,LinkedList class implements both Deque and List 接口。 这让我有些困惑。

在计算机科学教学大纲中,我从未被告知队列可以是一个列表,或者更准确地说,队列可以表现得像一个列表。也就是说,列表可以做一些事情,但队列不能。但是该列表可以表现得像一个队列。比如List接口有the following methods

add(E e)
add(int index, E element)

但是Queuehas only the following:

add(E e)

很明显Queue 不允许在特定索引处插入,而List 允许插入。与Queue.remove()List.remove(int index)List.get(int index)Queue.peek() 等其他操作的情况相同。 也就是说,list 是一种更通用的数据结构,可以模拟Queue

现在能够模拟与拥有合同子集不同。也就是说,Queue 不允许 List 的某些操作(索引)并且只允许以特定方式完成某些操作(仅在尾部插入并仅从头部移除)。所以Queue 并没有真正“添加”到List 的合同。这正是为什么Queue 没有在Java 集合框架中扩展List,而是都扩展Collection 接口的原因。我相信这也是为什么任何类都实现两者都不正确的原因,因为Queue 的合同与List 的合同冲突(这就是为什么他们分别从Collection 接口分叉出来的原因)。但是,LinkedList 实现了这两个接口。

我也遇到过this 回答:

LinkedList 实现恰好满足Deque 契约,那么为什么不让它实现接口呢?

我仍然不明白我们怎么说“LinkedList 实现恰好满足Deque 合同”。队列的概念不允许在任意索引处插入。因此,Queue 接口没有这样的方法。

但是,我们只能通过接口强制执行合同,不能禁止某些方法的实现。作为列表(名称中有“List”),我觉得在LinkedList 中有队列方法peek()pop()add(int index, E element) 是不正确的。

我相信,相反我们应该有单独的类LinkedQueue,它可以有队列的链接实现,类似于LinkedBlockingQueue,它包含BlockingQueue 的链接实现。

还要注意LinkedList 是唯一继承自列表和队列家族的类,也就是说,没有其他类同时实现ListQueue (AFAIK)。这是否表明LinkedList 做得不好?

我是不是完全错了,而且是在不必要地思考?

【问题讨论】:

  • Linked List 实现了 Deque 和 List。这并不意味着 Deque 是一个列表。相反,Deque 和 List 都可以实现为 Linked List。
  • 当前编写的LinkedList 的数据结构代表所有这些接口的有效实现。因此,很自然地宣布它是所有这些。你的第一段错了。 Queue 实际上没有 List 拥有的方法。但这就是为什么我们没有Queue implements ListLinkedList 是它们两者,这并不意味着 Queue 现在是 List
  • 这个问题问得很好,所以请投我赞成票。不知道为什么人们会拒绝投票,可能是因为他们认为这愚蠢(但这不是拒绝投票的正当理由)。
  • 任何接口都不会阻止实现支持更多操作。 (除非重载方法发生冲突)
  • 答案是说你没有抓住重点,但我认为你是绝对正确的。做class A implements B, C并不意味着A可以被认为是B或C,它意味着它同时是B和C。这里有一个队列类型的violated invariant(你不能修改中间的元素),所以它是不正确的子类型。应该有的实际参数是“队列是否要求元素不能在中间修改或仅在 FIFO 中可以在末端修改”。

标签: java collections


【解决方案1】:

你完全忽略了programming to interface 的意义。

如果你需要Queue,你永远不要写:

LinkedList<String> queue = new LinkedList<>();

因为,您是对的,这将允许您使用非队列方法。相反,您可以像这样对界面进行编程:

Queue<String> queue = new LinkedList<>();

现在您只能访问 6 个Queue 方法(以及所有Collection 方法)。因此,即使LinkedList 实现了更多方法,您也无法再访问它们。

因此,如果您需要队列,请选择最适合您所需的性能、存储和访问特性的Queue 接口的实现,例如


我从来没有被告知队列可以是一个列表,或者更准确地说,队列可以表现得像一个列表。

请记住,implements 定义了 behaves like 关系。 LinkedList 表现得像 ListLinkedList 表现得像 DequeLinkedList 表现得像 Queue

但仅仅因为 LinkedList 的行为与所有这些相似,并不意味着 List 表现得像 QueueQueue 表现得像 List。他们没有。

behaves like 关系只有一种方式。

【讨论】:

  • 现在我猜为什么LinkedList 没有实现Stack 接口,因为它也有push()pop() 方法。当然,linkedListObj.push() 听起来是错误的。这和java.util.Stack有关吗?
  • @anir 是的,不幸的是,他们将Stack 实现为一个类,而不是一个接口,这可以追溯到一开始,而我们现在坚持使用它。正如Stack 的javadoc 所说:Deque 接口及其实现提供了一组更完整和一致的 LIFO 堆栈操作,应优先使用此类。”
  • 我不同意这个答案,因为它暗示substitution does not need to preserve invariants。如果我可以用 LinkedList 替换 Queue ,然后违反其在 FIFO 中访问元素的不变性,那么就 OOP 纯度而言,这是不正确的。这是一个完全有效和合理的权衡,但它仍然不是一个应该被忽略的权衡,我认为说 OP“完全没有抓住重点”是不礼貌的,因为他们对此感到担忧。
  • 我同意这个答案。更简单的语言不提供像 Queue&lt;String&gt; queue = new LinkedList&lt;&gt;(); 这样的东西,在这种情况下,人们可以(也是未来)选择几个实现类,并且仍然可以针对干净的 API (Queue) 工作。在我那个时代,计算机科学只将 Stack 视为类型,并且只进行了一种实现;然后可能是并发实现。但是重用类如 LinkedList 并不是一个主题。 Java 有很多不错的设计选择,当然也有很多弱点。
【解决方案2】:

@Andreas 的回答非常好,所以我的目标是你关于你被教导或未被教导的论点:

在计算机科学课程中,我从来没有被告知队列可以是一个列表,或者更准确地说,队列可以像一个列表一样工作

队列不仅仅是任何列表,而是一种特殊的列表,具有自己的特殊属性和约束。

也就是说,列表可以做一些事情,但队列不能。

不,List 无能为力。它提供了由一个类实现的可能性,并且如果该类决定实现它们,那么该类可以做所有这些事情。

但列表可以像队列一样工作。

不,List 不正常;它只建议实现它的行为和类可以接受它们的全部或一部分,或者它们可以定义新的。

【讨论】:

  • “队列是一种特殊的列表” 并非总是如此,例如PriorityQueue 是一个堆,而不是一个列表。 --- "List 不正常" 这就是它唯一可以做的事情:BehaveList 接口是行为契约,例如“如果你调用add(2, "X"),结果一定是"X"是列表中的第三个值”。任何实现接口的类都必须按照接口描述的方式“表现”。哪个类实现List 无关紧要,List 按照约定行事。
  • @Andreas List 接口是一种行为契约,你说过。 List 没有行为,因为它不是真实的,它的实现是真实的并且可以行为。至于 PriorityQueue:我在回答中所说的不是 Java 特定的,而是学术性的。无论任何语言决定如何实现其机制,优先级队列和任何队列的概念都是列表的子集。
  • 如果我的代码有List,那是非常真实的。我可能不知道它是如何实现(实现)的,但如果它不是真实的,我将无法使用它,它的行为与我预期的完全一样。 List 接口 声明定义了一个合约,但一个列表List 接口的一个对象)是真实的并且它的行为。 --- 我的第一条评论是想说你的术语令人困惑/误导。
  • @Andreas 那么我们在争论什么?在我提到List 时的回答中,我指的是界面,因为这是OP 的问题。当然,实例化的 List 对象是真实的,因为它接受并实现 List 接口的 行为契约 中的所有内容。
  • “在计算机科学课程中,我从来没有被告知队列可以是一个列表,或者更准确地说,队列可以像一个列表一样表现......”在大学水平的课程中没有明确说明。相反,学生应该发展自己的理解力和心理技能来解决这些问题。
【解决方案3】:

LinkedList 是一个实现ListDeque 接口的类。这些接口中的每一个都定义了一个带有操作的契约,并且在这些契约中指定了这些操作必须执行的什么。但是,没有具体说明如何这些操作应该工作。

LinkedList 是一个实现ListDeque 接口的类。因此,尽管后缀 ListLinkedList 类名称的一部分,LinkedList 实际上既是 List 又是 Deque,因为它实现了在 @987654333 中定义的所有操作@ 和 Deque 接口。

所以LinkedList是一个List,它也是一个Deque。这并不意味着List 应该是Deque,或者Deque 应该是List

例如看下面的接口:

public interface BloodDrinker {

    void drinkBlood();
}

public interface FlyingInsect {

    void flyAround();
}

上面的每个接口都有一个操作并定义了一个合约。 drinkBlood 操作定义了 BloodDrinker 必须做什么,而不是如何做。同样适用于FlyingInsect:它的flyAround 操作定义了它必须做什么,而不是如何做。

现在考虑Mosquito 类:

public class Mosquito implements FlyingInsect, BloodDrinker {

    public void flyAround() {
        // fly by moving wings, 
        // buzzing and bothering everyone around
    }

    public void drinkBlood() {
        // drink blood by biting other animals:
        // suck their blood and inject saliva
    }
}

现在,这意味着Mosquito 既是FlyingInsect 又是BloodDrinker,但是为什么吸血者一定是飞虫,或者飞虫一定是吸血者?例如,吸血鬼是吸血者,但不是飞虫,而蝴蝶是飞虫,但不是吸血者。


现在,关于您关于 Queue 不允许某些 List 的操作(索引),并且只允许以 FIFO 方式在其末端添加/删除的论点......我不认为这个理由是正确,至少在 Java 集合框架的上下文中。 The contract of Deque 没有明确提到实现者永远无法在任何给定索引处添加/删除/检查元素。它只是说Deque 是:

支持两端元素插入和删除的线性集合。

它还说:

此接口定义方法以访问双端队列两端的元素。

(强调我的)。

几段之后,它确实明确表示:

List 接口不同,此接口不支持对元素的索引访问

(再次强调我的)。

这里的关键部分是不提供支持。它从不禁止实现者通过索引访问元素。只是不支持通过Deque 接口进行索引访问。

想想我上面的例子:为什么BloodDrinker 不允许它的实现者喝血以外的东西,即水?或者为什么 FlyingInsect 不允许其实现者以不同于飞行的方式移动,即步行?

最重要的是,只要这些合同不相互矛盾,实施就可以根据需要遵守任意数量的合同。正如它在 Java 中的措辞(一个非常谨慎和微妙的措辞,我必须承认),Deque 的合同与 List 的合同并不矛盾,因此可以完美地存在一个实现这两个接口的类,并且这个恰好是LinkedList

【讨论】:

    【解决方案4】:

    你是从一个弱前提开始的:

    我从来没有被告知队列可以是一个列表。

    让我们回到基础。那么什么是数据结构呢?以下是CLSR 处理该问题的方式1

    ...虽然数学集合是不变的,但由 算法可以随时间增长、缩小或以其他方式变化。

    在数学上,数据结构只是集合;动态集。从这个意义上说,队列可以是一个列表。其实CLSR(10.2-3)有一个问题,就是明确要求你用链表来实现队列。

    另一方面,面向对象编程是一种范式,它通过坚持关于问题和数据的特定哲学来帮助程序员解决问题。对象、接口和契约都是这一理念的一部分。使用这种范式可以帮助我们实现动态集合的抽象概念。然而,它有自己的行李,其中之一就是这里要问的问题。

    因此,如果您抱怨 Java 标准库中的数据结构没有严格遵守为基本数据结构定义的约定,那么您是对的。事实上,我们甚至不需要看java.util.Stack 就可以看到这个2。您还可以以任何您想要的方式推出自己的实现并使用它们而不是标准库集合。

    但要争辩说 Java 或其标准库已损坏或做得不好 - 一个非凡的主张 - 你需要非常具体地了解用例并清楚地展示如何库中的所谓缺陷会阻止您实现设计目标。


    1第三章介绍,p220

    2Sedgewick and Wayne 调用 java.util.Stack 一个“宽接口”(p 160),因为它允许随机访问堆栈元素;堆栈(在基本数据结构中定义)不应该具备的能力。

    【讨论】:

      【解决方案5】:

      您在这方面是完全正确的,完全没有错过重点。 Java 只是在正确性和易用性之间进行了权衡。让它同时实现这两个接口是简单的事情,也是对开发人员最有用的的事情。

      什么是子类型

      正确的(声音)子类型需要substitution 才能工作,这需要根据 LSP:

      超类型的不变量必须保存在子类型中。

      当我们在类型理论中说“LinkedList 是一个列表和一个队列”时,我们实际上是在说一个 LinkedList既是一个列表又是一个队列同时 并且不是 LinkedList 可以被认为是一个列表或一个队列。

      这里有一个队列类型的violated invariant(你不能修改它中间的元素),所以它是不正确的子类型。

      应该有的实际参数是“队列是否要求元素不能在中间修改或仅在FIFO中可以在末端修改”。

      有人可能会争辩说,队列的不变量只是您可以在 FIFO 问题中使用它,而不是您必须。这不是队列的常见交互方式。

      【讨论】:

      • 有多个Queue-实现不是 FIFO - 示例:DelayQueuePriorityQueue。 java 中的大多数Queue 实现允许通过其迭代器迭代和删除任意索引处的元素。然后是SynchronousQueue,它从未真正包含元素。 Queue-contract 的措辞允许所有这些。
      猜你喜欢
      • 2011-01-21
      • 2014-01-21
      • 1970-01-01
      • 2014-12-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-26
      • 2011-08-10
      相关资源
      最近更新 更多