"스트림이 이미 작동되었거나 닫혔습니다"를 방지하기 위해 스트림을 복사합니다.


121

두 번 처리 할 수 ​​있도록 Java 8 스트림을 복제하고 싶습니다. 나는 collect목록으로 할 수 있고 그로부터 새로운 스트림을 얻을 수 있습니다 .

// doSomething() returns a stream
List<A> thing = doSomething().collect(toList());
thing.stream()... // do stuff
thing.stream()... // do other stuff

하지만 좀 더 효율적이고 우아한 방법이 있어야한다고 생각합니다.

컬렉션으로 변환하지 않고 스트림을 복사하는 방법이 있습니까?

나는 실제로 Eithers 스트림으로 작업하고 있으므로 오른쪽 투영으로 이동하고 다른 방식으로 처리하기 전에 왼쪽 투영을 한 방향으로 처리하고 싶습니다. 이런 종류의 (지금까지는 toList트릭 을 사용해야합니다 ).

List<Either<Pair<A, Throwable>, A>> results = doSomething().collect(toList());

Stream<Pair<A, Throwable>> failures = results.stream().flatMap(either -> either.left());
failures.forEach(failure -> ... );

Stream<A> successes = results.stream().flatMap(either -> either.right());
successes.forEach(success -> ... );

"단방향 처리"에 대해 자세히 설명해 주시겠습니까? 개체를 소비하고 있습니까? 매핑? partitionBy () 및 groupingBy ()를 사용하면 2 개 이상의 목록으로 직접 이동할 수 있지만 먼저 매핑하거나 forEach ()에 의사 결정 포크를 갖는 것이 도움이 될 수 있습니다.
AjahnCharles

어떤 경우에는 무한 스트림을 처리하는 경우 컬렉션으로 전환하는 것이 옵션이 될 수 없습니다. 여기에서 메모 화에 대한 대안을 찾을 수 있습니다. dzone.com/articles/how-to-replay-java-streams
Miguel Gamboa

답변:


88

효율성에 대한 귀하의 가정은 다소 거꾸로 생각됩니다. 데이터를 저장할 필요가 없기 때문에 데이터를 한 번만 사용하려는 경우 이러한 엄청난 효율성을 얻을 수 있으며, 스트림은 파이프 라인을 통해 전체 데이터를 효율적으로 흐르게하는 강력한 "루프 융합"최적화를 제공합니다.

동일한 데이터를 재사용하려면 정의에 따라 두 번 (결정적) 생성하거나 저장해야합니다. 이미 컬렉션에있는 경우 좋습니다. 두 번 반복하면 저렴합니다.

우리는 "포크 스트림"으로 디자인을 실험했습니다. 우리가 발견 한 것은이를 지원하는 데 실제 비용이 든다는 것입니다. 그것은 흔하지 않은 경우를 희생하여 일반적인 경우 (한 번 사용)를 부담했습니다. 큰 문제는 "두 파이프 라인이 동일한 속도로 데이터를 소비하지 않을 때 발생하는 일"을 처리하는 것이 었습니다. 이제 어쨌든 버퍼링으로 돌아갑니다. 이것은 분명히 그 무게를 지니지 않은 기능이었습니다.

동일한 데이터에 대해 반복적으로 작업하려면 데이터를 저장하거나 작업을 소비자로 구성하고 다음을 수행합니다.

stream()...stuff....forEach(e -> { consumerA(e); consumerB(e); });

처리 모델이 이러한 종류의 "스트림 포크"에 더 적합하기 때문에 RxJava 라이브러리를 살펴볼 수도 있습니다.


1
아마도 내가 "효율성"나는 내가 할 모든 데이터를 저장 즉시 경우 스트림 (그리고 저장하지 아무것도) 신경 이유에서 얻는 종류의이야를 사용해서는 안 ( toList) (은 IT를 처리 할 수 있어야하는 Either경우 예)?
Toby

11
스트림은 표현력이 풍부 하고 효율적 입니다. 코드를 읽는 방식에서 우발적 인 세부 사항 (예 : 중간 결과)없이 복잡한 집계 작업을 설정할 수 있다는 점에서 표현력이 뛰어납니다. 또한 일반적으로 데이터에 대해 단일 전달을 수행하고 중간 결과 컨테이너를 채우지 않는다는 점에서 효율적입니다. 이 두 속성을 함께 사용하면 여러 상황에서 매력적인 프로그래밍 모델이됩니다. 물론 모든 프로그래밍 모델이 모든 문제에 맞는 것은 아닙니다. 작업에 적합한 도구를 사용하고 있는지 여부를 결정해야합니다.
Brian Goetz

1
그러나 스트림을 재사용 할 수 없기 때문에 개발자가 두 가지 다른 방법으로 스트림을 처리하기 위해 중간 결과 (수집)를 저장해야하는 상황이 발생합니다. 스트림이 두 번 이상 생성된다는 의미는 (수집하지 않는 한) 분명해 보입니다. 그렇지 않으면 collect 메소드가 필요하지 않기 때문입니다.
Niall Connaughton

@NiallConnaughton 나는 당신의 요점이 원하는지 확실하지 않습니다. 두 번 횡단하려면 누군가이를 저장하거나 재생성해야합니다. 누군가가 두 번 필요한 경우를 대비하여 라이브러리가 버퍼링해야한다고 제안하고 있습니까? 그것은 어리석은 일입니다.
Brian Goetz

라이브러리가 버퍼링해야한다고 제안하는 것이 아니라 스트림을 일회성으로 사용함으로써 시드 스트림을 재사용하려는 사용자 (예 : 정의에 사용 된 선언적 논리 공유)가 여러 파생 스트림을 작성하여 수집하도록 강요한다고 말합니다. 시드 스트림 또는 시드 스트림의 복제본을 생성 할 제공자 팩토리에 액세스 할 수 있습니다. 두 옵션 모두 단점이 있습니다. 이 답변에는 stackoverflow.com/a/28513908/114200 주제에 대한 자세한 내용이 있습니다.
Niall Connaughton

73

와 함께 로컬 변수를 사용 Supplier하여 스트림 파이프 라인의 공통 부분을 설정할 수 있습니다.

에서 http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/ :

스트림 재사용

Java 8 스트림은 재사용 할 수 없습니다. 터미널 작업을 호출하자마자 스트림이 닫힙니다.

Stream<String> stream = Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> s.startsWith("a"));
stream.anyMatch(s -> true);    // ok
stream.noneMatch(s -> true);   // exception

Calling `noneMatch` after `anyMatch` on the same stream results in the following exception:
java.lang.IllegalStateException: stream has already been operated upon or closed
at 
java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at 
java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
at com.winterbe.java8.Streams5.test7(Streams5.java:38)
at com.winterbe.java8.Streams5.main(Streams5.java:28)

이 한계를 극복하려면 실행하려는 모든 터미널 작업에 대해 새 스트림 체인을 만들어야합니다. 예를 들어 모든 중간 작업이 이미 설정된 새 스트림을 생성하는 스트림 공급자를 만들 수 있습니다.

Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
            .filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true);   // ok
streamSupplier.get().noneMatch(s -> true);  // ok

에 대한 각 호출 get()은 원하는 터미널 작업을 호출하기 위해 저장되는 새 스트림 을 생성합니다.


2
멋지고 우아한 솔루션. 가장 많이 찬성 된 솔루션보다 훨씬 더 많은 java8-ish.
dylaniato

를 "비용이 많이 드는"방식으로 구축 한 Supplier경우 사용에 대한 참고 사항 만 있으면 Stream으로 호출 할 때마다 해당 비용을 지불Supplier.get() 하게 됩니다 . 즉, 데이터베이스 쿼리가 ... 해당 쿼리가 매번 수행되는 경우
Julien

IntStream을 사용하더라도 mapTo 후에는이 패턴을 따를 수 없습니다. 나는 그것을 다시 Set<Integer>using collect(Collectors.toSet())... 로 변환하고 그것에 대해 몇 가지 작업을 수행해야한다는 것을 알았습니다 . 내가 원했고 max()특정 값이 두 개의 연산으로 설정된 경우 ...filter(d -> d == -1).count() == 1;
JGFMK

16

를 사용하여 Supplier각 종료 작업에 대한 스트림을 생성합니다.

Supplier<Stream<Integer>> streamSupplier = () -> list.stream();

해당 컬렉션의 스트림이 필요할 때마다을 사용 streamSupplier.get()하여 새 스트림을 가져옵니다.

예 :

  1. streamSupplier.get().anyMatch(predicate);
  2. streamSupplier.get().allMatch(predicate2);

여기에서 공급 업체를 처음으로 지적 했으므로 찬성합니다.
EnzoBnl

9

우리는 구현했습니다 duplicate()에서 스트림을위한 방법을 jOOλ , 우리가에 대한 통합 테스트를 개선하기 위해 만든 오픈 소스 라이브러리 jOOQ을 . 기본적으로 다음과 같이 작성할 수 있습니다.

Tuple2<Seq<A>, Seq<A>> duplicates = Seq.seq(doSomething()).duplicate();

내부적으로는 한 스트림에서 소비되었지만 다른 스트림에서는 소비되지 않은 모든 값을 저장하는 버퍼가 있습니다. 두 스트림이 거의 동일한 속도로 소비되고 thread-safety 부족으로 살 수 있다면 그것은 아마도 효율적일 것입니다 .

알고리즘 작동 방식은 다음과 같습니다.

static <T> Tuple2<Seq<T>, Seq<T>> duplicate(Stream<T> stream) {
    final List<T> gap = new LinkedList<>();
    final Iterator<T> it = stream.iterator();

    @SuppressWarnings("unchecked")
    final Iterator<T>[] ahead = new Iterator[] { null };

    class Duplicate implements Iterator<T> {
        @Override
        public boolean hasNext() {
            if (ahead[0] == null || ahead[0] == this)
                return it.hasNext();

            return !gap.isEmpty();
        }

        @Override
        public T next() {
            if (ahead[0] == null)
                ahead[0] = this;

            if (ahead[0] == this) {
                T value = it.next();
                gap.offer(value);
                return value;
            }

            return gap.poll();
        }
    }

    return tuple(seq(new Duplicate()), seq(new Duplicate()));
}

여기에 더 많은 소스 코드

Tuple2아마 당신처럼 Pair반면, 유형 Seq입니다 Stream몇 가지 향상된 기능.


2
이 솔루션은 스레드로부터 안전하지 않습니다. 스트림 중 하나를 다른 스레드로 전달할 수 없습니다. 두 스트림이 단일 스레드에서 동일한 비율로 소비 될 수 있고 실제로 두 개의 별개 스트림이 필요한 시나리오는 실제로 보이지 않습니다. 동일한 스트림에서 두 개의 결과를 생성하려면 결합 수집기를 사용하는 것이 훨씬 좋습니다 (이미 JOOL에 있음).
Tagir Valeev 2015

@TagirValeev : 스레드 안전성에 대해 맞습니다. 좋은 지적입니다. 수집가를 결합하여 어떻게 할 수 있습니까?
Lukas Eder 2015 년

1
나는 누군가가 이런 식으로 두 번 같은 스트림을 사용하고자하는 경우 의미 Tuple2<Seq<A>>, Seq<A>> t = duplicate(stream); long count = t.collect(counting()); List<A> list = t.collect(toList());, 더 나은로입니다 Tuple2<Long, List<A>> t = stream.collect(Tuple.collectors(counting(), toList()));. Collectors.mapping/reducing하나를 사용하면 다른 스트림 작업을 수집기 및 프로세스 요소로 표현할 수 있습니다. 따라서 일반적으로 중복없이 스트림을 한 번 소비하는 많은 작업을 수행 할 수 있으며 병렬 친화적입니다.
Tagir Valeev 2015

2
이 경우에도 계속해서 하나씩 스트림을 줄입니다. 따라서 어쨌든 전체 스트림을 후드 아래의 목록으로 수집하는 정교한 반복기를 도입하여 인생을 더 어렵게 만들 필요가 없습니다. 명시 적으로 목록에 수집 한 다음 OP가 말하는대로 두 개의 스트림을 생성 할 수 있습니다 (동일한 코드 행 수). 글쎄, 첫 번째 감소가 단락 인 경우에만 약간의 개선이있을 수 있지만 OP 사례는 아닙니다.
Tagir Valeev

1
@maaartinus : 감사합니다, 좋은 포인터입니다. 벤치 마크에 대한 문제 를 만들었습니다 . 나는 그것을 사용 offer()/의 poll()API하지만이 ArrayDeque똑같이 할 수 있습니다.
루카스 에더

7

실행 가능한 스트림을 만들 수 있습니다 (예 :).

results.stream()
    .flatMap(either -> Stream.<Runnable> of(
            () -> failure(either.left()),
            () -> success(either.right())))
    .forEach(Runnable::run);

적용 할 작업은 어디에 failure있고 있습니까 success? 그러나 이것은 꽤 많은 임시 객체를 생성하고 컬렉션에서 시작하여 두 번 스트리밍 / 반복하는 것보다 더 효율적이지 않을 수 있습니다.


4

요소를 여러 번 처리하는 또 다른 방법은 Stream.peek (Consumer) 를 사용하는 것입니다 .

doSomething().stream()
.peek(either -> handleFailure(either.left()))
.foreach(either -> handleSuccess(either.right()));

peek(Consumer) 필요한만큼 여러 번 연결할 수 있습니다.

doSomething().stream()
.peek(element -> handleFoo(element.foo()))
.peek(element -> handleBar(element.bar()))
.peek(element -> handleBaz(element.baz()))
.foreach(element-> handleQux(element.qux()));

peek는 이것을 위해 사용되어서는 안되는 것 같습니다 ( softwareengineering.stackexchange.com/a/308979/195787 참조 )
HectorJ

2
@HectorJ 다른 스레드는 요소 수정에 관한 것입니다. 여기서는 그게 아니라고 생각했습니다.
Martin

2

내가 기여한 라이브러리 인 cyclops-react 에는 Stream을 복제 할 수있는 정적 메서드가 있습니다 (그리고 jOOλ Tuple of Streams 반환).

    Stream<Integer> stream = Stream.of(1,2,3);
    Tuple2<Stream<Integer>,Stream<Integer>> streams =  StreamUtils.duplicate(stream);

주석을 참조하십시오. 기존 스트림에서 중복을 사용할 때 발생하는 성능 저하가 있습니다. 더 성능이 좋은 대안은 Streamable을 사용하는 것입니다.

Stream, Iterable 또는 Array에서 구성하고 여러 번 재생할 수있는 (lazy) Streamable 클래스도 있습니다.

    Streamable<Integer> streamable = Streamable.of(1,2,3);
    streamable.stream().forEach(System.out::println);
    streamable.stream().forEach(System.out::println);

AsStreamable.synchronizedFromStream (stream)-스레드간에 공유 할 수있는 방식으로 백업 컬렉션을 느리게 채울 Streamable을 만드는 데 사용할 수 있습니다. Streamable.fromStream (stream)은 동기화 오버 헤드를 발생시키지 않습니다.


2
물론 결과 스트림에는 상당한 CPU / 메모리 오버 헤드와 매우 열악한 병렬 성능이 있다는 점에 유의해야합니다. 또한이 솔루션은 스레드로부터 안전하지 않습니다 (결과 스트림 중 하나를 다른 스레드로 전달하고 병렬로 안전하게 처리 할 수 ​​없습니다). List<Integer> list = stream.collect(Collectors.toList()); streams = new Tuple2<>(list.stream(), list.stream())(OP가 제안한 것처럼) 훨씬 더 성능이 좋고 안전합니다 . 또한 귀하가 cyclop-streams의 저자라는 답변을 명시 적으로 공개하십시오. 이것을 읽으십시오 .
Tagir Valeev 2015

내가 저자임을 반영하도록 업데이트되었습니다. 또한 각각의 성능 특성을 논의하기에 좋은 점입니다. 위의 평가는 StreamUtils.duplicate에 대해 꽤 많이 있습니다. StreamUtils.duplicate는 한 스트림에서 다른 스트림으로 데이터를 버퍼링하여 CPU 및 메모리 오버 헤드를 모두 발생시킵니다 (사용 사례에 따라 다름). 그러나 Streamable.of (1,2,3)의 경우 매번 Array에서 직접 새 Stream이 생성되며 병렬 성능을 포함한 성능 특성은 일반적으로 생성 된 Stream과 동일합니다.
존 소매점을

또한 Stream에서 Streamable 인스턴스를 생성 할 수 있지만 Streamable이 생성 될 때이를 지원하는 컬렉션에 대한 액세스를 동기화하는 AsStreamable 클래스가 있습니다 (AsStreamable.synchronizedFromStream). 스레드 전체에서 사용하기에 더 적합합니다 (필요한 경우-스트림이 동일한 스레드에서 생성되고 재사용되는 시간의 99 %를 상상합니다).
John McClean 2015-09-17

Hi Tagir-귀하가 경쟁 도서관의 저자임을 귀하의 의견에 공개해야하지 않습니까?
존 소매점을

1
댓글은 답이 아니며 내 라이브러리에는 스트림을 복제하는 기능이 없기 때문에 여기에 내 라이브러리를 광고하지 않습니다 (쓸모 없다고 생각하기 때문에). 그래서 여기서 경쟁하지 않습니다. 물론 내 라이브러리와 관련된 솔루션을 제안 할 때 항상 내가 저자라고 명시 적으로 말합니다.
Tagir Valeev

0

이 특정 문제에 대해 파티셔닝을 사용할 수도 있습니다. 같은 것

     // Partition Eighters into left and right
     List<Either<Pair<A, Throwable>, A>> results = doSomething();
     Map<Boolean, Object> passingFailing = results.collect(Collectors.partitioningBy(s -> s.isLeft()));
     passingFailing.get(true) <- here will be all passing (left values)
     passingFailing.get(false) <- here will be all failing (right values)

0

스트림을 읽거나 반복 할 때 Stream Builder를 사용할 수 있습니다. 다음은 Stream Builder 문서입니다 .

https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.Builder.html

사용 사례

직원 스트림이 있고이 스트림을 사용하여 직원 데이터를 Excel 파일에 작성한 다음 직원 컬렉션 / 테이블을 업데이트해야한다고 가정 해 보겠습니다. [이는 Stream Builder 사용을 보여주는 사용 사례입니다] :

Stream.Builder<Employee> builder = Stream.builder();

employee.forEach( emp -> {
   //store employee data to excel file 
   // and use the same object to build the stream.
   builder.add(emp);
});

//Now this stream can be used to update the employee collection
Stream<Employee> newStream = builder.build();

0

비슷한 문제가 있었고 스트림 복사본을 만들 세 가지 중간 구조 인 a List, 배열 및 Stream.Builder. 나는 약간의 벤치 마크 프로그램을 작성했는데, 성능 관점에서 보면 List상당히 유사한 다른 두 가지보다 약 30 % 느리다는 것을 제안했습니다 .

배열로 변환 할 때의 유일한 단점은 요소 유형이 제네릭 유형 인 경우 까다 롭다는 것입니다. 따라서 나는 Stream.Builder.

나는 결국 다음을 만드는 작은 함수를 작성했습니다 Collector.

private static <T> Collector<T, Stream.Builder<T>, Stream<T>> copyCollector()
{
    return Collector.of(Stream::builder, Stream.Builder::add, (b1, b2) -> {
        b2.build().forEach(b1);
        return b1;
    }, Stream.Builder::build);
}

나는 그 어떤 스트림의 복사본을 만들 수 있습니다 str수행하여 str.collect(copyCollector())매우 스트림의 관용적 사용과 유지에 느끼는.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.