it-swarm.com.ru

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

Новый потоковый фреймворк 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();

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

202
Yona Appletree

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 (), но это дело вкуса.

219
Stuart Marks

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

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

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

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

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

63
Stuart Marks

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

Вы утверждаете, что не хотите .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();

is на самом деле лучший способ выполнить то, что вы хотите, то есть вы хотите сохранить результат как 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 внутри потоковых операций.

Заключение

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

12
skiwi

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

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

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

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

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

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

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

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
4
Roland Tepp

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

2
charlie

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

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

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

2
Daniel Dietrich

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

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

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

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

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

1
Ljubopytnov