Путем расширения класса Вектор разработчики Java смогли создать класс Стек быстро. Каковы отрицательные аспекты этого использования наследования, особенно для класса Стек?
Большое спасибо.
Одна проблема в том, что Stack - это класс, а не интерфейс. Это отличается от дизайна структуры коллекции, где ваше существительное обычно представлено как интерфейс (например, List, Tree, Set и т. Д.), И существуют определенные реализации (например, ArrayList, LinkedList). Если бы Java могла избежать обратной совместимости, то более правильным дизайном было бы иметь интерфейс Stack, а затем VectorStack в качестве реализации.
Вторая проблема заключается в том, что Stack теперь привязан к Vector, чего обычно избегают в пользу ArrayLists и т.п.
Третья проблема заключается в том, что вы не можете легко предоставить свою собственную реализацию стека, и что стеки поддерживают очень не-стековые операции, такие как получение элемента из определенного индекса, включая возможность исключений индекса. Как пользователь, вам также может потребоваться знать, находится ли вершина стека в индексе 0 или в индексе n. Интерфейс также раскрывает детали реализации, такие как емкость.
Из всех решений исходной библиотеки классов Java я считаю это одним из наиболее необычных. Сомневаюсь, что агрегирование было бы намного дороже наследования.
Наличие подкласса Stack
Vector
предоставляет методы, которые не подходят для стека, поскольку стек не является вектором (это нарушает Принцип замены Лискова ) .
Например, стек - это структура данных LIFO, но с помощью этой реализации вы можете вызвать методы elementAt
или get
для получения элемента по указанному индексу. Или вы можете использовать insertElementAt
, чтобы нарушить контракт стека.
Я думаю, что Джошуа Блох официально заявил, что наличие подкласса Stack
Vector
было ошибкой, но, к сожалению, я не могу найти ссылку.
В дополнение к основным действительным пунктам, упомянутым выше, еще одна большая проблема со стеком, унаследованным от Vector, заключается в том, что Vector полностью синхронизирован, поэтому вы получаете эти накладные расходы независимо от того, нужны они вам или нет (см. StringBuffer vs. StringBuilder). Лично я обычно использую ArrayDeque всякий раз, когда мне нужен стек.
Ну, Stack
должен быть интерфейсом.
Интерфейс Stack
должен определять операции, которые может выполнять стек. Тогда могут существовать различные реализации Stack
, которые в разных ситуациях работают по-разному.
Но, поскольку Stack
является конкретным классом, этого не может произойти. Мы ограничены одной реализацией стека.
Эффективное 2-е издание Java, пункт 16: Предпочтение композиции перед наследованием :
Наследование уместно только в тех случаях, когда подкласс действительно является подтипом суперкласс.Другими словами, класс B должен расширять только класс A только в том случае, если между двумя классами существует связь «есть». Если вы хотите, чтобы класс B расширил класс A , задайте себе следующий вопрос: действительно ли каждый B является A ? Если вы не можете честно ответить утвердительно на этот вопрос, B не следует расширять A . Если ответ отрицательный, часто бывает так, что B должен содержать частный экземпляр A и предоставлять меньший и более простой API; A не является неотъемлемой частью B , это просто деталь его реализации.
Есть ряд очевидных нарушений этого принципа в библиотеках платформы Java. Например, стек не является вектором, поэтому
Стек
не должен расширятьвектор
. Точно так же список свойств не является хеш-таблицей, поэтомуСвойства
не должны расширятьHashtable
. В обоих случаях композиция была бы предпочтительнее.
Книга содержит более подробные сведения и в сочетании с Правилом 17: Дизайн и документ для наследования или же запретить его , советует против чрезмерного использования и злоупотребления наследованием в вашем дизайне.
Вот простой пример, показывающий проблему стека
, допускающую не- стек
-подобное поведение:
Stack<String> stack = new Stack<String>();
stack.push("1");
stack.push("2");
stack.push("3");
stack.insertElementAt("squeeze me in!", 1);
while (!stack.isEmpty()) {
System.out.println(stack.pop());
}
// prints "3", "2", "squeeze me in!", "1"
Это грубое нарушение абстрактного типа данных стека.
В информатике стек - это абстрактный тип данных и структура данных «последний пришел, первый ушел» (LIFO).
Это нарушает самое первое правило, которое мы все узнали о наследовании: можете ли вы с невозмутимым видом сказать, что стек - это вектор? Ясно, что нет.
Другой более логичной операцией было бы использование агрегации, но лучшим вариантом IMO было бы сделать Stack интерфейсом, который мог бы быть реализован любой соответствующей структурой данных, подобной (но не точно такой же) тому, что C ++ STL делает.