it-swarm.com.ru

Должен ли я вернуть коллекцию или поток?

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

class Team
{
    private List<Player> players = new ArrayList<>();

    // ...

    public List<Player> getPlayers()
    {
        return Collections.unmodifiableList(players);
    }
}

Далее предположим, что все, что делает клиент, это итерация по списку один раз, немедленно. Возможно поместить игроков в JList или что-то. Клиент не сохраняет ссылку на список для последующей проверки!

Учитывая этот общий сценарий, я должен вместо этого вернуть поток?

    public Stream<Player> getPlayers()
    {
        return players.stream();
    }

Или возвращает поток не-идиоматических в Java? Были ли потоки предназначены для того, чтобы всегда быть "завершенными" внутри одного и того же выражения, в котором они были созданы?

145
fredoverflow

Ответ, как всегда, "это зависит". Это зависит от того, насколько большой будет возвращенная коллекция. Это зависит от того, изменяется ли результат с течением времени, и насколько важна согласованность возвращаемого результата. И это очень сильно зависит от того, как пользователь может использовать ответ.

Во-первых, обратите внимание, что вы всегда можете получить коллекцию из потока, и наоборот:

// If API returns Collection, convert with stream()
getFoo().stream()...

// If API returns Stream, use collect()
Collection<T> c = getFooStream().collect(toList());

Таким образом, вопрос в том, что более полезно для ваших абонентов.

Если ваш результат может быть бесконечным, есть только один выбор: поток.

Если ваш результат может быть очень большим, вы, вероятно, предпочли бы Stream, так как может не иметь смысла материализовать его все сразу, и это может создать значительное давление кучи.

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

Даже если вы знаете, что пользователь будет повторять его несколько раз или иным образом хранить его, вы все равно можете захотеть вернуть Stream вместо этого, потому что тот факт, что независимо от выбранной вами коллекции (например, ArrayList), может не быть форма, которую они хотят, и тогда звонящий должен все равно скопировать это. если вы возвращаете поток, они могут выполнить collect(toCollection(factory)) и получить его в той форме, которую они хотят.

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

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

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

199
Brian Goetz

У меня есть несколько моментов, которые нужно добавить к отличный ответ Брайана Гетца .

Весьма распространено возвращать Stream из вызова метода в стиле "getter". Смотрите Страница использования потока в javadoc Java 8 и ищите "методы ..., которые возвращают Stream" для пакетов, отличных от Java.util.Stream. Эти методы обычно используются в классах, которые представляют или могут содержать несколько значений или совокупностей чего-либо. В таких случаях API обычно возвращают коллекции или массивы из них. По всем причинам, которые Брайан отметил в своем ответе, очень гибко добавить сюда методы, возвращающие поток. Многие из этих классов уже имеют методы, возвращающие коллекции или массивы, потому что классы предшествуют API Streams. Если вы разрабатываете новый API, и имеет смысл предоставить методы, возвращающие поток, возможно, нет необходимости добавлять методы, возвращающие коллекцию.

Брайан упомянул стоимость "материализации" ценностей в коллекцию. Чтобы усилить этот момент, на самом деле здесь есть две затраты: стоимость хранения значений в коллекции (выделение памяти и копирование), а также стоимость создания значений в первую очередь. Последнюю стоимость часто можно уменьшить или избежать, воспользовавшись ленивым поведением Stream. Хорошим примером этого являются API в Java.nio.file.Files:

static Stream<String>  lines(path)
static List<String>    readAllLines(path)

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

try (Stream<String> lines = Files.lines(path)) {
    List<String> firstTen = lines.limit(10).collect(toList());
}

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

Идиома, которая, кажется, появляется, состоит в том, чтобы называть методы, возвращающие поток, во множественном числе от имени вещей, которые он представляет или содержит, без префикса get. Кроме того, хотя stream() является разумным именем для метода, возвращающего поток, когда существует только один возможный набор значений, которые должны быть возвращены, иногда существуют классы, которые имеют совокупности значений нескольких типов. Например, предположим, что у вас есть какой-то объект, который содержит как атрибуты, так и элементы. Вы можете предоставить два API, возвращающих поток:

Stream<Attribute>  attributes();
Stream<Element>    elements();
62
Stuart Marks

В отличие от коллекций, потоки имеют дополнительные характеристики . Поток, возвращаемый любым методом, может быть:

  • конечный или бесконечный
  • параллельный или последовательный (перенос с собственным выбором пула потоков)
  • заказанный или не заказанный

Эти различия также существуют в коллекциях, но там они являются частью очевидного контракта:

  • Все коллекции имеют размер, Iterator/Iterable может быть бесконечным.
  • Коллекции явно упорядочены или не упорядочены
  • К счастью, параллельность - это не то, о чем заботится коллекция, кроме безопасности потоков.

Для потребителя потока (из метода return или в качестве параметра метода) это, к сожалению, очень опасная и запутанная ситуация. Чтобы убедиться, что ваш алгоритм работает правильно, вы должны убедиться, что он отвечает, как определено, различным характеристикам, которые задает поток. И это очень сложно сделать. В модульном тестировании это будет означать, что вы должны умножить все ваши тесты, которые будут повторяться, с тем же содержимым потока, но с потоками, которые

  • (конечный, упорядоченный, последовательный)
  • (конечный, упорядоченный, параллельный)
  • (конечный, неупорядоченный, последовательный) ...

Вы даже не можете просто обойти проблему, написав методы защиты, которые выдают исключение IllegalArgumentException, если входной поток имеет характеристики, нарушающие ваш алгоритм, потому что свойства скрыты.

Это оставляет Stream только как допустимый выбор в сигнатуре метода, когда ни одна из характеристик не имеет значения для алгоритма потребления. Поскольку вы никогда не знаете, какую обработку кто-то еще захочет сделать с результатом вашего алгоритма, вы никому не сделаете одолжение, используя Stream в публичной сигнатуре метода.

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

2
tkruse

Были ли потоки предназначены для того, чтобы всегда быть "завершенными" внутри одного и того же выражения, в котором они были созданы?

Вот как они используются в большинстве примеров.

Примечание: возврат потока не так уж отличается от возврата итератора (допускается с гораздо большей выразительностью)

ИМХО, лучшее решение - заключить в капсулу, почему вы это делаете, а не возвращать коллекцию.

например.

public int playerCount();
public Player player(int n);

или если вы собираетесь их посчитать

public int countPlayersWho(Predicate<? super Player> test);
1
Peter Lawrey

Если поток конечен, и есть ожидаемая/нормальная операция с возвращенными объектами, которая вызовет проверенное исключение, я всегда возвращаю коллекцию. Потому что, если вы собираетесь что-то делать с каждым из объектов, которые могут вызвать исключение проверки, вы будете ненавидеть поток. Один реальный недостаток потоков - невозможность элегантно работать с проверенными исключениями.

Теперь, возможно, это признак того, что вам не нужны проверенные исключения, что справедливо, но иногда они неизбежны.

1
designbygravity

Возможно, фабрика Stream будет лучшим выбором. Большим преимуществом только экспонирования коллекций через Stream является то, что он лучше инкапсулирует структуру данных вашей доменной модели. При использовании классов вашего домена невозможно повлиять на внутреннюю работу вашего Списка или Сета, просто раскрыв поток.

Это также побуждает пользователей вашего класса домена писать код в более современном стиле Java 8. Существует возможность поэтапного рефакторинга в этом стиле, сохранив существующие геттеры и добавив новые потоковые геттеры. Со временем вы можете переписать свой прежний код, пока окончательно не удалите все получатели, которые возвращают список или набор. Этот вид рефакторинга действительно хорош после того, как вы очистили весь старый код!

0
Vazgen Torosyan

Я думаю, это зависит от вашего сценария. Может быть, если вы заставите свой Team реализовать Iterable<Player>, этого будет достаточно.

for (Player player : team) {
    System.out.println(player);
}

или в функциональном стиле:

team.forEach(System.out::println);

Но если вы хотите более полный и свободный API, поток может быть хорошим решением.

0
gontard