Почему коллекции Java были реализованы с помощью «дополнительных методов» в интерфейсе?

69

Во время моей первой реализации, расширяющей инфраструктуру Java-коллекции, я был очень удивлен, увидев, что интерфейс коллекции содержит методы, объявленные как необязательные. Ожидается, что разработчик выдаст исключение UnsupportedOperationException, если оно не поддерживается. Это сразу показалось мне плохим выбором дизайна API.

Прочитав большую часть превосходной книги Джошуа Блоха «Эффективная Ява», а затем узнав, что он может нести ответственность за эти решения, она, похоже, не согласилась с принципами, изложенными в книге. Я думаю, что объявление двух интерфейсов: Collection и MutableCollection, расширяющих Collection с помощью «необязательных» методов, привело бы к гораздо более поддерживаемому клиентскому коду.

Там отличный обзор вопросов здесь .

Была ли веская причина, почему вместо реализации двух интерфейсов были выбраны дополнительные методы?

glenviewjeff
источник
ИМО, нет веской причины. C ++ STL был создан Степановым в начале 80-х годов. Хотя его удобство использования ограничивалось неудобным синтаксисом шаблонов C ++, он является образцом последовательности и удобства использования по сравнению с классами коллекций Java.
Кевин Клайн
Был C ++ STL, «портирующий» на Java. Но это было в 5 раз больше в классе. С этим умом я не куплюсь на то, что больший является более последовательным и пригодным для использования. docs.oracle.com/javase/6/docs/technotes/guides/collections/…
m3th0dman
@ m3th0dman Это намного больше в Java, потому что у Java нет ничего эквивалентного по мощности шаблонам C ++.
Кевин Клайн
Может быть , это просто какой - то странный стиль , который я разработал, но мой код , как правило , чтобы рассматривать все коллекции , как «только для чтения», (точнее, только то , что вы считаете , или на которых вы итерацию) , за исключением для нескольких методов , которые фактически создают коллекцию. Вероятно, хорошая практика в любом случае (особенно для параллелизма). И дополнительные методы, на которые жалуются многие, никогда не были для меня настоящей проблемой. Также ночные кошмары с «супер» и «расширяет» никогда не были (большой) проблемой. Просто интересно, если другие используют эту общую практику?
user949300

Ответы:

28

FAQ дает ответ. Короче говоря, они увидели потенциальный комбинаторный взрыв необходимых интерфейсов с изменяемым, неизменяемым представлением, только для удаления, только для добавления, фиксированной длины, неизменяемым (для потоков) и так далее для каждого возможного набора реализованных методов параметров.

чокнутый урод
источник
6
Всего этого можно было бы избежать, если бы в Java было constключевое слово типа C ++.
Этьен де Мартель
@Etienne: Лучше было бы метаклассы, как Python. Тогда вы могли бы программно построить комбинаторный ряд интерфейсов. Проблема с сопзЬ является то , что она дает вам только один или два измерения: vector<int>, const vector<int>, vector<const int>, const vector<const int>. Пока все хорошо, но затем вы пытаетесь реализовать графы, и вы хотите сделать структуру графов постоянной, но атрибуты узла можно изменять и т. Д.
Нил Г.
2
@Etienne, еще одна причина, чтобы добавить «Learn Scala» в мой список дел!
glenviewjeff
2
Я не понимаю, почему они не canсоздали метод, который бы проверял, возможна ли операция? Это сделало бы интерфейс простым и быстрым.
Мердад
3
@ Этьен де Мартель Нонсенс. Как это поможет в комбинаторном взрыве?
Том Хотин - tackline
10

Для меня это звучит так, будто тогда Interface Segregation Principleне было так хорошо изучено, как сейчас; этот способ выполнения действий (т. е. ваш интерфейс включает в себя все возможные операции, и у вас есть «вырожденные» методы, которые генерируют исключения для тех, которые вам не нужны) был популярен до того, как SOLID и ISP стали стандартом де-факто для качественного кода.

Уэйн Молина
источник
2
Почему отрицательный голос? Кому-то не нравится провайдер?
Уэйн Молина
Стоит также отметить, что в средах, которые поддерживают дисперсию, существует ОГРОМНОЕ преимущество в отделении тех аспектов интерфейса, которые могут быть ковариантными или контравариантными от тех, которые являются принципиально инвариантными. Даже при отсутствии такой поддержки, в средах, которые не используют стирание типов, было бы целесообразно разделять аспекты интерфейса, которые не зависят от типа (например, позволяя получить Countколлекцию, не беспокоясь о том, какие типы элементов она содержит), но в основанных на стирании типов средах, таких как Java, это не такая проблема.
суперкат
4

Хотя некоторые люди могут ненавидеть «дополнительные методы», они могут во многих случаях предлагать лучшую семантику, чем сильно разделенные интерфейсы. Среди прочего, они учитывают возможности того, что объект может получить способности или характеристики в течение своего времени жизни или что объект (особенно объект-обертка) может не знать, когда он сконструирован, о каких точных способностях он должен сообщать.

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

Например, если коллекция поддерживает запись элемента по индексу или добавление элементов в конце, но не поддерживает вставку элементов в середине, тогда для кода, который хочет инкапсулировать его в оболочку, которая будет регистрировать все выполненные над ним действия, потребуется версия Оболочки журналирования, которая обеспечивала точную комбинацию поддерживаемых способностей, или, если ни одна из них не была доступна, пришлось бы использовать оболочку, которая поддерживала либо добавление, либо запись по индексу, но не оба. Если, однако, унифицированный интерфейс коллекции предоставил все три метода как «необязательные», но затем включил методы, чтобы указать, какой из дополнительных методов будет использоваться, то единственный класс-оболочка может обрабатывать коллекции, которые реализуют любую комбинацию функций. Когда его спросят, какие функции он поддерживает, оболочка может просто сообщить, что поддерживает инкапсулированная коллекция.

Обратите внимание, что существование «необязательных способностей» может в некоторых случаях позволять агрегированным коллекциям реализовывать определенные функции способами, которые были бы гораздо более эффективными, чем было бы возможно, если бы способности определялись существованием реализаций. Например, предположим, что concatenateметод использовался для формирования составной коллекции из двух других, первым из которых оказался ArrayList с 1 000 000 элементов, а последним был набор из двадцати элементов, который можно было повторять только с самого начала. Если в составной коллекции запрашивается 1 000 013-й элемент (индекс 1 000 012), он может спросить у ArrayList, сколько элементов в нем содержится (т.е. 1 000 000), вычесть это из запрошенного индекса (получая 12), прочитать и пропустить двенадцать элементов из второго коллекция, а затем вернуть следующий элемент.

В такой ситуации, даже несмотря на то, что у составной коллекции не было бы мгновенного способа возврата элемента по индексу, запрос составной коллекции для 1 000 013-го элемента все равно был бы намного быстрее, чем чтение 1 000 013 элементов из нее по отдельности и игнорирование всех, кроме последнего один.

Supercat
источник
@kevincline: Вы не согласны с приведенной формулировкой? Я бы посчитал, что разработка средств, с помощью которых реализации интерфейса могут описывать свои основные характеристики и возможности, является одним из наиболее важных аспектов проектирования интерфейса, хотя на него часто не обращают особого внимания. Если разные реализации интерфейса будут иметь разные оптимальные способы выполнения определенных общих задач, у клиентов, которые заботятся о производительности, должны быть средства для выбора наилучшего подхода в сценариях, где это будет иметь значение.
суперкат
1
извините, неполный комментарий. Я собирался сказать, что «способы расспросить коллекцию» ... переносят то, что должно быть проверкой во время компиляции, во время выполнения.
Кевин Клайн
@kevincline: В тех случаях, когда клиент должен иметь определенную способность и не может жить без нее, могут быть полезны проверки во время компиляции. С другой стороны, существует много ситуаций, когда коллекции могут обладать способностями, выходящими за рамки возможностей компиляции. В некоторых случаях может иметь смысл иметь производный интерфейс, в контракте которого указано, что все законные реализации производного интерфейса должны поддерживать конкретные методы, которые являются необязательными в базовом интерфейсе, но в тех случаях, когда код сможет обрабатывать что-либо независимо от того, работает ли он или нет у него есть какая-то особенность ...
суперкат
... но будет полезна возможность его существования, лучше, чтобы код спрашивал объект, поддерживает ли он функцию, чем спрашивал систему типов, обещает ли тип объекта поддержку функции. Если конкретный «конкретный» интерфейс будет широко использоваться, AsXXXв базовый интерфейс можно включить метод, который будет возвращать объект, для которого он вызывается, если он реализует этот интерфейс, возвращать объект-обертку, который поддерживает этот интерфейс, если это возможно, или генерировать исключение, если нет. Например, ImmutableCollectionинтерфейс может потребовать по контракту ...
суперкат
2
В вашем предложении есть проблема: шаблон «спроси потом» имеет проблему с параллелизмом, если возможности объекта могут измениться в течение срока его службы. (Если вы будете иметь , чтобы в любом случае риск исключения, вы могли бы также не стал спрашивать ...)
jhominal
-1

Я бы отнес это к первоначальным разработчикам, просто не зная тогда лучшего. Мы прошли долгий путь в ОО-дизайне с 1998 года или около того, когда впервые были выпущены Java 2 и Коллекции. То, что кажется очевидным плохим дизайном, не было таким очевидным в первые дни ООП.

Но это, возможно, было сделано, чтобы предотвратить дополнительное использование. Если бы это был второй интерфейс, вам пришлось бы приводить экземпляры своих коллекций для вызова тех необязательных методов, что также довольно уродливо. Как сейчас, вы сразу же поймаете исключение UnsupportedOperationException и исправите свой код. Но если бы было два интерфейса, вам пришлось бы использовать instanceof и приведение повсеместно. Возможно, они считали это действительным компромиссом. Также в начале двухдневной Java-версии на экземпляр instanceof приходилось неодобрительно из-за его низкой производительности, возможно, они пытались предотвратить его чрезмерное использование.

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

Jberg
источник
7
Какой кастинг? Если метод возвращает вам a, Collectionа не a MutableCollection, это явный признак того, что он не предназначен для изменения. Я не знаю, почему кто-то должен был их разыграть. Наличие отдельных интерфейсов означает, что вы получите такие ошибки во время компиляции, а не получите исключение во время выполнения. Чем раньше вы получите ошибку, тем лучше.
Этьен де Мартель
1
Поскольку это менее гибко, одним из самых больших преимуществ библиотеки коллекций является то, что вы можете возвращать интерфейсы высокого уровня везде и не беспокоиться о фактической реализации. Если вы используете два интерфейса, вы теперь более тесно связаны. В большинстве случаев вы просто хотите вернуть List, а не ImmutableList, потому что вы обычно хотите оставить это вызывающему классу для определения.
Jberg
6
Если я дам вам коллекцию только для чтения, то это потому, что я не хочу, чтобы она была изменена. Бросать исключение и полагаться на документацию - это чертовски похоже на патч. В C ++ я бы просто возвращал constобъект, мгновенно сообщая пользователю, что объект нельзя изменить.
Этьен де Мартель
7
1998 год не был "первыми днями ОО-дизайна".
quant_dev