Использование Java 8 необязательно с Stream::flatMap

Новая потоковая среда Java 8 и ее друзья создают очень лаконичный код Java, но я натолкнулся на кажущуюся простой ситуацию, которую сложно сделать кратко.

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

Лучшее, что я могу придумать, это:

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

Но это кажется ужасно скучным для того, что кажется очень распространенным случаем. У кого-нибудь есть идея получше?

10 ответов

Решение

Java 9

Optional.stream был добавлен в JDK 9. Это позволяет вам делать следующее без использования какого-либо вспомогательного метода:

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

Java 8

Да, это была небольшая дыра в API, потому что несколько неудобно превращать Optional в поток нулевой или одной длины. Вы могли бы сделать это:

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();

Здесь я включил вызов метод92 () вместо отдельной операции map(), но это дело вкуса.

Я добавляю этот второй ответ на основе предложенного редактирования пользователем srborlongan к моему другому ответу. Я думаю, что предложенная техника была интересной, но она не очень подходила для редактирования моего ответа. Другие согласились, и предложенное редактирование было отклонено. (Я не был одним из избирателей.) Однако у техники есть свои достоинства. Было бы лучше, если бы srborlongan разместил свой ответ. Этого еще не произошло, и я не хотел, чтобы техника терялась в тумане отклоненной истории редактирования Stackru, поэтому я решил представить ее как отдельный ответ.

В основном техника заключается в использовании некоторых из 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. Но если значение не присутствовало, результатом является пустой Необязательный.

Далее звонок orElseGet(Stream::empty) возвращает значение типа Stream<Other>, Если его входное значение присутствует, оно получает значение, которое является единственным элементом Stream<Other>, В противном случае (если входное значение отсутствует), возвращается пустое Stream<Other>, Таким образом, результат правильный, такой же, как исходный условный код.

В комментариях, обсуждающих мой ответ относительно отклоненного редактирования, я описал эту технику как "более краткую, но и более неясную". Я поддерживаю это. Мне потребовалось некоторое время, чтобы понять, что он делает, и мне также понадобилось время, чтобы написать приведенное выше описание того, что он делал. Ключевой тонкостью является преобразование из Optional<Other> в Optional<Stream<Other>>, Как только вы поймете это, это имеет смысл, но это не было очевидно для меня.

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

ОБНОВИТЬ: Optional.stream был добавлен в JDK 9.

Вы не можете сделать это более кратким, как вы уже делаете.

Вы утверждаете, что не хотите .filter(Optional::isPresent) а также .map(Optional::get),

Это было решено методом, описанным @StuartMarks, однако в результате вы теперь отображаете его на Optional<T>, так что теперь вам нужно использовать .flatMap(this::streamopt) и 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 внутри потока операций.

Заключение

Метод, который вы использовали, на самом деле лучший метод.

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

Краткий ответ:

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

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 Stackru {

    public static void main( String... args ) {
        try {
            final int integer = Stream.of( args )
                    .peek( s -> System.out.println( "Looking at " + s ) )
                    .map( Stackru::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

Немного более короткая версия с использованием reduce:

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

Вы также можете переместить функцию Reduce в метод статической утилиты, и тогда она станет такой:

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

Я хотел бы продвинуть фабричные методы для создания помощников для функциональных 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(...)

Поздно на вечеринку, но как насчет

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

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

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

Если вы сразу же вернете поток из функции разрешения, вы сохраните еще одну строку.

Если вы застряли на 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();

Null поддерживается потоком, предоставленным My library AbacusUtil. Вот код:

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

Если вы не против использовать стороннюю библиотеку, вы можете использовать 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.

Что об этом?

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

/questions/3531336/java-lyambda-kak-projti-neobyazatelnyij-spisok-potok-optsionnyih/55318742#55318742

Скорее всего, вы делаете это неправильно.

Java 8 Optional не предназначена для использования таким образом. Обычно он зарезервирован только для операций терминального потока, которые могут возвращать или не возвращать значение, например, например, find.

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

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

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

Другие вопросы по тегам