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

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

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

Ответ 1

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

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

IMHO лучшим решением является инкапсуляция, почему вы это делаете, и не возвращаете коллекцию.

например.

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

или если вы намерены считать их

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

Ответ 4

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

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

Ответ 5

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

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

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

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

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

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

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

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

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

Ответ 6

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

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

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

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

Но если вам нужна более полная и плавная api, поток может быть хорошим решением.

Ответ 7

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

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

Ответ 8

У меня, вероятно, было бы 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();
    }

}

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

Это также добавляет еще 1 метод для вашего API, поэтому у вас не слишком много методов.