Використання додатка Java 8 за допомогою Stream :: flatMap


240

Новий фреймворк Java 8 і його друзі створюють дуже короткий Java-код, але я натрапив на здавалося-просту ситуацію, яку складно робити стисло.

Розглянемо а List<Thing> thingsта метод Optional<Other> resolve(Thing thing). Я хочу скласти карту Things до Optional<Other>s і отримати перше Other. Очевидним рішенням буде використання things.stream().flatMap(this::resolve).findFirst(), але flatMapвимагає повернути потік, і Optionalвін не має stream()методу (або це a, Collectionабо надає метод для його перетворення чи перегляду як а Collection).

Найкраще, що я можу придумати, це:

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .findFirst();

Але це здається надзвичайно довгодушним для того, що здається дуже поширеним випадком. Хтось має кращу ідею?


Після трохи кодування у вашому прикладі я фактично вважаю явну версію більш читаною, ніж та, яка стосується, якщо вона існувала .flatMap(Optional::toStream), з вашою версією ви насправді бачите, що відбувається.
skiwi

19
@skiwi Ну, Optional.streamіснує в JDK 9 зараз ....
Стюарт

Мені цікаво, де це документально зафіксовано і яким був процес його отримання. Є деякі інші методи, які справді здаються, що вони повинні існувати, і мені цікаво, де відбувається обговорення змін API.
Yona Appletree


10
Найсмішніше, що JDK-8050820 насправді посилається на це питання у своєму описі!
Didier L

Відповіді:


265

Java 9

Optional.stream додано до JDK 9. Це дозволяє вам виконувати наступні дії, не потребуючи жодного допоміжного методу:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(Optional::stream)
          .findFirst();

Java 8

Так, це був невеликий отвір в API, оскільки дещо незручно перетворювати Optional<T>нуль у довжину чи одну Stream<T>. Ви можете це зробити:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty())
          .findFirst();

Маючи потрійного оператора всередині flatMap, дещо громіздко, тому, можливо, краще написати трохи допоміжної функції для цього:

/**
 * Turns an Optional<T> into a Stream<T> of length zero or one depending upon
 * whether a value is present.
 */
static <T> Stream<T> streamopt(Optional<T> opt) {
    if (opt.isPresent())
        return Stream.of(opt.get());
    else
        return Stream.empty();
}

Optional<Other> result =
    things.stream()
          .flatMap(t -> streamopt(resolve(t)))
          .findFirst();

Тут я наголосив на заклику resolve()замість того, щоб робити окрему map()операцію, але це питання смаку.


2
Я не думаю, що api можуть змінитися до Java 9 зараз.
assylias

5
@Hypher Дякую Техніка .filter (). Map () не надто погана і дозволяє уникнути залежностей від допоміжних методів. "Було б добре, якби існував більш стислий спосіб. Я буду досліджувати додавання Optional.stream ().
Стюарт Маркс

43
Я віддаю перевагу:static <T> Stream<T> streamopt(Optional<T> opt) { return opt.map(Stream::of).orElse(Stream.empty()); }
kubek2k

5
Я б хотів, щоб вони просто додали Optionalперевантаження Stream#flatMap... таким чином, ви могли просто написатиstream().flatMap(this::resolve)
пластівці

4
@flkes Так, ми пообіцяли цю ідею, але, схоже, не додамо всієї такої великої цінності, що є (у JDK 9) Optional.stream().
Стюарт Маркс

69

Я додаю цю другу відповідь на основі запропонованої користувачем srborlongan редакції до моєї іншої відповіді . Я думаю, що запропонована методика була цікавою, але вона не дуже підходила як редагування моєї відповіді. Інші погодилися, і запропонована редакція була відхилена. (Я не був одним із виборців.) Хоча ця техніка і заслуга. Було б найкраще, якби srborlongan опублікував власну відповідь. Цього ще не відбулося, і я не хотів, щоб техніка втрачалася в туманах StackOverflow, відхилених історією редагування, тому я вирішив викласти її як окрему відповідь.

В основному методика полягає в тому, щоб використовувати деякі Optionalметоди розумно, щоб уникнути необхідності використання потрійного оператора ( ? :) або оператора if / else.

Мій вбудований приклад буде переписаний так:

Optional<Other> result =
    things.stream()
          .map(this::resolve)
          .flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty))
          .findFirst();

Мій приклад, який використовує хелперний метод, буде переписаний так:

/**
 * Turns an Optional<T> into a Stream<T> of length zero or one depending upon
 * whether a value is present.
 */
static <T> Stream<T> streamopt(Optional<T> opt) {
    return opt.map(Stream::of)
              .orElseGet(Stream::empty);
}

Optional<Other> result =
    things.stream()
          .flatMap(t -> streamopt(resolve(t)))
          .findFirst();

КОМЕНТАР

Порівняємо безпосередньо оригінальну та модифіковану версії:

// original
.flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty())

// modified
.flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty))

Оригінал - це просто, якщо робочий підхід: ми отримуємо Optional<Other>; якщо у нього є значення, ми повертаємо потік, що містить це значення, і якщо воно не має значення, ми повертаємо порожній потік. Досить просто і легко пояснити.

Модифікація розумна і має ту перевагу, що вона уникає умовних умов. (Я знаю, що деякі люди не люблять потрійного оператора. Якщо його неправомірно використовувати, це дійсно може важко зрозуміти код.) Однак, іноді все може бути занадто розумним. Змінений код також починається з Optional<Other>. Потім він називає, Optional.mapякий визначається наступним чином:

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

map(Stream::of)Виклик повертає Optional<Stream<Other>>. Якщо значення було присутнє у введенні Необов’язково, що повертається Необов’язковий містить Потік, який містить єдиний Інший результат. Але якщо значення не було, результат є порожнім Необов’язковим.

Далі, виклик orElseGet(Stream::empty)повертає значення типу Stream<Other>. Якщо його вхідне значення присутнє, воно отримує значення, яке є одноелементним Stream<Other>. В іншому випадку (якщо вхідне значення відсутнє) воно повертається порожнім Stream<Other>. Отже результат правильний, такий же, як і вихідний умовний код.

У коментарях, що обговорювали мою відповідь, щодо відхиленої редакції, я описав цю методику як "більш стислу, але й більш незрозумілу". Я стою біля цього. Мені знадобилось певний час, щоб зрозуміти, що це робило, і мені також знадобився певний час, щоб написати вищеописаний опис того, що він робив. Ключова тонкість - це перетворення з Optional<Other>на Optional<Stream<Other>>. Після того, як ви це зробите, це має сенс, але мені це було очевидно.

Я все-таки визнаю, що спочатку незрозумілі речі можуть з часом стати ідіоматичними. Можливо, ця методика виявляється найкращим способом на практиці, принаймні, поки не Optional.streamбуде додана (якщо вона коли-небудь стане).

ОНОВЛЕННЯ: Optional.stream додано до JDK 9.


16

Ви не можете зробити це більш стисло, як ви вже робите.

Ви стверджуєте, що не хочете .filter(Optional::isPresent) і .map(Optional::get) .

Це було вирішено методом опису @StuartMarks, однак у результаті ви тепер перетворите його на Optional<T>, тож тепер вам потрібно скористатись .flatMap(this::streamopt)і a get().

Тож воно все ще складається з двох тверджень, і тепер ви можете отримати винятки за допомогою нового методу! Бо що робити, якщо кожен необов'язковий порожній? Тоді findFirst()заповіт поверне порожній вибір, і ваш get()збій буде невдалим!

Отже, що у вас є:

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .findFirst();

це на самому справі кращий спосіб домогтися того, чого ви хочете, і що ви хочете зберегти результат як T, не як Optional<T>.

Я взяв на себе сміливість створити CustomOptional<T>клас , який обертає Optional<T>і забезпечує додатковий метод, flatStream(). Зауважте, що ви не можете продовжити Optional<T>:

class CustomOptional<T> {
    private final Optional<T> optional;

    private CustomOptional() {
        this.optional = Optional.empty();
    }

    private CustomOptional(final T value) {
        this.optional = Optional.of(value);
    }

    private CustomOptional(final Optional<T> optional) {
        this.optional = optional;
    }

    public Optional<T> getOptional() {
        return optional;
    }

    public static <T> CustomOptional<T> empty() {
        return new CustomOptional<>();
    }

    public static <T> CustomOptional<T> of(final T value) {
        return new CustomOptional<>(value);
    }

    public static <T> CustomOptional<T> ofNullable(final T value) {
        return (value == null) ? empty() : of(value);
    }

    public T get() {
        return optional.get();
    }

    public boolean isPresent() {
        return optional.isPresent();
    }

    public void ifPresent(final Consumer<? super T> consumer) {
        optional.ifPresent(consumer);
    }

    public CustomOptional<T> filter(final Predicate<? super T> predicate) {
        return new CustomOptional<>(optional.filter(predicate));
    }

    public <U> CustomOptional<U> map(final Function<? super T, ? extends U> mapper) {
        return new CustomOptional<>(optional.map(mapper));
    }

    public <U> CustomOptional<U> flatMap(final Function<? super T, ? extends CustomOptional<U>> mapper) {
        return new CustomOptional<>(optional.flatMap(mapper.andThen(cu -> cu.getOptional())));
    }

    public T orElse(final T other) {
        return optional.orElse(other);
    }

    public T orElseGet(final Supplier<? extends T> other) {
        return optional.orElseGet(other);
    }

    public <X extends Throwable> T orElseThrow(final Supplier<? extends X> exceptionSuppier) throws X {
        return optional.orElseThrow(exceptionSuppier);
    }

    public Stream<T> flatStream() {
        if (!optional.isPresent()) {
            return Stream.empty();
        }
        return Stream.of(get());
    }

    public T getTOrNull() {
        if (!optional.isPresent()) {
            return null;
        }
        return get();
    }

    @Override
    public boolean equals(final Object obj) {
        return optional.equals(obj);
    }

    @Override
    public int hashCode() {
        return optional.hashCode();
    }

    @Override
    public String toString() {
        return optional.toString();
    }
}

Ви побачите, що я додав flatStream(), як тут:

public Stream<T> flatStream() {
    if (!optional.isPresent()) {
        return Stream.empty();
    }
    return Stream.of(get());
}

Використовується як:

String result = Stream.of("a", "b", "c", "de", "fg", "hij")
        .map(this::resolve)
        .flatMap(CustomOptional::flatStream)
        .findFirst()
        .get();

Вам все одно потрібно буде повернути Stream<T>сюди, як ви не можете повернутися T, тому що якщо !optional.isPresent(), тоді, T == nullякщо ви оголосите це таким, але тоді ви .flatMap(CustomOptional::flatStream)намагатиметеся додати nullдо потоку, а це неможливо.

Наприклад:

public T getTOrNull() {
    if (!optional.isPresent()) {
        return null;
    }
    return get();
}

Використовується як:

String result = Stream.of("a", "b", "c", "de", "fg", "hij")
        .map(this::resolve)
        .map(CustomOptional::getTOrNull)
        .findFirst()
        .get();

Тепер кине NullPointerExceptionвсередину операцій потоку.

Висновок

Метод, який ви використовували, насправді є найкращим методом.


6

Трохи коротша версія з використанням reduce:

things.stream()
  .map(this::resolve)
  .reduce(Optional.empty(), (a, b) -> a.isPresent() ? a : b );

Ви також можете перенести функцію зменшення до статичної утиліти, а потім вона стане:

  .reduce(Optional.empty(), Util::firstPresent );

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

1
І, на жаль, виконання кожного рішення є порушенням угоди. Але це розумно.
Yona Appletree

5

Оскільки моя попередня відповідь виявилася не дуже популярною, я піду ще раз.

Коротка відповідь:

Ви в основному на правильному шляху. Найкоротший код для отримання потрібного результату, який я міг би придумати, такий:

things.stream()
      .map(this::resolve)
      .filter(Optional::isPresent)
      .findFirst()
      .flatMap( Function.identity() );

Це відповідає всім вашим вимогам:

  1. Він знайде першу відповідь, яка не відповідає порожньому Optional<Result>
  2. Це дзвонить this::resolveліниво в міру потреби
  3. this::resolve не буде викликано після першого не порожнього результату
  4. Це повернеться Optional<Result>

Більш довга відповідь

Єдиною модифікацією порівняно з початковою версією OP було те, що я видалив .map(Optional::get)перед викликом .findFirst()і додав .flatMap(o -> o)як останній виклик у ланцюжку.

Це приємно впливає на позбавлення від подвійного Необов'язкового, коли потік знаходить реальний результат.

На Java не можна дійсно коротше, ніж це.

Альтернативний фрагмент коду з використанням більш звичайного for методики циклу буде приблизно однаковою кількістю рядків коду і матиме більш-менш однаковий порядок і кількість операцій, які потрібно виконати:

  1. Дзвінок this.resolve ,
  2. фільтрація на основі Optional.isPresent
  3. повернення результату і
  4. певний спосіб боротьби з негативним результатом (коли нічого не було знайдено)

Тільки щоб довести, що моє рішення працює як рекламоване, я написав невелику тестову програму:

public class StackOverflow {

    public static void main( String... args ) {
        try {
            final int integer = Stream.of( args )
                    .peek( s -> System.out.println( "Looking at " + s ) )
                    .map( StackOverflow::resolve )
                    .filter( Optional::isPresent )
                    .findFirst()
                    .flatMap( o -> o )
                    .orElseThrow( NoSuchElementException::new )
                    .intValue();

            System.out.println( "First integer found is " + integer );
        }
        catch ( NoSuchElementException e ) {
            System.out.println( "No integers provided!" );
        }
    }

    private static Optional<Integer> resolve( String string ) {
        try {
            return Optional.of( Integer.valueOf( string ) );
        }
        catch ( NumberFormatException e )
        {
            System.out.println( '"' + string + '"' + " is not an integer");
            return Optional.empty();
        }
    }

}

(У нього є кілька додаткових рядків для налагодження та перевірки, що потрібно вирішити лише стільки дзвінків, скільки потрібно ...)

Виконуючи це в командному рядку, я отримав такі результати:

$ java StackOferflow a b 3 c 4
Looking at a
"a" is not an integer
Looking at b
"b" is not an integer
Looking at 3
First integer found is 3

Я думаю так само, як і Роланд Тепп. Чому хтось зробить потік <stream <? >> та flat, коли ви можете просто
Young Hyun Yoo

3

Якщо ви не проти використовувати бібліотеку сторонніх організацій, ви можете використовувати Javaslang . Це як Scala, але реалізовано на Java.

Він постачається з повною бібліотекою незмінних колекцій, дуже схожою на відому Scala. Ці колекції замінюють колекції Java та потік Java 8's. Він також має власну реалізацію Варіанту.

import javaslang.collection.Stream;
import javaslang.control.Option;

Stream<Option<String>> options = Stream.of(Option.some("foo"), Option.none(), Option.some("bar"));

// = Stream("foo", "bar")
Stream<String> strings = options.flatMap(o -> o);

Ось рішення для прикладу початкового питання:

import javaslang.collection.Stream;
import javaslang.control.Option;

public class Test {

    void run() {

        // = Stream(Thing(1), Thing(2), Thing(3))
        Stream<Thing> things = Stream.of(new Thing(1), new Thing(2), new Thing(3));

        // = Some(Other(2))
        Option<Other> others = things.flatMap(this::resolve).headOption();
    }

    Option<Other> resolve(Thing thing) {
        Other other = (thing.i % 2 == 0) ? new Other(i + "") : null;
        return Option.of(other);
    }

}

class Thing {
    final int i;
    Thing(int i) { this.i = i; }
    public String toString() { return "Thing(" + i + ")"; }
}

class Other {
    final String s;
    Other(String s) { this.s = s; }
    public String toString() { return "Other(" + s + ")"; }
}

Відмова: Я творець Javaslang.


3

Пізно на вечірку, але що робити

things.stream()
    .map(this::resolve)
    .filter(Optional::isPresent)
    .findFirst().get();

Ви можете позбутися останнього get (), якщо створити метод util для перетворення необов'язкового потоку вручну:

things.stream()
    .map(this::resolve)
    .flatMap(Util::optionalToStream)
    .findFirst();

Якщо ви повернете потік одразу з функції вирішення, ви збережете ще один рядок.


3

Я хотів би просувати заводські методи створення помічників для функціональних API:

Optional<R> result = things.stream()
        .flatMap(streamopt(this::resolve))
        .findFirst();

Заводський метод:

<T, R> Function<T, Stream<R>> streamopt(Function<T, Optional<R>> f) {
    return f.andThen(Optional::stream); // or the J8 alternative:
    // return t -> f.apply(t).map(Stream::of).orElseGet(Stream::empty);
}

Обґрунтування:

  • Як і у випадку посилань на методи в цілому, порівняно з лямбда-виразами, ви не можете випадково захопити змінну з доступної області, наприклад:

    t -> streamopt(resolve(o))

  • Це компонований варіант, наприклад, ви можете зателефонувати Function::andThenза фабричним результатом методу:

    streamopt(this::resolve).andThen(...)

    В той час, як у випадку з лямбда, вам потрібно спробувати її спочатку:

    ((Function<T, Stream<R>>) t -> streamopt(resolve(t))).andThen(...)


3

Null підтримується потоком, наданим Моєю бібліотекою AbacusUtil . Ось код:

Stream.of(things).map(e -> resolve(e).orNull()).skipNull().first();

3

Якщо ви застрягли з Java 8, але у вас є доступ до Guava 21.0 або новішої версії, ви можете використовувати Streams.streamдля перетворення необов'язкового в потік.

Таким чином, дано

import com.google.common.collect.Streams;

можна писати

Optional<Other> result =
    things.stream()
        .map(this::resolve)
        .flatMap(Streams::stream)
        .findFirst();

0

Що на рахунок того?

private static List<String> extractString(List<Optional<String>> list) {
    List<String> result = new ArrayList<>();
    list.forEach(element -> element.ifPresent(result::add));
    return result;
}

https://stackoverflow.com/a/58281000/3477539


Навіщо це робити, коли ви можете передавати та збирати потоки?
OneCricketeer

return list.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())), як і питання (і ваша відповідна відповідь) ...
OneCricketeer

Можливо, я помиляюся, але я вважаю, що використання isPresent (), а потім get () не є хорошою практикою. Тому я намагаюся піти від цього.
Растаман

Якщо ви використовуєте .get() без isPresent() , то ви отримаєте попередження в IntelliJ
OneCricketeer

-5

Швидше за все, ви робите це неправильно.

Java 8 Необов'язково не використовується таким чином. Зазвичай зарезервовано лише для операцій потокового терміналу, які можуть або не можуть повернути значення, як, наприклад, знайти.

У вашому випадку може бути краще спершу спробувати знайти дешевий спосіб відфільтрувати ті елементи, які можна вирішити, а потім отримати перший елемент як необов'язковий і вирішити його як останню операцію. Ще краще - замість фільтрування знайдіть перший вирішуваний елемент та вирішіть його.

things.filter(Thing::isResolvable)
      .findFirst()
      .flatMap(this::resolve)
      .get();

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


6
Я думаю, що метод вирішення () повернення Факультативного (ОП) ОП - це цілком розумне використання Опціонального. Я, звичайно, не можу говорити про проблемну область ОП, але це може бути спосіб визначити, чи можна щось вирішити, намагатися вирішити. Якщо так, то необов'язково з’єднує логічний результат "було це вирішувано" з результатом роздільної здатності, якщо це вдалося, в один виклик API.
Стюарт відзначає

2
Стюарт в основному правильний. У мене є набір пошукових термінів у порядку бажаності, і я шукаю знайти результат першого, який повертає що-небудь. Так в основному Optional<Result> searchFor(Term t). Це, мабуть, відповідає наміру Факультативу. Крім того, потоки (s) повинні бути ліниво оцінені, тому ніяких додаткових робіт, що вирішують терміни, що минають раніше першого, не повинно виникати.
Yona Appletree

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