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

새로운 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();

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



답변

자바 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()작업 대신 호출을 인라인 했지만 맛의 문제입니다.


답변

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

당신은 여전히 를 반환해야합니다 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)한 것입니다.

스트림이 실제 결과를 찾을 때마다 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


답변

타사 라이브러리를 사용하지 않으려면 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의 제작자입니다.


답변

파티에 늦었지만

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

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

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

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