Ответ 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
). Это было бы "самым безопасным" решением в этом отношении:
- Вызывающий не может изменить возвращенный список
- Вызывающий не может напрямую изменить внутреннее состояние.
- Если вызывающий изменяет внутреннее состояние косвенно, то итерация все еще работает
Но нужно подумать о том, действительно ли требуется "безопасность" для соответствующего приложения, и как это может быть документировано таким образом, чтобы допустить изменение внутренних деталей реализации.