Чи варто повернути колекцію чи потік?


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? Чи були потоки розроблені так, щоб завжди "припинятися" всередині того самого виразу, в якому вони були створені?


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 вже має вбудовані і не потрібно матеріалізувати колекцію (особливо якщо користувач може не обробляти весь результат.) Це дуже поширений випадок.

Навіть якщо ви знаєте, що користувач буде повторювати його кілька разів або іншим чином тримати його навколо, ви все одно можете захотіти повернути Потік замість цього простого факту, що в будь-яку колекцію, яку ви вирішите помістити, (наприклад, ArrayList), можливо, це не буде форму, яку вони хочуть, і тоді абонент повинен все-таки скопіювати її. якщо ви повернете потік, вони можуть це зробити collect(toCollection(factory))і отримати їх у потрібній формі.

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

Єдиний випадок, коли ви повинні повернути колекцію, - коли є чіткі вимоги послідовності, і ви повинні створити послідовний знімок рухомої цілі. Потім ви захочете помістити елементи в колекцію, яка не зміниться.

Тому я б сказав, що більшість часу Stream - це правильна відповідь - вона більш гнучка, вона не покладає зазвичай-зайвих витрат на матеріалізацію, і при необхідності може бути легко перетворена на вибрану Вами колекцію. Але іноді, можливо, вам доведеться повернути колекцію (скажімо, через сильні вимоги послідовності), або ви можете повернути колекцію, оскільки ви знаєте, як користувач буде користуватися нею і знаєте, що це найзручніша річ для них.


6
Як я вже говорив, є кілька випадків, коли він не пролетить, наприклад, коли ви хочете повернути знімок під час переміщення цілі, особливо, коли у вас є чіткі вимоги послідовності. Але більшість часу Stream здається більш загальним вибором, якщо ви не знаєте чогось конкретного про те, як він буде використовуватися.
Брайан Гец

8
@Marko Навіть якщо ви обмежите своє запитання так вузько, я все одно не згоден з вашим висновком. Можливо, ви припускаєте, що створити Потік чимось набагато дорожче, ніж упакувати колекцію незмінною обгорткою? (І навіть якщо ви цього не зробите, вигляд потоку, який ви отримуєте на обгортці, гірше того, що ви знімаєте з оригіналу; оскільки UnmodifiableList не перекриває сплейтератор (), ви фактично втратите весь паралелізм.) Підсумок: будьте обережні упередженості знайомства; Ви знаєте колекцію вже багато років, і це може змусити вас не довірити новачкові.
Брайан Гец

5
@MarkoTopolnik Звичайно. Моєю метою було вирішити загальне питання щодо дизайну API, яке стає FAQ. Щодо вартості, зауважте, що якщо у вас ще немає матеріалізованої колекції, ви можете повернути або завернути (OP робить, але часто немає такої), матеріалізація колекції методом getter є не дешевшою, ніж повернення потоку та надання абонент матеріалізує один (і, звичайно, рання матеріалізація може бути набагато дорожчою, якщо абонент цього не потребує або якщо ви повернете ArrayList, але абонент хоче TreeSet.) Але Stream новий, і люди часто припускають його більше $$$, ніж Це є.
Брайан Гец

4
@MarkoTopolnik Хоча вбудована пам'ять є дуже важливим випадком використання, є також деякі інші випадки, які мають хорошу підтримку паралелізації, наприклад, не упорядковані генеровані потоки (наприклад, Stream.generate). Однак, де потоки погано підходять, це випадок реактивного використання, коли дані надходять із випадковою затримкою. Для цього я б запропонував RxJava.
Брайан Гец

4
@MarkoTopolnik Я не думаю, що ми не згодні, хіба що, можливо, вам сподобалося, щоб ми сфокусували свої зусилля дещо інакше. (Ми звикли до цього; не може зробити всіх людей щасливими.) Дизайнерський центр для потоків орієнтований на структури даних в пам'яті; Центр дизайну RxJava зосереджується на подіях, що генеруються зовні. Обидва - хороші бібліотеки; також обидва не дуже спрацьовують, коли ви намагаєтесь застосувати їх до випадків, які виходять із центру дизайну. Але тільки тому, що молоток - це жахливий інструмент для введення спиць, це не дозволяє припустити, що з молотком щось не так.
Брайан Гец

63

У мене є кілька моментів, щоб додати відмінну відповідь Брайана Геца .

Досить поширене повернення потоку з виклику методу "getter". Перегляньте сторінку використання потоку в Java 8 javadoc і шукайте "методи ..., які повертають потік" для інших пакетів java.util.Stream. Ці методи зазвичай є на класах, які представляють або можуть містити кілька значень або агрегацій чогось. У таких випадках API зазвичай повертають колекції або масиви з них. З усіх причин, які Брайан зазначив у своїй відповіді, додати сюди потокові методи повернення дуже гнучко. У багатьох з цих класів вже є методи, що повертають колекції або масиви, оскільки класи передують API Streams. Якщо ви розробляєте новий API і має сенс запропонувати потокові методи, що повертаються, можливо, не потрібно буде також додавати методи повернення колекції.

Брайан зазначив вартість "матеріалізації" цінностей у колекцію. Щоб посилити цю точку, тут насправді дві витрати: вартість зберігання значень у колекції (розподіл пам'яті та копіювання), а також вартість створення значень в першу чергу. Останню вартість часто можна зменшити або уникнути, скориставшись поведінкою Потоку, яка шукає лінь. Хорошим прикладом цього є 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

@dkatzel Це залежить від того, є кінцевими користувачами автор чи хтось, з ким вони працюють. Якщо кінцеві користувачі не знають, то вам потрібно більш загальне рішення. Ви все ще можете обмежити доступ до базової колекції.
Пітер Лорі

1

Якщо потік обмежений, і на повернених об'єктах очікувана / нормальна робота, яка викине перевірений виняток, я завжди повертаю колекцію. Тому що якщо ви збираєтеся щось робити на кожному з об’єктів, які можуть кинути виняток з чека, ви будете ненавидіти потік. Один справжній брак потоків i є неможливість елегантно впоратися з перевіреними винятками.

Можливо, це знак того, що вам не потрібні перевірені винятки, що справедливо, але іноді вони неминучі.


1

На відміну від колекцій, потоки мають додаткові характеристики . Потік, повернутий будь-яким методом, може бути:

  • кінцевий або нескінченний
  • паралельний або послідовний (із глобальним спільним потоком потоків за замовчуванням, який може впливати на будь-яку іншу частину програми)
  • замовлені або не замовлені

Ці відмінності існують і в колекціях, але вони є частиною очевидного контракту:

  • Усі колекції мають розмір, Ітератор / Ітерабельний може бути нескінченним.
  • Колекції явно впорядковані або не упорядковані
  • Паралельність, на щастя, не є чимось дбайливим для колекції поза безпекою ниток.

Як споживач потоку (або з методу повернення, або як параметр методу), це небезпечна та заплутана ситуація. Щоб переконатися, що їх алгоритм веде себе правильно, споживачі потоків повинні переконатися, що алгоритм не робить неправильного припущення щодо характеристик потоку. І це дуже важко зробити. Під час одиничного тестування це означатиме, що вам потрібно помножити всі ваші тести, щоб повторити з тим самим вмістом потоку, але з потоками, які є

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

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

Це залишає Потік лише як правильний вибір у підписі методу, коли жодна з проблем, що перераховані вище, не має значення, що рідко буває.

Набагато безпечніше використовувати інші типи даних у підписах методів із явним контрактом (і без участі в неявному обробці пулу потоків), що унеможливлює випадкову обробку даних з помилковими припущеннями про впорядкованість, розмір чи паралельність (та використання потокової нитки).


2
Ваші занепокоєння щодо нескінченних потоків є безпідставними; питання: "чи варто повернути колекцію чи потік". Якщо колекція - це можливість, результат за визначенням обмежений. Тому тривоги, що абоненти загрожують нескінченною ітерацією, враховуючи, що ви могли повернути колекцію , є безпідставними. Решта порад у цій відповіді просто погані. Мені це здається, що ти наткнувся на когось, хто надмірно використовував Потік, і ти перевертаєшся в іншому напрямку. Зрозуміла, але погана порада.
Брайан Гец

0

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

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

або у функціональному стилі:

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

Але якщо ви хочете більш повної та вільної api, потік може стати хорошим рішенням.


Зауважте, що в коді, який розміщено в ОП, кількість гравців майже не потрібна, окрім як оцінка («1034 гравці грають зараз, натисніть тут, щоб почати!») Це відбувається тому, що ви повертаєте незмінний вигляд змінної колекції , тож кількість, яку ви отримаєте зараз, може не дорівнювати рахунку три мікросекунди відтепер. Отже, повертаючи колекцію, ви отримуєте "простий" спосіб дістатись до підрахунку (і насправді, stream.count()це дуже просто), це число насправді не має великого значення для нічого, крім налагодження чи оцінки.
Брайан Гец

0

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

Якщо у вас вже є "матеріалізований" Collectionв руці (тобто він був створений ще до виклику - як це відбувається у наведеному прикладі, де це поле учасника), немає сенсу перетворювати його на a Stream. Абонент може легко зробити це самостійно. Тоді як, якщо абонент хоче споживати дані у первісному вигляді, ви перетворюєте їх на Streamзмушування їх виконувати зайву роботу, щоб повторно матеріалізувати копію оригінальної структури.


-1

Можливо, фабрика «Стрім» була б кращим вибором. Великий виграш лише відкриття колекцій через Stream полягає в тому, що вона краще інкапсулює структуру даних вашої доменної моделі. Неможливо, щоб будь-яке використання класів домену впливало на внутрішню роботу списку або набору, просто викривши потік.

Він також заохочує користувачів вашого доменного класу писати код у більш сучасному стилі Java 8. До цього стилю можна поступово відтворювати рефактор, зберігаючи існуючі геттери та додаючи нові гейттери, що повертаються. З часом ви можете переписати свій застарілий код, поки ви остаточно не видалите всі розпорядники, які повертають список або встановити. Цей вид рефакторингу відчуває себе дуже добре, коли ви очистили всі застарілі коди!


7
чи є причина це повністю цитується? є джерело?
Xerus

-5

Ймовірно, у мене є 2 методи, один для повернення a 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, щоб у вас не було занадто багато методів


1
Тому що він хотів вибрати між цими двома варіантами і запитував плюси та мінуси кожного з них. Більше того, вона забезпечує краще розуміння цих понять.
Ліберт Піу Піу

Будь ласка, не робіть цього. Уявіть API!
Франсуа Готьє
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.