Bezdepbonusu7Программирование: статьи и ответы

Вопрос

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

Автор вопроса: fredoverflowОтветов: 9

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

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

  • В этой идиоме определенно нет ничего плохого. В конце концов, players.stream() - это именно такой метод, который возвращает поток вызывающей стороне. Реальный вопрос заключается в том, действительно ли вы хотите ограничить вызывающую сторону одним обходом, а также запретить ему доступ к вашей коллекции через Collection API? Может быть, вызывающий абонент просто хочет addAll переместить его в другую коллекцию? Marko Topolnik 10.07.2014
  • Все это зависит. Вы всегда можете использовать как collection.stream(), так и Stream.collect(). Так что это зависит от вас и вызывающего абонента, который использует эту функцию. Raja Anbazhagan 04.07.2017

javacollectionsjava-8java-streamencapsulation

Ответы

Ответ 1 · рейтинг 235 · Brian Goetz

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

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

// 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 по той простой причине, что любое Collection, которое вы решите вставить (например, ArrayList), может не быть форма, которую они хотят, и тогда вызывающая сторона все равно должна скопировать ее. Если вы возвращаете Stream, они могут сделать collect(toCollection(factory)) и получить именно то, что им нужно.

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

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

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

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

Ответ 2 · рейтинг 68 · Stuart Marks

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

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

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

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

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

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

e.g.

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

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

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

Ответ 4 · рейтинг 2 · Daniel Avery

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

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

Ответ 5 · рейтинг 2 · tkruse

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

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

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

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

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

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

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

Документация смягчает проблему, но она ошибочна и часто упускается из виду. В качестве примера см. эти javadocs файлов Java8:

 /**
  * [...] The returned stream encapsulates a Reader. If timely disposal of
  * file system resources is required, the try-with-resources
  * construct should be used to ensure that the stream's close
  * method is invoked after the stream operations are completed.
  */
 public static Stream<String> lines(Path path, Charset cs)
 /**
  * [...] no mention of closing even if this wraps the previous method
  */
public static Stream<String> lines(Path path)

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

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

Ответ 6 · рейтинг 1 · designbygravity

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

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

Ответ 7 · gontard

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

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

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

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

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

Ответ 8 · рейтинг -1 · Vazgen Torosyan

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

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

Ответ 9 · рейтинг -5 · dkatzel

У меня, вероятно, было бы 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, поэтому у вас не будет слишком много методов.

Перевод вопроса и ответов сообщества Stack Overflow, лицензия CC BY-SA. Оригинал на Stack Overflow.

Похожие материалы