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

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

Рассмотрим a 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();

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

Ответ 1

Java 9

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

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

Java 8

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

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

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

Ответ 2

Я добавляю этот второй ответ на основе предлагаемого редактирования пользователем 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 не будет добавлен (если он когда-либо будет).

UPDATE: Optional.stream добавлен в JDK 9.

Ответ 3

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

Вы утверждаете, что не хотите .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 внутри операций потока.

Заключение

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

Ответ 4

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

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

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

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

Ответ 5

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

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

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

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

Это будет соответствовать всем вашим требованиям:

  • Он найдет первый ответ, который разрешит непустую Optional<Result>
  • Он называет this::resolve лениво при необходимости
  • this::resolve не будет вызываться после первого непустого результата
  • Он вернет Optional<Result>

Более длинный ответ

Единственная модификация по сравнению с исходной версией OP заключалась в том, что я удалил .map(Optional::get) перед вызовом .findFirst() и добавил .flatMap(o -> o) в качестве последнего вызова в цепочке.

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

Вы не можете сделать это на Java меньше.

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

  • Вызов this.resolve,
  • на основе Optional.isPresent
  • возвращает результат и
  • некоторый способ борьбы с отрицательным результатом (когда ничего не найдено)

Чтобы доказать, что мое решение работает как рекламируемое, я написал небольшую тестовую программу:

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

Ответ 6

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

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

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.

Ответ 7

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

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

Ответ 8

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

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

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

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

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

Ответ 9

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

Ответ 10

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

Ответ 11

Как насчет этого?

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

fooobar.com/questions/16138026/...

Ответ 12

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

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

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

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

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