Являются ли интерфейсы Java-коллекции и иерархия классов плохой?

Я узнал, что в Java класс LinkedList реализует интерфейсы Deque и List. И это несколько сбивало с толку.

В учебной программе по информатике я никогда не учил, что очередь может быть списком, или, точнее, очередь может вести себя как список. То есть, есть вещи, которые могут делать списки, но очереди не могут. Но список может вести себя как очередь. Например, интерфейс List имеет следующие методы:

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

Но Queue имеет только следующее:

add(E e)

Поэтому ясно, что Queue не допускается вставлять по определенному индексу, что разрешено в List. То же самое относится к другим операциям, таким как Queue.remove() vs. List.remove(int index), List.get(int index) и Queue.peek(). Другими словами, список представляет собой более обобщенную структуру данных и может эмулировать Queue.

Теперь способность эмулировать отличается от наличия подмножества контракта. То есть Queue запрещает определенные операции (индексирование) List и позволяет определенные операции выполняются только определенным образом (вставлять только в хвост и удалять только из головы). Таким образом, Queue действительно не делает "дополнение" к контрактам List. Именно поэтому Queue не расширяет рамки List in Java, но расширяет интерфейс Collection. Я считаю, что это также, почему это неправильно для любого класса, чтобы реализовать оба, как и Queue контракта конфликты с договором List (поэтому они раскошелиться из Collection интерфейса отдельно). Однако LinkedList реализует оба интерфейса.

Я также наткнулся на этот ответ:

Реализация LinkedList выполняется, чтобы удовлетворить контракт Deque, поэтому почему бы не реализовать его интерфейс?

Я до сих пор не понимаю, как мы можем сказать, что реализация LinkedList выполняется, чтобы удовлетворить контракт Deque. Понятие очереди не допускает вставки с произвольным индексом. Следовательно, интерфейс Queue не имеет таких методов.

Однако мы можем только заключать контракты через интерфейсы и не можем запретить реализацию определенных методов. Будучи списком (имея "Список" в его имени), я считаю неправильным иметь методы очереди peek(), pop() и add(int index, E element) в LinkedList.

Я полагаю, вместо этого у нас должен быть отдельный класс LinkedQueue который может связать реализацию для очереди, похожую на LinkedBlockingQueue которая содержит связанную реализацию BlockingQueue.

Также обратите внимание, что LinkedList является единственным классом, который наследуется от обоих семейств списков и очередей, т.е. Нет другого класса, который реализует как List и Queue (AFAIK). Может ли это быть свидетельством того, что LinkedList плохо сделано?

Я просто ошибаюсь и думаю излишне?

Ответ 1

Вам совершенно не хватает точки программирования для интерфейса.

Если вам нужна Queue, вы никогда не пишете:

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

Потому что, вы правы, это позволит вам использовать методы без очереди. Вместо этого вы программируете на интерфейс следующим образом:

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

Теперь у вас есть доступ к 6 методам Queue (и всем методам Collection). Итак, хотя LinkedList реализует больше методов, у вас больше нет доступа к ним.

Итак, если вам нужна очередь, вы выбираете реализацию интерфейса Queue которая наилучшим образом соответствует характеристикам производительности, хранения и доступа, которые вам нужны, например

  • LinkedList использует больше памяти, но сжимается при опустошении очереди.

  • ArrayDeque использует меньше памяти, но не сокращается.

  • PriorityQueue - это не-FIFO-очередь с приоритетом элемента.

  • ConcurrentLinkedQueue, ConcurrentLinkedDeque поддерживает многопоточный параллельный доступ.

  • и больше...


Мне никогда не учили, что очередь может быть списком, или, точнее, очередь может вести себя как список.

Помните, что implements определяет поведение, подобное отношениям. LinkedList ведет себя как List. LinkedList ведет себя как Deque. LinkedList ведет себя как Queue.

Но только потому, что LinkedList ведет себя как все, это не означает, что List ведет себя как Queue или что Queue ведет себя как List. Они не.

Ведет себя как отношение только в одном направлении.

Ответ 2

Ответ @Andreas превосходный, поэтому мой мишень нацелен на ваши аргументы о том, что вы были или не учили:

В учебной программе по информатике я никогда не учил, что очередь может быть списком или, точнее, очередь может вести себя как список

Очередь - это не просто список, а особый вид списка с его собственными специальными свойствами и ограничениями.

То есть, есть вещи, которые могут делать списки, но очереди не могут.

Нет, List ничего не может сделать. Он предоставляет возможности для реализации класса, и если этот класс решает их реализовать, то этот класс может делать все это.

Но список может вести себя как очередь.

Нет, List не ведет себя; это только предполагает, что поведение и классы, которые его реализуют, могут принимать все или их подмножество или определять новые.

Ответ 3

LinkedList - это класс, который реализует интерфейсы List и Deque. Каждый из этих интерфейсов определяет контракт с операциями, и в этих контрактах указывается, что должны делать эти операции. Однако не указано, как эти операции должны работать.

LinkedList - это класс, который реализует интерфейсы List и Deque. Таким образом, несмотря на то, что List суффиксов является частью имени класса LinkedList, LinkedList фактически является List и Deque, поскольку он реализует все операции, определенные в интерфейсах List и 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. В контракте Deque явно не упоминается, что разработчики никогда не смогут добавлять/удалять/проверять элементы в любом заданном индексе. Он просто говорит, что Deque:

Линейный набор, который поддерживает вставку и удаление элементов на обоих концах.

И он также говорит, что:

Этот интерфейс определяет методы доступа к элементам на обоих концах deque.

(Акцент мой).

Несколько абзацев позже он прямо говорит, что:

В отличие от интерфейса List, этот интерфейс не поддерживает индексированный доступ к элементам.

(Подчеркните еще раз).

Ключевая часть здесь не обеспечивает поддержку. Он никогда не запрещает разработчикам доступ к элементам через индексы. Это просто, что индексированный доступ не поддерживается через интерфейс Deque.

Подумайте о моем примере выше: почему BloodDrinker запретил своим разработчикам пить что-то другое, кроме крови, то есть воды? Или почему FlyingInsect запрещает своим разработчикам двигаться не так, как летать, то есть ходить?

Итог, реализация может придерживаться столько контрактов, сколько пожелает, если эти контракты не противоречат друг другу. И, как сказано в Java (очень осторожная и тонкая формулировка, я должен признать), контракт Deque не противоречит контракту List, поэтому вполне может существовать класс, который реализует оба интерфейса, и это, случается, LinkedList.

Ответ 4

Вы совершенно правы в этом и не пропустите точку вообще. Java просто сделала компромисс между правильностью и легкостью. Сделать его реализация обоих интерфейсов было непростой задачей, и тот, который был наиболее полезен для разработчиков.

Что означает подтипирование

Правильный (звуковой) подтипирование требует замены на работу, которая требуется в соответствии с LSP:

Инварианты супертипа должны быть сохранены в подтипе.

Когда мы говорим в теории типов "LinkedList - это список и очередь", мы на самом деле говорим, что LinkedList одновременно является списком и очередью, а не тем, что LinkedList можно рассматривать как список или очередь,

Существует нарушенный инвариант типа очереди здесь (что вы не можете изменять элементы в его середине), поэтому это неправильный подтипирование.

Фактический аргумент, который должен иметься, заключается в том, "требует ли очередь, чтобы элементы не могли быть изменены в середине или только чтобы их можно было изменить в концах FIFO".

Можно утверждать, что инварианты Очереди - это только то, что вы можете использовать ее в FIFO, а не в том, что вам нужно. Это не общая интерпертация очереди.

Ответ 5

Вы начинаете с слабого помещения:

Мне никогда не учили, что очередь может быть списком.

Вернемся к основам. Итак, каковы структуры данных? Вот как CLSR подходит к этому вопросу 1:

... В то время как математические множества неизменны, множества, управляемые алгоритмами, могут расти, сокращаться или иным образом меняться со временем.

Математически структуры данных являются просто множествами; динамические множества. В этом смысле очередь может быть списком. На самом деле в CLSR (10.2-3) есть проблема, которая явно требует, чтобы вы реализовали очередь, используя связанный список.

С другой стороны, объектно-ориентированное программирование - это парадигма, которая помогает программистам решать проблемы, придерживаясь определенной философии проблемы и данных. Объекты, интерфейсы и контракты являются частью этой философии. Использование этой парадигмы помогает нам реализовать абстрактную концепцию динамических множеств. Тем не менее, он поставляется со своим собственным багажом, один из которых является самой проблемой, заданной здесь.

Поэтому, если вы жалуетесь, что структуры данных в стандартной библиотеке Java не строго придерживаются конвенций, определенных для элементарных структур данных, вы правы. На самом деле нам даже не нужно смотреть дальше, чем java.util.Stack чтобы увидеть это 2. Вам также разрешено развертывать свою собственную реализацию любым способом и использовать их вместо стандартных коллекций библиотек.

Но утверждать, что Java или его стандартная библиотека, если на то пошло, сломаны или плохо сделаны - экстраординарный claim-, вам нужно быть очень конкретным в отношении использования и четко продемонстрировать, как предполагаемый недостаток в библиотеке мешает вам достичь целей дизайна.


1 Введение в главу III, стр220

2 Sedgewick и Wayne вызывают java.util.Stack "широкий интерфейс" (стр. 160), поскольку он позволяет произвольный доступ к элементам стека;то, как полагают, не может быть установлен стек -as, определенный в элементарных данных structures-.