В моем недавнем проекте я определил класс со следующим заголовком:
public class Node extends ArrayList<Node> {
...
}
Однако, после обсуждения с моим профессором CS, он заявил, что урок будет «ужасным для памяти» и «плохой практикой». Я не обнаружил, что первое особенно верно, а второе субъективно.
Мое обоснование такого использования заключается в том, что у меня была идея для объекта, который необходимо определить как нечто, обладающее произвольной глубиной, где поведение экземпляра может быть определено либо пользовательской реализацией, либо поведением нескольких подобных объектов, взаимодействующих , Это позволит абстрагировать объекты, физическая реализация которых будет состоять из множества взаимодействующих подкомпонентов.
С другой стороны, я вижу, как это может быть плохой практикой. Идея определения чего-либо как списка сама по себе не является простой или физически осуществимой.
Есть ли веская причина, почему я не должен использовать это в своем коде, учитывая мое использование для этого?
¹ Если мне нужно объяснить это дальше, я был бы рад; Я просто пытаюсь сделать этот вопрос лаконичным.
источник
ArrayList<Node>
, по сравнению с внедрениемList<Node>
?Ответы:
Честно говоря, я не вижу необходимости наследования здесь. Это не имеет смысла;
Node
этоArrayList
изNode
?Если это просто рекурсивная структура данных, вы просто напишите что-то вроде:
Что имеет смысл; узел имеет список дочерних или дочерних узлов.
источник
Item
иItem
работает независимо от списков.Node
этоArrayList
вNode
качествеNode
содержит 0 для многихNode
объектов. По сути, они одинаковы, но вместо того, чтобы быть списком похожих объектов, он имеет список похожих объектов. Первоначальный дизайн написанного класса заключался в убеждении, что любой из нихNode
может быть выражен как набор подпрограммNode
, что и в обеих реализациях, но этот, безусловно, менее запутан. +1Children
всего один или два раза, и, как правило, предпочтительнее выписывать его в таких случаях для ясности кода.Аргумент «ужасно для памяти» совершенно неверен, но это объективно «плохая практика». Когда вы наследуете от класса, вы не просто наследуете поля и методы, которые вас интересуют. Вместо этого вы наследуете все . Каждый метод, который он объявляет, даже если он бесполезен для вас. И самое главное, вы также наследуете все его контракты и гарантии, которые предоставляет класс.
Акроним SOLID обеспечивает некоторую эвристику для хорошего объектно-ориентированного дизайна. Здесь я nterface Сегрегация принцип (ISP) и L Иськов Замена Pricinple (LSP) есть что - то сказать.
Интернет-провайдер говорит нам, чтобы наши интерфейсы были как можно меньше. Но, наследуя от
ArrayList
, вы получаете много-много методов. Есть ли смыслget()
,remove()
,set()
(заменить), илиadd()
(вставка) дочерний узел с определенным индексом? Разумно ли этоensureCapacity()
из основного списка? Что это значит дляsort()
узла? Пользователи вашего класса действительно должны получитьsubList()
? Поскольку вы не можете скрыть ненужные методы, единственное решение состоит в том, чтобы ArrayList являлся переменной-членом и пересылать все методы, которые вам действительно нужны:Если вам действительно нужны все методы, которые вы видите в документации, мы можем перейти к LSP. LSP говорит нам, что мы должны иметь возможность использовать подкласс везде, где ожидается родительский класс. Если функция принимает в
ArrayList
качестве параметра, и мыNode
вместо этого передаем наш , ничего не должно измениться.Совместимость подклассов начинается с простых вещей, таких как сигнатуры типов. Когда вы переопределяете метод, вы не можете сделать типы параметров более строгими, поскольку это может исключить использование, допустимое для родительского класса. Но это то, что компилятор проверяет нас в Java.
Но LSP работает гораздо глубже: мы должны поддерживать совместимость со всем, что обещано документацией всех родительских классов и интерфейсов. В своем ответе , Линн обнаружила один такой случай , когда
List
интерфейс (который вы унаследовали черезArrayList
) гарантирует , какequals()
иhashCode()
методы должны работать. ДляhashCode()
вас даже дан определенный алгоритм, который должен быть реализован точно. Давайте предположим, что вы написали этоNode
:Это требует, чтобы
value
не мог внести свой вкладhashCode()
и не может влиятьequals()
.List
Интерфейс - что вы обещаете честь, унаследовав от него - требует ,new Node(0).equals(new Node(123))
чтобы быть правдой.Поскольку наследование от классов позволяет слишком легко случайно нарушить обещание, данное родительским классом, и поскольку оно обычно предоставляет больше методов, чем вы предполагали, обычно рекомендуется выбирать композицию вместо наследования . Если вы должны что-то наследовать, предлагается наследовать только интерфейсы. Если вы хотите повторно использовать поведение определенного класса, вы можете сохранить его как отдельный объект в переменной экземпляра, чтобы все его обещания и требования не были навязаны вам.
Иногда наш естественный язык предполагает наследственные отношения: автомобиль - это транспортное средство. Мотоцикл - это транспортное средство. Должен ли я определить классы
Car
иMotorcycle
что наследовать отVehicle
класса? Объектно-ориентированный дизайн - это не отражение реального мира именно в нашем коде. Мы не можем легко закодировать богатые таксономии реального мира в нашем исходном коде.Одним из таких примеров является проблема моделирования сотрудник-босс. У нас есть несколько
Person
s, каждый с именем и адресом. AnEmployee
естьPerson
и имеетBoss
. АBoss
такжеPerson
. Так я должен создатьPerson
класс, который наследуетсяBoss
иEmployee
? Теперь у меня есть проблема: начальник - тоже работник и другой начальник. Так что вроде какBoss
должно расширятьсяEmployee
. НоCompanyOwner
это,Boss
но неEmployee
? Любой вид графа наследования здесь как-то сломается.ООП - это не иерархии, наследование и повторное использование существующих классов, а обобщение поведения . ООП - это «У меня есть куча объектов, и я хочу, чтобы они выполняли определенную работу, и мне все равно, как». Вот для чего нужны интерфейсы . Если вы реализуете
Iterable
интерфейс для своего,Node
потому что хотите сделать его итеративным, это прекрасно. Если вы реализуетеCollection
интерфейс, потому что хотите добавить / удалить дочерние узлы и т. Д., Тогда это нормально. Но наследование от другого класса, потому что это дает вам все, чего нет, или, по крайней мере, нет, если вы не продумали его тщательно, как описано выше.источник
Расширение контейнера как таковое обычно считается плохой практикой. Там действительно очень мало причин, чтобы расширить контейнер вместо того, чтобы просто иметь его. Расширение контейнера себя просто делает его еще более странным.
источник
В добавление к тому, что было сказано, есть несколько специфическая для Java причина избегать такого рода структур.
Контракт
equals
метода для списков требует, чтобы список считался равным другому объектуИсточник: https://docs.oracle.com/javase/7/docs/api/java/util/List.html#equals(java.lang.Object)
В частности, это означает, что наличие класса, спроектированного как сам список, может сделать сравнение равенства дорогостоящим (и вычисления хеша также, если списки изменяемы), и если у класса есть несколько полей экземпляра, то их следует игнорировать при сравнении равенства ,
источник
По поводу памяти:
Я бы сказал, что это вопрос перфекционизма. Конструктор по умолчанию
ArrayList
выглядит следующим образом:Источник . Этот конструктор также используется в Oracle-JDK.
Теперь рассмотрите возможность создания односвязного Списка с вашим кодом. Вы только что успешно увеличили потребление памяти в 10 раз (точнее, даже чуть выше). Для деревьев это тоже может быть довольно плохо без особых требований к структуре дерева. Использование
LinkedList
альтернативного или другого класса должно решить эту проблему.Если честно, в большинстве случаев это всего лишь перфекционизм, даже если мы игнорируем количество доступной памяти. A
LinkedList
немного замедлит код в качестве альтернативы, так что в любом случае это компромисс между производительностью и потреблением памяти. Тем не менее, мое личное мнение по этому поводу было бы не тратить так много памяти таким образом, который можно так легко обойти, как этот.РЕДАКТИРОВАТЬ: разъяснения относительно комментариев (@amon, чтобы быть точным). Этот раздел ответа не касается вопроса наследования. Сравнение использования памяти производится по сравнению с односвязным списком и с лучшим использованием памяти (в реальных реализациях коэффициент может немного измениться, но он все еще достаточно велик, чтобы суммировать до некоторой степени потраченной памяти).
Относительно "Плохой практики":
Определенно. Это не стандартный способ реализации графика по одной простой причине: граф-узел имеет дочерние узлы-, не является в списке дети-узлы. Точное выражение того, что вы имеете в виду в коде, является основным навыком. Будь то в именах переменных или выражая структуры, подобные этой. Следующий пункт: поддержание интерфейсов минимальным: вы сделали каждый метод
ArrayList
доступным для пользователя класса посредством наследования. Нет способа изменить это, не нарушая всю структуру кода. Вместо этого сохранитеList
как внутреннюю переменную и сделайте необходимые методы доступными через метод-адаптер. Таким образом, вы можете легко добавлять и удалять функциональные возможности из класса, не все портя.источник
new Object[0]
используется всего 4 слова, anew Object[10]
будет использовать только 14 слов памяти.ArrayList
s занимают довольно много памяти.Это похоже на семантику составного паттерна . Он используется для моделирования рекурсивных структур данных.
источник
public class Node extends ArrayList<Node> { public string Item; }
версия наследования вашего ответа. Ваш проще рассуждать, но оба достигают некоторой вещи: они определяют недвоичные деревья.