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.