IT이야기

스트림에서 Java 8의 옵션 사용::flatMap

cyworld 2022. 5. 20. 21:41
반응형

스트림에서 Java 8의 옵션 사용::flatMap

새로운 자바 8 스트림 프레임워크와 친구들은 매우 간결한 자바 코드를 만들어 내지만, 나는 겉보기에는 간결하게 하기 어려운 간단한 상황을 우연히 만났다.

고려하다 aList<Thing> things방법 및 방법Optional<Other> resolve(Thing thing)나는 그 지도를 만들고 싶다.Thing로.Optional<Other>와 첫 첫을 .Other은 . 분한국 해은은은을 이다.things.stream().flatMap(this::resolve).findFirst()그렇지만flatMap 냇물을 반환해야 한다.Optional없다stream()(은 a)이다.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()수술은 하지만 이건 취향의 문제야

사용자 srborrongan의 제안된 편집을 기반으로 이 두 번째 답변을 내 다른 답변에 추가하려고 한다.제안된 기술은 흥미로웠다고 생각하지만, 내 대답에 대한 편집으로는 그다지 적합하지 않았다.다른 사람들은 동의했고, 제안된 편집본이 부결되었다. (나는 유권자 중 한 명이 아니었다.)하지만 이 기술은 장점이 있다.스보롱건이 직접 답변을 올렸더라면 가장 좋았을 것이다.이런 일은 아직 일어나지 않았고, 스택오버플로우의 미스트에서 편집 히스토리가 거부되는 것을 원하지 않았기 때문에, 나는 그것을 스스로 별도의 답변으로 표출하기로 했다.

기본적으로 기법은 몇 가지를 사용하는 것이다.Optional제3의 연산자를 사용할 필요가 없도록 교묘한 방법으로 하는 방법 (? : 또는 if/message 문.

내 인라인 예는 다음과 같이 다시 쓰일 것이다.

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>; 값이 있으면 그 값이 들어 있는 스트림을 반환하고, 값이 없으면 빈 스트림을 반환한다.꽤 간단하고 설명하기 쉽다.

수정이 영리하고 조건부를 회피한다는 장점이 있다.(일부 사람들은 3차 연산자를 싫어한다는 것을 알고 있다.잘못 사용하면 코드를 이해하기 어려울 수 있다.)하지만, 때때로 상황이 너무 영리할 수 있다.도 수정이 되는 으로 한다.Optional<Other>그러면 그것은 전화한다.Optional.map다음과 같이 정의된다.

값이 있으면 제공된 매핑 함수를 적용하고, null이 아닌 경우에는 결과를 설명하는 Option(옵션)을 반환한다.그렇지 않으면 빈 선택사항을 반환하십시오.

map(Stream::of)전화를 걸다Optional<Stream<Other>>입력 Option(옵션)에 값이 있는 경우 반환된 Option(옵션)은 단일 Other(기타) 결과를 포함하는 스트림을 포함한다.그러나 값이 없으면 빈 선택 항목이 된다.

다음, 에 대한 호출orElseGet(Stream::empty)유형 값을 반환하다.Stream<Other> 그 되는데, 그 인 단소인 입으면 일요인이다.Stream<Other>. 그렇지 않으면(입력 값이 없는 경우) 빈 값을 반환함Stream<Other> 따라서 결과는 정확하며, 원래의 조건부 코드와 동일하다.

나의 답변에 대해 토론하는 논평에서, 거부된 편집에 대해, 나는 이 기법을 "더 간결하지만 또한 더 모호하다"라고 묘사했다.나는 이것을 지지한다.그것이 무엇을 하고 있는지 알아내는 데 시간이 걸렸고, 그것이 무엇을 하고 있는지에 대한 위의 설명을 쓰는 데도 시간이 걸렸다.중요한 미묘함은 로부터의 변신이다.Optional<Other>Optional<Stream<Other>>한번 더듬어보면 말이 되긴 하지만, 그건 내게는 분명하지 않았다.

하지만 나는 처음에 알려지지 않았던 것들이 시간이 지남에 따라 관용적이 될 수 있다는 것을 인정하겠다.적어도 그 때까지는 이 테크닉이 실제로 가장 좋은 방법이 될 수도 있을 것이다.Optional.stream추가됨(있을 경우).

업데이트: Optional.streamJDK 9에 추가되었다.

너는 이미 하고 있는 것처럼 그것을 더 간결하게 할 수 없다.

당신은 당신이 원하지 않는다고 주장한다..filter(Optional::isPresent) , 그리고 .map(Optional::get).

이 문제는 @StuartMarks가 설명하는 방법에 의해 해결되었지만, 그 결과 당신은 이제 그것을 a에 매핑한다.Optional<T>그래서 이제 당신은.flatMap(this::streamopt)그리고 aget()종국에 가서는

그래서 그것은 여전히 두 개의 문장으로 구성되어 있고 당신은 이제 새로운 방법으로 예외를 받을 수 있다!왜냐하면, 모든 선택사항이 비어있다면?그러면.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(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)사슬의 마지막 통화로

이것은 스트림이 실제 결과를 찾을 때마다 이중 선택 항목을 제거하는 좋은 효과가 있다.

자바에서는 이것보다 더 짧게 할 수 없다.

더 일반적인 코드를 사용하는 대체 코드 조각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

파티에 늦었지만, 어때?

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

선택적 스트림을 수동으로 변환하는 유틸리티 방법을 만들면 마지막 get()을 제거할 수 있다.

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

해결 기능에서 스트림을 즉시 반환하면 한 줄 더 저장된다.

기능 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(...)

Java 8에 갇혀 있지만 Guava 21.0 이상에 액세스할 수 있는 경우 를 사용하여 선택 항목을 스트림으로 변환할 수 있다.

그러므로, 주어진

import com.google.common.collect.Streams;

너는 쓸 수 있다.

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

만약 당신이 제3자 도서관을 사용해도 괜찮다면 당신은 자바슬랑을 사용할 수 있다.그것은 스칼라와 같지만 자바에서 시행된다.

그것은 스칼라에서 알려진 것과 매우 유사한 완전한 불변의 수집 도서관과 함께 제공된다.이 컬렉션은 자바의 컬렉션과 자바 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은 My library AbacusUtil이 제공하는 스트림이 지원한다.코드:

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

그건 어때?

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

아마도 당신은 그것을 잘못하고 있을 것이다.

Java 8 Option(선택 사항)은 이러한 방식으로 사용해서는 안 된다.그것은 예를 들어 찾기처럼 값을 반환할 수도 있고 반환하지 않을 수도 있는 터미널 스트림 운영만을 위해 일반적으로 예약된다.

당신의 경우 먼저 해결 가능한 항목들을 걸러내는 값싼 방법을 찾아보고 선택사항으로 첫 번째 항목을 구해 마지막 작업으로 해결하는 것이 좋을 것이다.더 나은 방법 - 필터링 대신 첫 번째 확인 가능한 항목을 찾아 해결하십시오.

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

경험칙은 다른 것으로 변형하기 전에 스트림의 항목 수를 줄이기 위해 노력해야 한다는 것이다.물론 YMMV.

참조URL: https://stackoverflow.com/questions/22725537/using-java-8s-optional-with-streamflatmap

반응형