스트림을 두 개의 스트림으로 분할 할 수 있습니까?


146

Java 8 스트림으로 표시되는 데이터 세트가 있습니다.

Stream<T> stream = ...;

임의의 하위 집합을 얻기 위해 필터링하는 방법을 볼 수 있습니다-예를 들어

Random r = new Random();
PrimitiveIterator.OfInt coin = r.ints(0, 2).iterator();   
Stream<T> heads = stream.filter((x) -> (coin.nextInt() == 0));

또한이 스트림을 줄여 데이터 세트의 임의의 절반을 나타내는 두 개의 목록을 얻은 다음 다시 스트림으로 변환하는 방법을 알 수 있습니다. 그러나 초기 스트림에서 두 개의 스트림을 생성하는 직접적인 방법이 있습니까? 같은 것

(heads, tails) = stream.[some kind of split based on filter]

통찰력을 가져 주셔서 감사합니다.


Mark의 답변은 Louis의 답변보다 훨씬 도움이되지만 Louis의 답변은 원래 질문과 더 관련이 있습니다. 문제는 오히려 변환 할 수있는 가능성에 초점을 맞추고 Stream배수 Stream중간 변환없이 나는이 질문에 도달 사람들이 실제로 그렇게 관계없이 마크의 대답은 이러한 제약 조건의 달성하기 위해 방법을 찾고 있다고 생각하지만. 이는 제목의 질문 이 설명 의 질문과 동일하지 않기 때문일 수 있습니다 .
devildelta

답변:


9

정확히. Stream하나에서 두 개를 얻을 수는 없습니다 . 이것은 이해가되지 않습니다. 동시에 다른 하나를 생성 할 필요없이 하나를 어떻게 반복 하시겠습니까? 스트림은 한 번만 조작 할 수 있습니다.

그러나 목록이나 무언가에 덤프하려는 경우 할 수 있습니다

stream.forEach((x) -> ((x == 0) ? heads : tails).add(x));

65
왜 이치에 맞지 않습니까? 스트림이 파이프 라인이기 때문에 원래 스트림의 두 제작자를 만들 수 없었을 이유가 없기 때문에 두 개의 스트림을 제공하는 수집기가이를 처리 할 수 ​​있습니다.
Brett Ryan

36
스레드 안전하지 않습니다. 컬렉션에 직접 추가하려고하는 나쁜 조언, 그래서 우리는 stream.collect(...)스레드 안전이 Collectors아닌 컬렉션 (동기화 된 잠금 경합이없는)에서도 잘 작동 하는 사전 정의 된 thread-safe 를 사용합니다. @MarkJeronimus의 베스트 답변.
YoYo

1
@JoD 헤드와 테일이 스레드에 안전하면 스레드에 안전합니다. 또한 비 병렬 스트림을 사용한다고 가정하면 순서 만 보장되지 않으므로 스레드로부터 안전합니다. 동시성 문제를 해결하는 것은 프로그래머에게 달려 있으므로 컬렉션이 스레드로부터 안전하다면이 답변은 완벽하게 적합합니다.
Nicolas

1
@Nixon 그것은 우리가 여기에있는 더 나은 솔루션의 존재에 적합하지 않습니다. 그러한 코드를 가지면 선례가 잘못되어 다른 사람들이 잘못된 방식으로 코드를 사용할 수 있습니다. 병렬 스트림이 사용되지 않더라도 단 한 걸음 떨어져 있습니다. 좋은 코딩 관행은 스트림 작업 중에 상태를 유지하지 않아야합니다. 우리가하는 다음 일은 Apache spark와 같은 프레임 워크에서 코딩하는 것이며, 동일한 방식으로 인해 예기치 않은 결과가 발생할 수 있습니다. 그것은 독창적 인 해결책이었습니다.
YoYo

1
@JoD 더 나은 솔루션은 아니지만 실제로는 비효율적입니다. 이러한 사고 방식은 의도하지 않은 결과를 방지하기 위해 기본적으로 모든 컬렉션이 스레드로부터 안전해야한다는 결론에 이르게됩니다.
니콜라스

301

콜렉터 이 사용될 수있다.

  • 두 가지 범주의 경우 Collectors.partitioningBy()factory를 사용하십시오 .

이렇게하면 Mapfrom Boolean을 만들고를 기준으로 List항목을 하나 또는 다른 목록에 넣습니다 Predicate.

참고 : 스트림 전체를 소비해야하므로 무한 스트림에서는 작동하지 않습니다. 스트림은 어쨌든 소비되기 때문에이 방법을 사용하면 메모리로 새로운 스트림을 만드는 대신 단순히 목록에 넣습니다. 출력으로 스트림이 필요한 경우 언제든지 해당 목록을 스트리밍 할 수 있습니다.

또한 사용자가 제공 한 헤드 전용 예제에서도 반복자가 필요하지 않습니다.

  • 이진 분할은 다음과 같습니다.
Random r = new Random();

Map<Boolean, List<String>> groups = stream
    .collect(Collectors.partitioningBy(x -> r.nextBoolean()));

System.out.println(groups.get(false).size());
System.out.println(groups.get(true).size());
  • 더 많은 카테고리는 Collectors.groupingBy()공장을 사용하십시오 .
Map<Object, List<String>> groups = stream
    .collect(Collectors.groupingBy(x -> r.nextInt(3)));
System.out.println(groups.get(0).size());
System.out.println(groups.get(1).size());
System.out.println(groups.get(2).size());

스트림이 아닌 Stream기본 스트림 중 하나 인 IntStream경우이 .collect(Collectors)방법을 사용할 수 없습니다. 컬렉터 팩토리없이 수동으로 수행해야합니다. 구현은 다음과 같습니다.

[2020-04-16 이후의 예제 2.0]

    IntStream    intStream = IntStream.iterate(0, i -> i + 1).limit(100000).parallel();
    IntPredicate predicate = ignored -> r.nextBoolean();

    Map<Boolean, List<Integer>> groups = intStream.collect(
            () -> Map.of(false, new ArrayList<>(100000),
                         true , new ArrayList<>(100000)),
            (map, value) -> map.get(predicate.test(value)).add(value),
            (map1, map2) -> {
                map1.get(false).addAll(map2.get(false));
                map1.get(true ).addAll(map2.get(true ));
            });

이 예제에서는 초기 컬렉션의 전체 크기로 ArrayLists를 초기화합니다 (이것이 전혀 알려지지 않은 경우). 이렇게하면 최악의 시나리오에서도 크기 조정 이벤트를 방지 할 수 있지만 2 * N * T 공간을 잠재적으로 증가시킬 수 있습니다 (N = 초기 요소 수, T = 스레드 수). 속도를 위해 공간을 절충하기 위해 한 파티션에서 예상되는 가장 많은 수의 요소 (일반적으로 균형 잡힌 분할의 경우 N / 2 이상)와 같이 공간을 그대로 두거나 가장 잘 교육 된 추측을 사용할 수 있습니다.

Java 9 메소드를 사용하여 다른 사람을 화나게하지 않기를 바랍니다. Java 8 버전의 경우 편집 히스토리를보십시오.


2
아름다운. 그러나 IntStream의 마지막 솔루션은 병렬 스트림의 경우 스레드로부터 안전하지 않습니다. 해결책은 생각보다 훨씬 간단합니다 ... stream.boxed().collect(...);! 그것은 광고 된대로 할 것입니다 : 기본 형식 IntStream을 박스 Stream<Integer>버전으로 변환하십시오 .
YoYo

32
OP 질문을 직접 해결하므로 허용되는 답변이어야합니다.
ejel

27
스택 오버플로를 사용하면 더 나은 답변을 찾은 경우 커뮤니티가 선택한 답변을 무시할 수 있기를 바랍니다.
GuiSim

이것이 질문에 대한 대답인지 확실하지 않습니다. 질문은 스트림을 목록이 아닌 스트림으로 분할하도록 요청합니다.
AlikElzin-kilaka

1
누산기 기능은 불필요하게 장황합니다. 대신 (map, x) -> { boolean partition = p.test(x); List<Integer> list = map.get(partition); list.add(x); }간단하게 사용할 수 있습니다 (map, x) -> map.get(p.test(x)).add(x). 또한 collect작업이 스레드로부터 안전하지 않아야 하는 이유 는 없습니다. 작동 해야하는 방식과 정확히 일치하는 방식으로 Collectors.partitioningBy(p)작동합니다. 그러나 권투를 두 번 피하기 위해를 사용하지 않을 때 IntPredicate대신 대신을 사용합니다 . Predicate<Integer>boxed()
Holger

21

나는이 질문을 내 자신으로 우연히 발견했으며 분기 된 스트림에는 유효한 것으로 입증 된 유스 케이스가 있다고 생각합니다. 나는 아래 코드를 소비자로 작성하여 아무것도하지 않지만 함수 및 다른 것들에 적용 할 수 있습니다.

class PredicateSplitterConsumer<T> implements Consumer<T>
{
  private Predicate<T> predicate;
  private Consumer<T>  positiveConsumer;
  private Consumer<T>  negativeConsumer;

  public PredicateSplitterConsumer(Predicate<T> predicate, Consumer<T> positive, Consumer<T> negative)
  {
    this.predicate = predicate;
    this.positiveConsumer = positive;
    this.negativeConsumer = negative;
  }

  @Override
  public void accept(T t)
  {
    if (predicate.test(t))
    {
      positiveConsumer.accept(t);
    }
    else
    {
      negativeConsumer.accept(t);
    }
  }
}

이제 코드 구현은 다음과 같습니다.

personsArray.forEach(
        new PredicateSplitterConsumer<>(
            person -> person.getDateOfBirth().isPresent(),
            person -> System.out.println(person.getName()),
            person -> System.out.println(person.getName() + " does not have Date of birth")));

20

불행히도, 당신이 요구하는 것은 JavaDoc of Stream 에서 직접 찌푸 립니다 .

스트림은 한 번만 작동해야합니다 (중간 또는 터미널 스트림 작동 호출). 예를 들어, 동일한 소스가 둘 이상의 파이프 라인 또는 동일한 스트림의 여러 순회를 공급하는 "포크 (forked)"스트림을 제외합니다.

이러한 peek유형의 동작을 진정으로 원한다면이 방법을 사용 하거나 다른 방법을 사용할 수 있습니다 . 이 경우해야 할 일은 포크 필터를 사용하여 동일한 원본 스트림 소스에서 두 개의 스트림을 백업하는 대신 스트림을 복제하고 각 중복 항목을 적절하게 필터링하는 것입니다.

그러나 Stream사용 사례에 적합한 구조 인 경우 재고를 다시 고려할 수 있습니다.


6
긴 단일 스트림 항목은에 간다 같이 javadoc의 문구는 여러 스트림으로 분할 배제하지 않는 이들의
Thorbjørn Ravn 안데르센

2
@ ThorbjørnRavnAndersen 스트림 항목을 복제하는 것이 분기 스트림에 대한 주요 장애가 될지 확실하지 않습니다. 주요 문제는 포크 작업이 본질적으로 터미널 작업이므로 포크를 결정하면 기본적으로 일종의 컬렉션을 만드는 것입니다. 예를 들어 메서드를 작성할 수 List<Stream> forkStream(Stream s)있지만 결과 스트림은 filter터미널 스트림 작업이 아닌 기본 스트림이 아닌 컬렉션에 의해 적어도 부분적으로 백업됩니다 .
Trevor Freeman

7
이것이 스트림 의 포인트가 잠재적으로 무한한 요소 집합에 작업을 적용하고 실제 작업에 종종 분할이 필요하기 때문에 Java 스트림이 github.com/ReactiveX/RxJava/wiki에 비해 약간 반쯤 빠졌다고 생각하는 이유 중 하나입니다 , 스트림 복제 및 병합
Usman Ismail

8

이것은 스트림의 일반적인 메커니즘에 위배됩니다. 원하는대로 스트림 S0을 Sa와 Sb로 분할 할 수 있다고 가정합니다. 터미널 작업 수행count() 를 들어 Sa에서 하면 반드시 S0의 모든 요소를 ​​"소비"해야합니다. 따라서 Sb는 데이터 소스를 잃었습니다.

이전에는 스트림에 tee() 에 스트림을 두 개로 복제하는 방법이 있다고 생각했습니다. 이제 제거되었습니다.

Stream에는 peek () 메서드가 있지만이를 사용하여 요구 사항을 달성 할 수 있습니다.


1
peek정확히 예전의 것 tee입니다.
Louis Wasserman

5

정확하게는 아니지만 호출하여 필요한 것을 달성 할 수 있습니다 Collectors.groupingBy(). 새 컬렉션을 만든 다음 해당 새 컬렉션에서 스트림을 인스턴스화 할 수 있습니다.


2

이것은 내가 생각해 낼 수있는 가장 나쁜 대답이었습니다.

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;

public class Test {

    public static <T, L, R> Pair<L, R> splitStream(Stream<T> inputStream, Predicate<T> predicate,
            Function<Stream<T>, L> trueStreamProcessor, Function<Stream<T>, R> falseStreamProcessor) {

        Map<Boolean, List<T>> partitioned = inputStream.collect(Collectors.partitioningBy(predicate));
        L trueResult = trueStreamProcessor.apply(partitioned.get(Boolean.TRUE).stream());
        R falseResult = falseStreamProcessor.apply(partitioned.get(Boolean.FALSE).stream());

        return new ImmutablePair<L, R>(trueResult, falseResult);
    }

    public static void main(String[] args) {

        Stream<Integer> stream = Stream.iterate(0, n -> n + 1).limit(10);

        Pair<List<Integer>, String> results = splitStream(stream,
                n -> n > 5,
                s -> s.filter(n -> n % 2 == 0).collect(Collectors.toList()),
                s -> s.map(n -> n.toString()).collect(Collectors.joining("|")));

        System.out.println(results);
    }

}

이것은 정수 스트림을 취하여 5로 나눕니다. 5보다 큰 숫자의 경우 짝수 만 필터링하여 목록에 넣습니다. 나머지는 |와 결합합니다.

출력 :

 ([6, 8],0|1|2|3|4|5)

스트림을 깨는 중간 컬렉션으로 모든 것을 수집하기 때문에 이상적이지 않습니다 (논쟁이 너무 많습니다!)


1

스트림에서 특정 요소를 필터링하고 오류로 기록하는 방법을 찾는 동안이 질문을 우연히 발견했습니다. 따라서 나는 방해하지 않는 구문으로 술어에 조기 종료 조치를 첨부하는 것처럼 스트림을 분할 할 필요가 없었습니다. 이것이 내가 생각해 낸 것입니다.

public class MyProcess {
    /* Return a Predicate that performs a bail-out action on non-matching items. */
    private static <T> Predicate<T> withAltAction(Predicate<T> pred, Consumer<T> altAction) {
    return x -> {
        if (pred.test(x)) {
            return true;
        }
        altAction.accept(x);
        return false;
    };

    /* Example usage in non-trivial pipeline */
    public void processItems(Stream<Item> stream) {
        stream.filter(Objects::nonNull)
              .peek(this::logItem)
              .map(Item::getSubItems)
              .filter(withAltAction(SubItem::isValid,
                                    i -> logError(i, "Invalid")))
              .peek(this::logSubItem)
              .filter(withAltAction(i -> i.size() > 10,
                                    i -> logError(i, "Too large")))
              .map(SubItem::toDisplayItem)
              .forEach(this::display);
    }
}

0

롬복을 사용하는 더 짧은 버전

import java.util.function.Consumer;
import java.util.function.Predicate;

import lombok.RequiredArgsConstructor;

/**
 * Forks a Stream using a Predicate into postive and negative outcomes.
 */
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PROTECTED)
public class StreamForkerUtil<T> implements Consumer<T> {
    Predicate<T> predicate;
    Consumer<T> positiveConsumer;
    Consumer<T> negativeConsumer;

    @Override
    public void accept(T t) {
        (predicate.test(t) ? positiveConsumer : negativeConsumer).accept(t);
    }
}

-3

어때요?

Supplier<Stream<Integer>> randomIntsStreamSupplier =
    () -> (new Random()).ints(0, 2).boxed();

Stream<Integer> tails =
    randomIntsStreamSupplier.get().filter(x->x.equals(0));
Stream<Integer> heads =
    randomIntsStreamSupplier.get().filter(x->x.equals(1));

1
공급 업체가 두 번 호출되므로 두 개의 다른 임의 수집이 제공됩니다. 나는이에 고르게에서 가능성을 분할 할 수있는 OP의 마음을 생각 동일한 생성 순서
USR-로컬 ΕΨΗΕΛΩΝ
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.