Stream :: flatMap과 함께 Java 8의 Optional 사용하기


240

새로운 Java 8 스트림 프레임 워크와 친구는 매우 간결한 Java 코드를 만들지 만 간결하게하기가 까다로워 보이는 단순한 상황을 발견했습니다.

a List<Thing> things와 method를 고려하십시오 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();

그러나 그것은 매우 일반적인 경우처럼 보일 정도로 끔찍한 것처럼 보입니다. 더 좋은 아이디어가 있습니까?


예제로 약간 코딩 한 후에는 명시 적 버전이 존재하는 경우 관련 버전보다 더 읽기 쉽다 .flatMap(Optional::toStream)는 것을 알았습니다.
— skiwi

19
@skiwi 음, Optional.stream지금 JDK 9에 존재합니다 ....
— Stuart Marks

나는 이것이 어디에 문서화되어 있고 그것을 얻는 과정이 무엇인지 궁금합니다. 실제로 존재하는 것처럼 보이는 다른 방법이 있으며 API 변경에 대한 토론이 진행되는 곳이 궁금합니다.
— Yona Appletree 2015 년


10
재밌는 점은 JDK-8050820이 실제로이 질문에 대한 설명에서 언급한다는 것입니다!
— Didier L

답변:


265

자바 9

Optional.stream JDK 9에 추가되었습니다.이를 통해 헬퍼 메소드가 없어도 다음을 수행 할 수 있습니다.

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

자바 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
Java 9까지 API가 바뀔 수 있다고 생각하지 않습니다.
— assylias

5
@Hypher 감사합니다. .filter (). map () 기술은 그리 나쁘지 않으며 도우미 메서드에 대한 종속성을 피합니다. '보다 간결한 방법이 있다면 좋을 것입니다. Optional.stream () 추가 여부를 조사하겠습니다.
— Stuart Marks

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>>. 선택 입력에 값이 있으면 반환 된 선택에 단일 기타 결과가 포함 된 스트림이 포함됩니다. 그러나 값이 없으면 결과는 비어 있습니다.

다음에 대한 호출 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에 의해 해결 된 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스트림 작업 내부를 던질 것 입니다.

결론

사용한 방법이 실제로 가장 좋은 방법입니다.


6

다음을 사용하는 약간 짧은 버전 reduce:

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

reduce 함수를 정적 유틸리티 메소드로 이동하면 다음과 같이됩니다.

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

6
나는 이것을 좋아하지만 Stream의 모든 항목을 평가하는 반면 findFirst ()는 현재 항목을 찾을 때까지만 평가한다는 점을 지적 할 가치가 있습니다.
— Duncan McGregor

1
불행히도, 각 해결을 실행하는 것은 거래를 중단시키는 사람입니다. 그러나 영리합니다.
— Yona Appletree

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)한 것입니다.

스트림이 실제 결과를 찾을 때마다 double-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을 사용할 수 있습니다 . 스칼라와 비슷하지만 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 + ")"; }
}

면책 조항 : 저는 Javaslang의 제작자입니다.


3

파티에 늦었지만

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

옵션을 스트림으로 수동으로 변환하는 util 메소드를 작성하면 마지막 get ()을 제거 할 수 있습니다.

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

resolve 함수에서 즉시 스트림을 반환하면 한 줄을 더 저장합니다.


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은 내 라이브러리 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 ()은 좋은 습관이 아닙니다. 그래서 나는 그것을 멀리하려고합니다.
— rastaman

.get() 없이 사용 isPresent()하면 IntelliJ
— OneCricketeer에서

-5

아마 당신은 잘못하고 있습니다.

Java 8 Optional은 이러한 방식으로 사용되지 않습니다. 일반적으로 찾기와 같은 값을 반환하거나 반환하지 않을 수있는 터미널 스트림 작업에만 예약되어 있습니다.

귀하의 경우 먼저 해결할 수있는 항목을 필터링하는 저렴한 방법을 찾은 다음 첫 번째 항목을 옵션으로 가져 와서 마지막 작업으로 해결하는 것이 좋습니다. 더 나은 방법-필터링 대신 첫 번째 해결 가능한 항목을 찾아서 해결하십시오.

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

경험상 스트림에서 항목을 다른 것으로 변환하기 전에 항목 수를 줄이려고 노력해야합니다. 물론 YMMV.


6
Optional <Other>를 반환하는 OP의 resolve () 메서드는 Optional을 완벽하게 사용하는 것으로 생각합니다. 물론 OP의 문제 영역과 이야기 할 수는 없지만 해결할 수 있는지 여부를 결정하는 방법은 문제를 해결하려고 시도하는 것일 수 있습니다. 그렇다면 Optional은 "이 문제를 해결할 수 있었다"라는 부울 결과를 성공한 경우 단일 API 호출로 해결 결과와 통합합니다.
— Stuart Marks

2
스튜어트는 기본적으로 정확합니다. 원하는 순서대로 검색어 세트가 있으며 첫 번째 결과를 반환하는 것을 찾고 있습니다. 기본적으로 Optional<Result> searchFor(Term t). 그것은 선택의 의도에 맞는 것 같습니다. 또한 stream ()은 느리게 평가되어야하므로 일치하는 첫 번째 항목을 지나는 추가 작업은 발생하지 않아야합니다.
— Yona Appletree 2014 년

질문은 완벽하게 의미가 있으며, FlatMap을 Optional과 함께 사용하는 것은 종종 Scala와 같은 다른 유사한 프로그래밍 언어에서 실행됩니다.
— dzs
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.