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

163

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

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? Были ли потоки предназначены для того, чтобы всегда быть «завершенными» внутри одного и того же выражения, в котором они были созданы?

fredoverflow
источник
12
В этом, как идиоме, определенно нет ничего плохого. Ведь players.stream()именно такой метод возвращает поток вызывающей стороне. Реальный вопрос в том, действительно ли вы хотите ограничить вызывающего абонента одним обходом, а также лишить его доступа к вашей коллекции через CollectionAPI? Может, звонящий просто хочет, чтобы addAllон попал в другую коллекцию?
Марко Топольник
2
Все это зависит. Вы всегда можете сделать collection.stream (), а также Stream.collect (). Так что это зависит от вас и абонента, который использует эту функцию.
Раджа Анбажаган

Ответы:

222

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

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

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

Брайан Гетц
источник
6
Как я уже говорил, есть несколько случаев, когда он не будет летать, например, когда вы хотите вернуть моментальный снимок во время движущейся цели, особенно когда у вас есть строгие требования согласованности. Но в большинстве случаев Stream кажется более общим выбором, если вы не знаете что-то конкретное о том, как он будет использоваться.
Брайан Гетц
8
@Marko Даже если вы ограничите свой вопрос так узко, я все же не согласен с вашим выводом. Возможно, вы предполагаете, что создание потока несколько дороже, чем упаковка коллекции неизменяемой оболочкой? (И даже если вы этого не сделаете, потоковое представление, которое вы получаете на обертке, хуже, чем то, что вы получаете с оригинала; поскольку UnmodifiableList не переопределяет spliterator (), вы фактически потеряете весь параллелизм.) Итог: остерегайтесь предвзятости знакомства; Вы знаете коллекцию в течение многих лет, и это может заставить вас не доверять новичку.
Брайан Гетц
5
@MarkoTopolnik Конечно. Моя цель состояла в том, чтобы ответить на общий вопрос разработки API, который становится FAQ. Что касается стоимости, обратите внимание, что, если у вас еще нет материализованной коллекции, которую вы можете вернуть или свернуть (OP делает, но часто ее нет), материализация коллекции в методе получения не дешевле, чем возврат потока и вызывающая сторона материализует одну (и, конечно, ранняя материализация может быть намного дороже, если вызывающая сторона не нуждается в этом или если вы возвращаете ArrayList, но вызывающая сторона хочет TreeSet.) Но Stream является новым, и люди часто предполагают, что его больше $$$, чем это.
Брайан Гетц
4
@MarkoTopolnik Хотя in-memory является очень важным вариантом использования, существуют также некоторые другие случаи, которые имеют хорошую поддержку распараллеливания, такие как неупорядоченные сгенерированные потоки (например, Stream.generate). Однако, когда потоки плохо подходят, это случай реактивного использования, когда данные поступают со случайной задержкой. Для этого я бы предложил RxJava.
Брайан Гетц
4
@MarkoTopolnik Я не думаю, что мы не согласны, за исключением, возможно, того, что вам, возможно, понравилось, что мы сосредоточили свои усилия немного по-другому. (Мы привыкли к этому; не можем порадовать всех людей.) Центр дизайна для Streams сосредоточился на структурах данных в памяти; Центр дизайна RxJava фокусируется на событиях, генерируемых извне. Оба являются хорошими библиотеками; Кроме того, оба не очень хорошо, когда вы пытаетесь применить их к случаям далеко от их дизайн-центра. Но только потому, что молоток - ужасный инструмент для иглы, это не значит, что с молотком что-то не так.
Брайан Гетц
63

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

Весьма распространено возвращать Stream из вызова метода в стиле «getter». Смотрите страницу использования Stream в Java 8 javadoc и ищите «методы ... которые возвращают 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();
Стюарт Маркс
источник
3
Великолепные моменты. Можете ли вы рассказать больше о том, где вы видите возникновение этой идиомы именования, и сколько тяги (пара?) Она набирает? Мне нравится идея соглашения об именах, которое делает очевидным, что вы получаете поток против коллекции - хотя я также часто ожидаю, что завершение IDE на "get" скажет мне, что я могу получить.
Джошуа Голдберг
1
Я также очень заинтересован в этой идиоме именования
избрал
5
@JoshuaGoldberg JDK, похоже, принял эту идиому именования, хотя и не исключительно. Обратите внимание: CharSequence.chars () и .codePoints (), BufferedReader.lines () и Files.lines () существуют в Java 8. В Java 9 были добавлены следующие элементы: Process.children (), NetworkInterface.addresses ( ), Scanner.tokens (), Matcher.results (), java.xml.catalog.Catalog.catalogs (). Были добавлены другие методы возврата потока, которые не используют эту идиому - на ум приходит Scanner.findAll (), - но идиома множественного числа существительного, кажется, вошла в добросовестное использование в JDK.
Стюарт Маркс
1

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

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

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

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

например

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

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

public int countPlayersWho(Predicate<? super Player> test);
Питер Лори
источник
2
Проблема с этим ответом состоит в том, что от автора требуется ожидать каждого действия, которое хочет выполнить клиент, и это значительно увеличит число методов в классе.
дкацель
@dkatzel Это зависит от того, является ли конечный пользователь автором или кем-то, с кем они работают. Если конечные пользователи не знают, то вам нужно более общее решение. Вы все еще можете ограничить доступ к основной коллекции.
Питер Лори
1

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

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

designbygravity
источник
1

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

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

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

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

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

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

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

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

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

tkruse
источник
2
Ваши опасения по поводу бесконечных потоков беспочвенны; вопрос «должен ли я вернуть коллекцию или поток». Если возможна коллекция, результат по определению конечен. То, что вызывающие абоненты рискнули бы бесконечной итерацией, учитывая, что вы могли бы вернуть коллекцию , необоснованно. Остальные советы в этом ответе просто плохие. Для меня это звучит так, как будто вы столкнулись с кем-то, кто чрезмерно использовал Stream, и вы слишком сильно вращаетесь в другом направлении. Понятно, но плохой совет.
Брайан Гетц
0

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

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

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

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

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

Gontard
источник
Обратите внимание, что в коде, опубликованном ОП, количество игроков практически бесполезно, за исключением оценки («1034 игрока играют сейчас, нажмите здесь, чтобы начать!»). Это потому, что вы возвращаете неизменный вид изменяемой коллекции. Таким образом, счетчик, который вы получаете сейчас, может не равняться счету через три микросекунды. Поэтому, возвращая коллекцию, дает вам «простой» способ подсчета (и на самом деле, stream.count()довольно простой), но это число не имеет большого значения ни для чего, кроме отладки или оценки.
Брайан Гетц
0

Хотя некоторые из наиболее влиятельных респондентов давали отличные общие советы, я удивлен, что никто так и не сказал:

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

Даниэль Эйвери
источник
-1

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

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

Вазген Торосян
источник
7
есть ли причина, по которой это полностью цитируется? есть ли источник?
Xerus
-5

Вероятно, у меня было бы 2 метода: один для возврата Collectionа другой для возврата коллекции в виде Stream.

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

// ...

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

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

}

Это лучшее из обоих миров. Клиент может выбрать, хотят ли они Список или Поток, и ему не нужно делать дополнительное создание объекта, создавая неизменную копию списка, просто чтобы получить Поток.

Это также добавляет только 1 метод к вашему API, чтобы у вас не было слишком много методов.

dkatzel
источник
1
Потому что он хотел выбрать между этими двумя вариантами и спросил плюсы и минусы каждого. Более того, он дает каждому лучшее понимание этих концепций.
Либерт Пиу Пиу
Пожалуйста, не делай этого. Представьте API!
Франсуа Готье