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



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



рассмотрим List<Thing> things и метод Optional<Other> resolve(Thing thing). Я хочу, чтобы карта Thingь 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();


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

786   9  

9 ответов:

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 немного громоздко, хотя, возможно, лучше написать a маленькая вспомогательная функция для этого:

/**
 * 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 (), но это вопрос вкуса.

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

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

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

вы еще нужно будет вернуть a 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 внутри операций потока.

вывод

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

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

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

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

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

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

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

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

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

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

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

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 + ")"; }
}

отказ от ответственности: я создатель Джавасланга.

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

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

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

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

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

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

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

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

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

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

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

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

Comments

    Ничего не найдено.