Следует ли использовать Iterator или Iterable при экспонировании внутренних элементов коллекции?

У меня есть класс с частным измененным списком данных.

Мне нужно выставить элементы списка, заданные в следующих условиях:

  • Список не должен изменяться вне;
  • Для разработчиков, использующих функцию getter, должно быть ясно, что список, который они получают, не может быть изменен.

Какая функция геттера должна быть отмечена как рекомендуемый подход? Или вы можете предложить лучшее решение?

class DataProcessor {
    private final ArrayList<String> simpleData = new ArrayList<>();
    private final CopyOnWriteArrayList<String> copyData = new CopyOnWriteArrayList<>();

    public void modifyData() {
        ...
    }

    public Iterable<String> getUnmodifiableIterable() {
        return Collections.unmodifiableCollection(simpleData);
    }

    public Iterator<String> getUnmodifiableIterator() {
        return Collections.unmodifiableCollection(simpleData).iterator();
    }

    public Iterable<String> getCopyIterable() {
        return copyData;
    }

    public Iterator<String> getCopyIterator() {
        return copyData.iterator();
    }
}

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

Ответ 1

"Лучшее" решение на самом деле зависит от предполагаемых шаблонов приложений (и не столько от "мнений", как предлагает близкий избиратель). У каждого возможного решения есть плюсы и минусы, о которых можно судить объективно (и оценивать разработчик).


Изменить: возник вопрос: "Должен ли я вернуть коллекцию или поток?", с подробными ответами Брайана Гетца. Вы должны также проконсультироваться с этими ответами, прежде чем принимать какое-либо решение. Мой ответ не относится к потокам, а относится только к различным способам отображения данных в виде коллекции, указывая на плюсы, минусы и последствия различных подходов.


Возврат итератора

Возврат только Iterator неудобен, независимо от дальнейших подробностей, например. независимо от того, разрешит ли он изменения или нет. Только Iterator не может использоваться в цикле foreach. Поэтому клиентам приходилось писать

Iterator<String> it = data.getUnmodifiableIterator();
while (it.hasNext()) {
    String s = it.next();
    process(s);
}

тогда как в основном все остальные решения позволят им просто написать

for (String s : data.getUnmodifiableIterable()) {
    process(s);
}

Предоставление представления Collections.unmodifiable... для внутренних данных:

Вы можете открыть внутреннюю структуру данных, завернутую в соответствующую коллекцию Collections.unmodifiable.... Любые попытки изменить возвращенную коллекцию вызовут UnsupportedOperationException, явно заявив, что клиент не должен изменять данные.

Одна степень свободы в пространстве дизайна здесь заключается в том, скрываете ли вы дополнительную информацию: если у вас есть List, вы можете предложить метод

private List<String> internalData;

List<String> getData() {
    return Collections.unmodifiableList(internalData);
}

В качестве альтернативы вы можете быть менее конкретными в отношении типа внутренних данных:

  • Если вызывающий абонент не сможет выполнить индексированный доступ с помощью метода List#get(int index), вы можете изменить тип возврата этого метода на Collection<String>.
  • Если вызывающий абонент дополнительно не сможет получить размер возвращаемой последовательности, вызвав Collection'size(), вы можете вернуть Iterable<String>.

Также учтите, что при экспонировании менее специфических интерфейсов у вас есть выбор, например, изменить тип внутренних данных как Set<String>. Если бы вы гарантированно вернули List<String>, то изменение этого позже может вызвать некоторые головные боли.


Предоставление копии внутренних данных:

Очень простое решение - просто вернуть копию списка:

private List<String> internalData;

List<String> getData() {
    return new ArrayList<String>(internalData);
}

Это может иметь недостаток (потенциально больших и частых) копий памяти, и поэтому их следует учитывать только тогда, когда коллекция "мала".

Кроме того, вызывающий может изменить список, и он может ожидать, что изменения будут отражены во внутреннем состоянии (что не так). Эта проблема может быть устранена путем дополнительной упаковки нового списка в Collections.unmodifiableList.


Предоставление CopyOnWriteArrayList

Экспонирование CopyOnWriteArrayList через его Iterator или как Iterable, вероятно, не очень хорошая идея: у вызывающего есть возможность изменить его с помощью вызовов Iterator#remove, и вы явно хотели этого избежать.

Решение об экспонировании a CopyOnWriteArrayList, которое завернуто в Collections.unmodifiableList, может быть опцией. Он может выглядеть как излишне толстый брандмауэр с первого взгляда, но он определенно может быть оправданным - см. Следующий абзац.


Общие соображения

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

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

Рассмотрим следующий случай:

/**
 * Returns the data. The returned list is unmodifiable. 
 */
List<String> getData() {
    return Collections.unmodifiableList(internalData);
}

В документации здесь также должно быть указано, что...

/* ...
 * The returned list is a VIEW on the internal data. 
 * Changes in the internal data will be visible in 
 * the returned list.
 */

Это может быть важная информация, учитывая безопасность потоков и поведение во время итерации. Рассмотрим цикл, который выполняет итерацию по немодифицируемому представлению внутренних данных. И подумайте, что в этом цикле кто-то вызывает функцию, которая вызывает изменение внутренних данных:

for (String s : data.getData()) {
    ...
    data.changeInternalData();
}

Этот цикл будет разбит на ConcurrentModificationException, поскольку внутренние данные будут изменены во время повтора.

Компромисс относительно документации здесь относится к тому факту, что, как только определенное поведение будет указано, клиенты будут полагаться на это поведение. Представьте, что клиент делает это:

List<String> list = data.getList();
int oldSize = list.size();
data.insertElementToInternalData();

// Here, the client relies on the fact that he received
// a VIEW on the internal data:
int newSize = list.size();
assertTrue(newSize == oldSize+1);

Такие вещи, как ConcurrentModificationException, можно было бы избежать, если верная копия внутренних данных была возвращена, или с помощью CopyOnWriteArrayList (каждая из которых завернута в Collections.unmodifiableList). Это было бы "самым безопасным" решением в этом отношении:

  • Вызывающий не может изменить возвращенный список
  • Вызывающий не может напрямую изменить внутреннее состояние.
  • Если вызывающий изменяет внутреннее состояние косвенно, то итерация все еще работает

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

Ответ 2

Как правило, Iterator используется только с Iterable, для каждого цикла. Будет довольно странно видеть, что тип, отличный от итерации, содержит метод, возвращающий Iterator, и, возможно, он нарушает пользователя, что он не может использоваться для каждого цикла.

Поэтому я предлагаю Iterable в этом случае. Вы можете даже иметь свой класс implements Iterable, если это имеет смысл.

Если вы хотите прыгать на вагон Java 8, возвращение Stream, вероятно, является более "современным".

Ответ 3

По правилам инкапсуляции вам всегда нужно возвращать немодифицируемый список, в вашем случае это правило проектирования, поэтому верните Collections.unmodifiableCollection, и вам не нужно называть этот метод как getUnmodifiable, использовать процедуру присвоения имен геттера и использовать Javadoc сообщить другому разработчику, какой список вы вернетесь и почему... неосторожные пользователи будут предупреждены об исключении!!