2015-09-09 5 views
3

Вопрос оформлен под List, но он легко применим к другим в каркасе коллекций java.Уместно ли распространять список для добавления полей

Например, я бы сказал, что для создания нового подтипа List необходимо сохранить что-то вроде счетчика дополнений, поскольку оно является неотъемлемой частью операции списка и не изменяет его. список". Что-то вроде этого:

public class ChangeTrackingList<E> extends ArrayList<E> { 
    private int changeCount; 
    ... 
    @Override public boolean add(E e) { 
     changeCount++; 
     return super.add(e); 
    } 
    // other methods likewise overridden as appropriate to track change counter 
} 

Однако то, что о добавлении дополнительной функциональности из знаний о списке ADT, такие как хранение произвольных данных, связанных с элементом списка? Предполагая, что связанные данные были правильно обработаны, когда элементы добавлены и удалены, конечно. Что-то вроде этого:

public class PayloadList<E> extends ArrayList<E> { 
    private Object[] payload; 
    ... 
    public void setData(int index, Object data) { 
     ... // manage 'payload' array 
     payload[index] = data; 
    } 
    public Object getData(int index) { 
     ... // manage 'payload' array, error handling, etc. 
     return payload[index]; 
    } 
} 

В этом случае я переделал, что это «просто список», добавив не только дополнительные функции (поведение), но и дополнительное состояние. Разумеется, часть цели спецификации типа и наследования, но есть ли скрытое ограничение (табу, устаревание, плохая практика и т. Д.) На типы коллекций Java для их обработки специально?

Например, при ссылках на этот PayloadList в качестве java.util.List один из них будет мутировать и повторять как обычно и игнорировать полезную нагрузку. Это проблематично, когда дело доходит до чего-то вроде настойчивости или копирования, которое не ожидает, что List несут дополнительные данные, которые необходимо сохранить. Я видел много мест, которые принимают объект, проверьте, чтобы он был «списком», а затем просто обрабатывал его как java.util.List. Должны ли они вместо этого позволить произвольные вклады приложений для управления конкретными конкретными подтипами?

Поскольку эта реализация постоянно создавала проблемы в срединном разрезе (игнорируя поля подтипа), является ли плохая идея расширять коллекцию таким образом и всегда использовать композицию, когда есть дополнительные данные для управления? Или вместо этого задача сохранения или копирования для учета всех конкретных подтипов, включая дополнительные поля?

+2

Вы знаете, 'list' сам интерфейс, так что только интерфейс может продлить его. То, что вы делаете, это не совсем «создание чего-то, что является« списком », а скорее« создание специального типа «ArrayList». – RealSkeptic

+2

«Оплатить композицию над наследованием», «Дизайн и документ для наследования или запретить». См. «Эффективное Java 2-е издание», пункт 16, в котором обсуждается именно это. – Natix

+0

@RealSkeptic Я имел в виду «продлить» в смысле «is-a» или любой подтип, а не только с ключевым словом 'extends'. Конечно, 'ArrayList' - это конкретный класс, а' List' - это интерфейс; использование «ArrayList» было для примеров, а не для самого сердца вопроса. –

ответ

0

Один аспект, который вы должны учитывать, состоит в том, что все классы, которые наследуют от AbstractList, являются классов значений. Это означает, что они имеют значащие методы equals(Object) и hashCode(), поэтому два списка считаются равными, даже если они не являются одним и тем же экземпляром любого класса.

Кроме того, договор equals() от AbstractList позволяет сравнивать любой список с текущим списком, а не только с той же реализацией.

Теперь, если добавить элемент в значение для класса значения, когда вы расширяете его, вам нужно включить этот пункт значение в equals() и hashCode() методов. В противном случае вы разрешите двум спискам PayloadList с разными полезными нагрузками считаться «одинаковыми», когда кто-то использует их на карте, наборе или просто равном equals() сравнении в любом алгоритме.

Но на самом деле невозможно расширить класс значений, добавив элемент ценности!В итоге вы нарушите договор equals(), либо нарушая симметрию (A plain ArrayList, содержащий [1,2,3], вернет значение true по сравнению с PayloadList, содержащим [1,2,3] с полезной нагрузкой [a,b,c], но обратное сравнение не вернет true). Или вы нарушите транзитивность.

Это означает, что в основном единственный способ расширения класса значений - это добавление нецензурного поведения (например, список, который регистрирует каждые add() и remove()). Единственный способ избежать нарушения контракта - использовать композицию. И должен быть состав, который не реализует List вообще (потому что в других списках будут приниматься все, что реализует List и дает те же значения при его итерации).

Этот ответ основан на пункт 8 Effective Java, 2-е издание, Джошуа Блох

+0

Ах, вот оно. То, что я забыл, заключается в том, что для контракта API для «List» требуется «equals», чтобы быть истинным, если они имеют одинаковый размер, и все соответствующие пары элементов между списками равны. Таким образом, ответ на мой вопрос затем обобщается на простое выполнение контракта: если вы можете расширить класс и добавить состояние, подчиняясь контракту, то это нормально, поэтому нет ничего особенного в API коллекций. Это также объясняет, почему библиотеки безопасно принимают список, это просто набор элементов и не требует особых случаев для конкретных подтипов. Благодаря! –

0

Следующее - это всего лишь мнение, вопрос приглашает упрямые ответы (я думаю, что его граница не соответствует для SO).

Хотя ваш подход работоспособен в некоторых ситуациях, я бы утвердил его плохой дизайн, и он очень хрупкий. Его также довольно сложно покрыть все лазейки (возможно, даже невозможно, например, когда список сортируется с помощью других, чем List.sort).

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

Если вам действительно нужен тип списка ассоциаций, считайте его не экземпляром java.util.List, а специализированным типом со специализированным API. Таким образом, несчастные случаи невозможны, передавая их там, где ожидается простой список.

+0

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

3

Это исключительно вопрос мнения, но лично я бы посоветовал не распространять такие классы, как ArrayList практически во всех обстоятельствах, и предпочитаю композицию.

Даже ваш ChangeTrackingList довольно изворотливый. Что такое

list.addAll(Arrays.asList("foo", "bar")); 

do?Увеличивает ли он changeCount два раза или вообще не работает? Это зависит от того, использует ли ArrayList.addAll()add(), который представляет собой деталь реализации, о которой вам не о чем беспокоиться. Вам также необходимо будет поддерживать ваши методы в синхронизации с методами ArrayList. Например, в настоящее время addAll(Collection<?> collection)является реализован на вершине add(), но если они решили в будущем выпуске, чтобы проверить первый, если collection instanceof ArrayList, и если так использовать System.arraycopy непосредственно копировать данные, вы должны изменить свой метод addAll() только приращение changeCount по collection.size(), если коллекция ArrayList (в противном случае это делается в add()).

Кроме того, если метод всегда добавляется к List (это случилось с forEach() и stream(), например), это может вызвать проблемы, если вы используете это имя метода означает что-то другое. Это может произойти и при расширении классов abstract, но, по крайней мере, класс abstract не имеет состояния, поэтому вы вряд ли сможете нанести слишком большой урон, переопределив методы.

Я бы по-прежнему использовал интерфейс List, а в идеале - AbstractList. Что-то вроде этого

public final class PayloadList<E> extends AbstractList<E> implements RandomAccess { 

    private final ArrayList<E> list; 
    private final Object[] payload; 

    // details missing 
} 

Таким образом, у вас есть класс, который реализует List и делает использование ArrayList без необходимости беспокоиться о деталях реализации.

(Кстати, на мой взгляд, класс AbstractList удивительно. Вы только должны переопределить get() и size() иметь реализацию функционирования List и методы, как containsAll(), toString() и stream() все просто работать.)

+0

Примечание в коде '// другие методы, также переопределенные по мере необходимости для отслеживания счетчика изменений', предназначались для обработки addAll, add (int, E), сортировки и т. Д. Предположительно, дополнительные методы в« List »должны были бы использовать существующие API, так как это интерфейс или существующие подтипы, сломается, так что это другой аргумент. Если я перейду из 'AbstractList' вместо' ArrayList', вопрос остается тем же: есть ли сборка java «special», когда дело доходит до нарезки и рассматривает ее как абстрактный тип? –

+1

@PaulBilnoski Независимо от того, сколько методов 'ArrayList' вы переопределяете, вы не можете быть уверены в том, что получите' ChangeTrackingList' работу по желанию, потому что, не прочитав весь исходный код, вы не можете быть уверены, когда методы будут вызваны другие методы. Вы также должны синхронизировать код с оригиналом (они могут легко решить изменить реализацию 'addAll' в любой момент, и вам придется также изменить свой метод' addAll'). Разница между 'AbstractList' и' ArrayList' заключается в том, что 'AbstractList' четко и тщательно разработан для расширения. –

+0

Композиционный подход для меня был как вариант, но он не может быть реализован таким образом, чтобы гарантировать, что семантика предварительно задана во всех случаях. Основным виновником является метод get (index), set (index, item), который клиентский код, ожидающий списка, может использовать для переоформления элементов. Невозможно отслеживать изменения таким образом, чтобы разумно сохранить связанную полезную нагрузку. – Durandal

0

Если класс не final, вы всегда можете его расширить. Все остальное является субъективным и вопросом мнения.

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

Смежные вопросы