Использование 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()метода (или он является 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.
Йона Апплетри


10
Самое смешное, что JDK-8050820 на самом деле ссылается на этот вопрос в своем описании!
Дидье Л

Ответы:


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
@ Привет, спасибо. Техника .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>>. Если значение присутствовало во входном необязательном элементе, возвращаемый необязательный элемент содержит поток, содержащий единственный результат Other. Но если значение не присутствовало, результатом является пустой Необязательный.

Далее вызов to 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, однако в результате вы теперь отображаете его на a 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 в метод статической утилиты, и тогда она станет такой:

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

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

1
И, к сожалению, выполнение каждого решения является нарушителем соглашения. Но это умно.
Йона Апплетри

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)как последний вызов в цепочке.

Это имеет хороший эффект избавления от двойного Optional, когда поток находит реальный результат.

Вы не можете быть немного короче, чем это в 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

Я думаю так же, как Роланд Тепп. Зачем кому-то делать стрим <стрим <? >> и флет, если вы можете просто флетить с одним необязательным <опционально <? >>
Юнг Хён Ю,

3

Если вы не против использовать стороннюю библиотеку, вы можете использовать Javaslang . Это похоже на Scala, но реализовано на Java.

Он поставляется с полной неизменной коллекционной библиотекой, которая очень похожа на ту, что известна из Scala. Эти коллекции заменяют коллекции Java и поток Java 8. Он также имеет собственную реализацию Option.

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 поддерживается потоком, предоставленным My library 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 Optional не предназначена для использования таким образом. Обычно он зарезервирован только для операций терминального потока, которые могут возвращать или не возвращать значение, например, например, для поиска.

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

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

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


6
Я думаю, что метод resol () OP, возвращающий Optional <Other>, является вполне разумным использованием Optional. Конечно, я не могу говорить с проблемной областью ОП, но возможно, что способ определить, является ли что-то разрешимым, состоит в том, чтобы попытаться разрешить это. Если это так, Optional объединяет логический результат «Было ли это разрешимо» с результатом разрешения, если оно прошло успешно, в один вызов API.
Стюарт Маркс

2
Стюарт в основном прав. У меня есть набор поисковых терминов в порядке желательности, и я ищу, чтобы найти результат первого, который возвращает что-нибудь. Так в основном Optional<Result> searchFor(Term t). Это, кажется, соответствует намерению Факультативного. Кроме того, stream () следует оценивать лениво, поэтому не нужно выполнять дополнительные термины по разрешению после первого совпадения.
Йона Апплетри

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