คุณสามารถแบ่งสตรีมเป็นสองสตรีมได้หรือไม่?


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]

ขอบคุณสำหรับความเข้าใจใด ๆ


คำตอบของมาร์คมีประโยชน์มากกว่าคำตอบของหลุยส์ แต่ฉันต้องบอกว่าหลุยส์เกี่ยวข้องกับคำถามเดิมมากขึ้น คำถามที่จะมุ่งเน้นไปที่ค่อนข้างเป็นไปได้ในการแปลงStreamหลายStreams โดยไม่ต้องแปลงกลางแต่ผมคิดว่าคนที่มาถึงคำถามนี้เป็นจริงหาวิธีการที่จะประสบความสำเร็จดังนั้นไม่คำนึงถึงข้อ จำกัด ดังกล่าวซึ่งเป็นคำตอบของมาร์ค ซึ่งอาจเกิดจากความจริงที่ว่าคำถามในชื่อเรื่องไม่เป็นเช่นเดียวกับที่ในคำอธิบาย
devildelta

คำตอบ:


9

ไม่แน่นอน คุณไม่สามารถรับสองStreamออกจากหนึ่งเดียว สิ่งนี้ไม่สมเหตุสมผล - คุณจะทำซ้ำสิ่งใดสิ่งหนึ่งโดยไม่จำเป็นต้องสร้างอีกแบบในเวลาเดียวกันได้อย่างไร สตรีมสามารถใช้งานได้เพียงครั้งเดียว

อย่างไรก็ตามหากคุณต้องการทิ้งมันลงในรายการหรือบางสิ่งคุณสามารถทำได้

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

65
ทำไมมันไม่สมเหตุสมผล เนื่องจากสตรีมเป็นไพพ์ไลน์ไม่มีเหตุผลที่มันไม่สามารถสร้างโปรดิวเซอร์สองคนของสตรีมดั้งเดิมได้ฉันสามารถเห็นสิ่งนี้ถูกจัดการโดยนักสะสมที่มีสตรีมสองสตรีม
Brett Ryan

36
ไม่ปลอดภัยเธรด คำแนะนำที่ไม่ถูกต้องพยายามเพิ่มลงในคอลเลกชันโดยตรงนั่นคือสาเหตุที่เรามีstream.collect(...)เธรดสำหรับเซฟเธรดที่กำหนดไว้ล่วงหน้าCollectorsซึ่งทำงานได้ดีแม้ในคอลเลกชันที่ไม่ใช้เธรดที่ปลอดภัย คำตอบที่ดีที่สุดโดย @MarkJeronimus
YoYo

1
@JoD มันเป็นเธรดที่ปลอดภัยถ้าหัวและก้อยนั้นปลอดภัยสำหรับเธรด นอกจากนี้สมมติว่ามีการใช้สตรีมที่ไม่ขนานเท่านั้นไม่รับประกันการสั่งซื้อดังนั้นจึงปลอดภัยสำหรับเธรด มันขึ้นอยู่กับโปรแกรมเมอร์ที่จะแก้ไขปัญหาที่เกิดขึ้นพร้อมกันดังนั้นคำตอบนี้เหมาะอย่างยิ่งหากคอลเลกชันที่มีความปลอดภัยด้าย
นิโคลัส

1
@ นิกสันมันไม่เหมาะในที่ที่มีทางออกที่ดีกว่าซึ่งเรามีที่นี่ มีรหัสดังกล่าวสามารถนำไปสู่แบบอย่างที่ไม่ดีทำให้ผู้อื่นใช้ในทางที่ผิด แม้ว่าจะไม่ได้ใช้สตรีมแบบขนาน แต่ก็อยู่ห่างออกไปเพียงขั้นตอนเดียวเท่านั้น การเขียนโค้ดที่ดีต้องการให้เราไม่รักษาสถานะไว้ระหว่างการดำเนินการสตรีม สิ่งต่อไปที่เราทำคือการเขียนโค้ดในกรอบงานเช่น Apache spark และแนวทางปฏิบัติเดียวกันจะนำไปสู่ผลลัพธ์ที่ไม่คาดคิดจริงๆ มันเป็นวิธีการแก้ปัญหาที่สร้างสรรค์ฉันให้สิ่งหนึ่งที่ฉันอาจจะเขียนเองเมื่อไม่นานมานี้
YoYo

1
@JoD มันไม่ใช่ทางออกที่ดีกว่า แต่จริงๆแล้วมันไม่มีประสิทธิภาพมากกว่านั่นคือแนวความคิดในที่สุดก็จบลงด้วยข้อสรุปที่ว่าคอลเล็คชั่นทั้งหมดควรมีความปลอดภัยโดยปริยายเพื่อป้องกันผลกระทบที่ไม่ได้ตั้งใจ
นิโคลัส

301

ตัวสะสมสามารถใช้สำหรับสิ่งนี้

  • สำหรับสองประเภทให้ใช้Collectors.partitioningBy()โรงงาน

สิ่งนี้จะสร้างMapจาก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)วิธีการจะไม่สามารถใช้ได้ คุณจะต้องทำด้วยตัวเองโดยไม่ต้องใช้โรงงานเก็บ การใช้งานมีลักษณะดังนี้:

[ตัวอย่าง 2.0 ตั้งแต่ 2020-04-16]

    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 = จำนวนเธรด) ในการแลกเปลี่ยนพื้นที่เพื่อความเร็วคุณสามารถละทิ้งมันหรือใช้การเดาที่มีการศึกษาดีที่สุดของคุณเช่นจำนวนองค์ประกอบสูงสุดที่คาดไว้ในพาร์ติชันเดียว

ฉันหวังว่าฉันจะไม่รุกรานใครโดยใช้วิธีการของ Java 9 สำหรับเวอร์ชัน Java 8 ให้ดูที่ประวัติการแก้ไข


2
สวย. อย่างไรก็ตามทางออกสุดท้ายสำหรับ IntStream จะไม่ปลอดภัยต่อเธรดในกรณีที่สตรีมแบบขนาน การแก้ปัญหานั้นง่ายกว่าที่คุณคิดว่ามันเป็น ... stream.boxed().collect(...);! มันจะทำตามที่โฆษณา: แปลงดั้งเดิมIntStreamเป็นStream<Integer>รุ่นที่บรรจุอยู่
YoYo

32
นี่ควรเป็นคำตอบที่ได้รับการยอมรับเนื่องจากมันแก้ปัญหา OP ได้โดยตรง
ejel

27
ฉันหวังว่า Stack Overflow จะอนุญาตให้ชุมชนแทนที่คำตอบที่เลือกหากพบคำตอบที่ดีกว่า
GuiSim

ฉันไม่แน่ใจว่านี่ตอบคำถาม คำถามที่ถามแยกกระแสเป็นลำธาร - ไม่ใช่รายการ
AlikElzin-kilaka

1
ฟังก์ชั่นการสะสมเป็น verbose ที่ไม่จำเป็น แทนที่จะคุณสามารถใช้(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 :

ควรดำเนินการสตรีมบน (เรียกใช้การดำเนินการสตรีมกลางหรือเทอร์มินัล) เพียงครั้งเดียว กฎนี้ออกตัวอย่างเช่นสตรีม "ทางแยก" ซึ่งแหล่งที่มาเดียวกันฟีดสองท่อหรือมากกว่าหรือหลายเส้นทางของสตรีมเดียวกัน

คุณสามารถหลีกเลี่ยงปัญหานี้ได้โดยใช้peekวิธีการอื่นหรือหากคุณต้องการพฤติกรรมประเภทนั้นอย่างแท้จริง ในกรณีนี้สิ่งที่คุณควรทำคือแทนที่จะพยายามสำรองสองสตรีมจากแหล่งสตรีมดั้งเดิมเดียวกันด้วยตัวกรองฟอร์กกิ้งคุณจะทำซ้ำสตรีมของคุณและกรองรายการที่ซ้ำกันแต่ละรายการอย่างเหมาะสม

อย่างไรก็ตามคุณอาจต้องการพิจารณาใหม่หาก a Streamเป็นโครงสร้างที่เหมาะสมสำหรับกรณีการใช้งานของคุณ


6
ถ้อยคำ Javadoc ไม่ได้ยกเว้นการแบ่งออกเป็นหลายลำธารตราบเท่าที่รายการกระแสเดียวเท่านั้นไปในหนึ่งเหล่านี้
Thorbjørn Ravn Andersen

2
@ ThorbjørnRavnAndersenฉันไม่แน่ใจว่าการทำซ้ำไอเท็มสตรีมเป็นอุปสรรคสำคัญต่อสตรีมแบบแยก ปัญหาหลักคือการดำเนินการฟอร์กเป็นการดำเนินการของเทอร์มินัลดังนั้นเมื่อคุณตัดสินใจที่จะแยกคุณจะสร้างคอลเลกชันบางประเภท เช่นฉันสามารถเขียนวิธีได้List<Stream> forkStream(Stream s)แต่อย่างน้อยกระแสข้อมูลของฉันจะได้รับการสนับสนุนบางส่วนจากคอลเลกชันและไม่ได้โดยตรงจากสตรีมพื้นฐานซึ่งต่างจากการพูดfilterซึ่งไม่ใช่การดำเนินการสตรีมเทอร์มินัล
เทรเวอร์ฟรีแมน

7
นี่คือหนึ่งในเหตุผลที่ฉันรู้สึกว่ากระแส Java เป็นบิตครึ่ง assed เมื่อเทียบกับgithub.com/ReactiveX/RxJava/wikiเพราะจุดของกระแสคือการใช้การดำเนินงานในชุดองค์ประกอบที่ไม่มีที่สิ้นสุดและการดำเนินงานในโลกแห่งความเป็นจริงต้องแยก การทำซ้ำและการรวมสตรีม
Usman Ismail

8

สิ่งนี้ขัดต่อกลไกทั่วไปของสตรีม สมมติว่าคุณสามารถแยกสตรีม S0 เป็น Sa และ Sb ได้ตามที่คุณต้องการ ทำการแสดงการทำงานของเทอร์มินัลใด ๆcount()บน Sa จะต้อง "กิน" องค์ประกอบทั้งหมดใน S0 ดังนั้น Sb จึงสูญเสียแหล่งข้อมูล

ก่อนหน้านี้สตรีมมี tee()วิธีหนึ่งฉันคิดว่าสตรีมซ้ำกับสองวิธี มันถูกลบออกตอนนี้

สตรีมมีวิธีการแอบดู () แต่คุณอาจจะสามารถใช้เพื่อให้บรรลุตามความต้องการของคุณ


1
peekteeเป็นสิ่งที่เคยเป็น
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 ที่จะแยกอัตราต่อรองจาก evens ในลำดับเดียวกันที่สร้างขึ้น
usr-local-ΕΨΗΕΛΩΝ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.