Toplu işleme özelliğine sahip Java 8 Stream


101

Bir öğe listesi içeren büyük bir dosyam var.

Bir grup öğe oluşturmak istiyorum, bu grupla bir HTTP isteği yapmak istiyorum (tüm öğeler HTTP isteğinde parametre olarak gereklidir). Bunu bir fordöngü ile çok kolay bir şekilde yapabilirim , ancak Java 8 aşığı olarak bunu Java 8'in Akış çerçevesi ile yazmayı denemek (ve tembel işlemenin faydalarından yararlanmak) istiyorum.

Misal:

List<String> batch = new ArrayList<>(BATCH_SIZE);
for (int i = 0; i < data.size(); i++) {
  batch.add(data.get(i));
  if (batch.size() == BATCH_SIZE) process(batch);
}

if (batch.size() > 0) process(batch);

Uzun bir şey yapmak istiyorum lazyFileStream.group(500).map(processBatch).collect(toList())

Bunu yapmanın en iyi yolu ne olabilir?


Gruplamanın nasıl yapılacağını tam olarak anlayamıyorum, üzgünüm, ancak Dosya # satırları dosyanın içeriğini tembel bir şekilde okuyacaktır.
Toby

1
yani temelde tersine ihtiyacınız var flatMap(+ akışları tekrar daraltmak için ek bir flatMap)? Bunun gibi bir şeyin standart kitaplıkta uygun bir yöntem olduğunu düşünmüyorum. Ya bir 3. taraf kitaplığı bulmanız ya da ayırıcılara ve / veya akış akışı yayan bir toplayıcıya dayalı olarak kendi kitaplığınızı yazmanız gerekecek
the8472

3
Belki ve Stream.generateile birleştirebilirsiniz , ancak sorun şu ki akışlar İstisnalar ile iyi gitmiyor. Ayrıca, bu muhtemelen iyi paralelleştirilemez. Döngünün hala en iyi seçenek olduğunu düşünüyorum . reader::readLinelimitfor
tobias_k

Sadece bir örnek kod ekledim. FlatMap'in gitmenin yolu olduğunu sanmıyorum. Özel bir Spliterator yazmam gerekebileceğinden şüpheleniyorum
Andy Dang

1
Bunun gibi sorular için "Akışın kötüye kullanımı" terimini kullanıyorum.
kervin

Yanıtlar:


13

Not! Bu çözüm, forEach'i çalıştırmadan önce tüm dosyayı okur.

Bunu , tek iş parçacıklı, sıralı akış kullanım durumları için Java 8 akışlarını genişleten bir kitaplık olan jOOλ ile yapabilirsiniz :

Seq.seq(lazyFileStream)              // Seq<String>
   .zipWithIndex()                   // Seq<Tuple2<String, Long>>
   .groupBy(tuple -> tuple.v2 / 500) // Map<Long, List<String>>
   .forEach((index, batch) -> {
       process(batch);
   });

Perde arkasında zipWithIndex()sadece:

static <T> Seq<Tuple2<T, Long>> zipWithIndex(Stream<T> stream) {
    final Iterator<T> it = stream.iterator();

    class ZipWithIndex implements Iterator<Tuple2<T, Long>> {
        long index;

        @Override
        public boolean hasNext() {
            return it.hasNext();
        }

        @Override
        public Tuple2<T, Long> next() {
            return tuple(it.next(), index++);
        }
    }

    return seq(new ZipWithIndex());
}

... oysa groupBy()API uygunluğu:

default <K> Map<K, List<T>> groupBy(Function<? super T, ? extends K> classifier) {
    return collect(Collectors.groupingBy(classifier));
}

(Sorumluluk reddi: jOOλ'nın arkasındaki şirket için çalışıyorum)


Vay. Tam olarak aradığım şey bu. Sistemimiz normalde veri akışlarını sırayla işler, bu yüzden bu Java 8'e geçmek için uygun olur.
Andy Dang 05

16
Bu çözümün tüm girdi akışını gereksiz yere ara Map
düzeye depoladığını unutmayın

129

Tamlık için işte bir Guava çözümü.

Iterators.partition(stream.iterator(), batchSize).forEachRemaining(this::process);

Soruda koleksiyon mevcuttur, bu nedenle bir akışa gerek yoktur ve şu şekilde yazılabilir:

Iterables.partition(data, batchSize).forEach(this::process);

11
Lists.partitionbahsetmem gereken başka bir varyasyon.
Ben Manes

2
bu tembel, değil mi? Streamilgili partiyi işlemeden önce tümünü belleğe
çağırmaz

1
@orirab evet. batchSizeYineleme başına öğeleri tüketeceği için gruplar arasında tembeldir .
Ben Manes

Lütfen bir göz
atar

62

Saf Java-8 uygulaması da mümkündür:

int BATCH = 500;
IntStream.range(0, (data.size()+BATCH-1)/BATCH)
         .mapToObj(i -> data.subList(i*BATCH, Math.min(data.size(), (i+1)*BATCH)))
         .forEach(batch -> process(batch));

JOOl'den farklı olarak, paralel olarak güzelce çalışabileceğini unutmayın ( datarastgele erişim listeniz olması koşuluyla ).


1
Ya verileriniz aslında bir akışsa? (bir dosyadaki veya hatta ağdaki satırlar diyelim).
Omry Yadan

7
@OmryYadan, soru girişi olan oldu List(bakınız data.size(), data.get()söz konusu). Sorulan soruyu cevaplıyorum. Başka bir sorunuz varsa, onun yerine sorun (bence akış sorusu da zaten sorulmuştu).
Tagir Valeev

1
Partiler paralel olarak nasıl işlenir?
soup_boy

38

Saf Java 8 çözümü :

Bunu zarif bir şekilde yapmak için, her partiyi işlemek için a batch sizeve a alan bir özel toplayıcı oluşturabiliriz Consumer:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.*;
import java.util.stream.Collector;

import static java.util.Objects.requireNonNull;


/**
 * Collects elements in the stream and calls the supplied batch processor
 * after the configured batch size is reached.
 *
 * In case of a parallel stream, the batch processor may be called with
 * elements less than the batch size.
 *
 * The elements are not kept in memory, and the final result will be an
 * empty list.
 *
 * @param <T> Type of the elements being collected
 */
class BatchCollector<T> implements Collector<T, List<T>, List<T>> {

    private final int batchSize;
    private final Consumer<List<T>> batchProcessor;


    /**
     * Constructs the batch collector
     *
     * @param batchSize the batch size after which the batchProcessor should be called
     * @param batchProcessor the batch processor which accepts batches of records to process
     */
    BatchCollector(int batchSize, Consumer<List<T>> batchProcessor) {
        batchProcessor = requireNonNull(batchProcessor);

        this.batchSize = batchSize;
        this.batchProcessor = batchProcessor;
    }

    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    public BiConsumer<List<T>, T> accumulator() {
        return (ts, t) -> {
            ts.add(t);
            if (ts.size() >= batchSize) {
                batchProcessor.accept(ts);
                ts.clear();
            }
        };
    }

    public BinaryOperator<List<T>> combiner() {
        return (ts, ots) -> {
            // process each parallel list without checking for batch size
            // avoids adding all elements of one to another
            // can be modified if a strict batching mode is required
            batchProcessor.accept(ts);
            batchProcessor.accept(ots);
            return Collections.emptyList();
        };
    }

    public Function<List<T>, List<T>> finisher() {
        return ts -> {
            batchProcessor.accept(ts);
            return Collections.emptyList();
        };
    }

    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
}

İsteğe bağlı olarak, bir yardımcı yardımcı program sınıfı oluşturun:

import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collector;

public class StreamUtils {

    /**
     * Creates a new batch collector
     * @param batchSize the batch size after which the batchProcessor should be called
     * @param batchProcessor the batch processor which accepts batches of records to process
     * @param <T> the type of elements being processed
     * @return a batch collector instance
     */
    public static <T> Collector<T, List<T>, List<T>> batchCollector(int batchSize, Consumer<List<T>> batchProcessor) {
        return new BatchCollector<T>(batchSize, batchProcessor);
    }
}

Örnek kullanım:

List<Integer> input = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> output = new ArrayList<>();

int batchSize = 3;
Consumer<List<Integer>> batchProcessor = xs -> output.addAll(xs);

input.stream()
     .collect(StreamUtils.batchCollector(batchSize, batchProcessor));

Biri bakmak isterse kodumu GitHub'da da yayınladım:

Github'a bağlantı


1
Akışınızdaki tüm öğeleri belleğe sığdıramadığınız sürece bu iyi bir çözümdür. Ayrıca sonsuz akışlarda da çalışmayacaktır - toplama yöntemi uçbirimdir, yani toplu iş akışı üretmek yerine akış tamamlanana kadar bekleyecek ve ardından sonucu gruplar halinde işleyecektir.
Alex Ackerman

2
@AlexAckerman sonsuz bir akış, sonlandırıcının asla çağrılmayacağı anlamına gelir, ancak akümülatör yine de çağrılır, böylece öğeler işlenmeye devam eder. Ayrıca, herhangi bir zamanda yalnızca öğelerin parti boyutunun bellekte olmasını gerektirir.
Solubris

@Solubris, haklısın! Benim hatam, bunu belirttiğiniz için teşekkürler - Birisi toplama yönteminin nasıl çalıştığı konusunda aynı fikre sahipse, referans için yorumu silmeyeceğim.
Alex Ackerman

Tüketiciye gönderilen liste, değiştirmeyi güvenli hale getirmek için kopyalanmalıdır, örneğin: batchProcessor.accept (copyOf (ts))
Solubris

19

Bunun gibi senaryolar için özel bir Spliterator yazdım. Giriş Akışından belirli bir boyuttaki listeleri dolduracaktır. Bu yaklaşımın avantajı, tembel işlem yapması ve diğer akış işlevleriyle çalışacak olmasıdır.

public static <T> Stream<List<T>> batches(Stream<T> stream, int batchSize) {
    return batchSize <= 0
        ? Stream.of(stream.collect(Collectors.toList()))
        : StreamSupport.stream(new BatchSpliterator<>(stream.spliterator(), batchSize), stream.isParallel());
}

private static class BatchSpliterator<E> implements Spliterator<List<E>> {

    private final Spliterator<E> base;
    private final int batchSize;

    public BatchSpliterator(Spliterator<E> base, int batchSize) {
        this.base = base;
        this.batchSize = batchSize;
    }

    @Override
    public boolean tryAdvance(Consumer<? super List<E>> action) {
        final List<E> batch = new ArrayList<>(batchSize);
        for (int i=0; i < batchSize && base.tryAdvance(batch::add); i++)
            ;
        if (batch.isEmpty())
            return false;
        action.accept(batch);
        return true;
    }

    @Override
    public Spliterator<List<E>> trySplit() {
        if (base.estimateSize() <= batchSize)
            return null;
        final Spliterator<E> splitBase = this.base.trySplit();
        return splitBase == null ? null
                : new BatchSpliterator<>(splitBase, batchSize);
    }

    @Override
    public long estimateSize() {
        final double baseSize = base.estimateSize();
        return baseSize == 0 ? 0
                : (long) Math.ceil(baseSize / (double) batchSize);
    }

    @Override
    public int characteristics() {
        return base.characteristics();
    }

}

gerçekten yardımcı. Birisi bazı özel kriterlere göre toplu işlem yapmak isterse (örneğin, bayt cinsinden koleksiyon boyutu), o zaman özel tahmininizi devredebilir ve bunu bir koşul olarak for-döngüde kullanabilirsiniz (bu durumda döngü daha okunaklı olacaktır)
lütfen

Uygulamanın doğru olduğundan emin değilim. Örneğin, temel akış, SUBSIZEDdöndürülen bölünmeler ise , bölünmeden öncekine trySplitgöre daha fazla öğe içerebilir (bölünme, partinin ortasında gerçekleşirse).
Malt

@Malt eğer anlayışım Spliteratorsdoğruysa, o trySplitzaman her zaman verileri kabaca eşit iki parçaya bölmeli, böylece sonuç asla orijinalden daha büyük olmamalıdır?
Bruce Hamilton

@BruceHamilton Maalesef, belgelere göre parçalar kabaca eşit olamaz . Onlar gerekir eşit olması:if this Spliterator is SUBSIZED, then estimateSize() for this spliterator before splitting must be equal to the sum of estimateSize() for this and the returned Spliterator after splitting.
Malt

Evet, bu benim Spliterator bölme anlayışımla tutarlı. Bununla birlikte, "trySplit'ten dönen bölünmelerin, bölünmeden öncekinden daha fazla öğeye sahip olabileceğini" anlamakta zorlanıyorum, orada ne demek istediğinizi açıklayabilir misiniz?
Bruce Hamilton

14

Çözmemiz gereken benzer bir sorun vardı. Sistem belleğinden daha büyük bir akış almak (bir veritabanındaki tüm nesneler boyunca yinelenerek) ve sıralamayı mümkün olan en iyi şekilde rastgele hale getirmek istedik - 10.000 öğeyi arabelleğe almanın ve rastgele hale getirmenin uygun olacağını düşündük.

Hedef, bir akışı ele alan bir işlevdi.

Burada önerilen çözümlerden bir dizi seçenek var gibi görünüyor:

  • Java 8 olmayan çeşitli ek kitaplıkları kullanın
  • Akış olmayan bir şeyle başlayın - örneğin rastgele erişim listesi
  • Bir ayırıcıda kolayca bölünebilen bir akışa sahip olun

İçgüdülerimiz aslında özel bir toplayıcı kullanmaktı, ancak bu akıştan çıkmak anlamına geliyordu. Yukarıdaki özel toplayıcı çözümü çok iyi ve neredeyse kullandık.

İşte size, akışların desteklemediği fazladan bir şey yapmanıza izin vermek için bir kaçış kapısı olarak kullanabileceğiniz Streambir gerçeği kullanarak hile yapan bir çözüm . Java 8 başka bit kullanarak bir akışa dönüştürülmüş geri büyücülük.IteratorIteratorStreamSupport

/**
 * An iterator which returns batches of items taken from another iterator
 */
public class BatchingIterator<T> implements Iterator<List<T>> {
    /**
     * Given a stream, convert it to a stream of batches no greater than the
     * batchSize.
     * @param originalStream to convert
     * @param batchSize maximum size of a batch
     * @param <T> type of items in the stream
     * @return a stream of batches taken sequentially from the original stream
     */
    public static <T> Stream<List<T>> batchedStreamOf(Stream<T> originalStream, int batchSize) {
        return asStream(new BatchingIterator<>(originalStream.iterator(), batchSize));
    }

    private static <T> Stream<T> asStream(Iterator<T> iterator) {
        return StreamSupport.stream(
            Spliterators.spliteratorUnknownSize(iterator,ORDERED),
            false);
    }

    private int batchSize;
    private List<T> currentBatch;
    private Iterator<T> sourceIterator;

    public BatchingIterator(Iterator<T> sourceIterator, int batchSize) {
        this.batchSize = batchSize;
        this.sourceIterator = sourceIterator;
    }

    @Override
    public boolean hasNext() {
        prepareNextBatch();
        return currentBatch!=null && !currentBatch.isEmpty();
    }

    @Override
    public List<T> next() {
        return currentBatch;
    }

    private void prepareNextBatch() {
        currentBatch = new ArrayList<>(batchSize);
        while (sourceIterator.hasNext() && currentBatch.size() < batchSize) {
            currentBatch.add(sourceIterator.next());
        }
    }
}

Bunu kullanmanın basit bir örneği şuna benzer:

@Test
public void getsBatches() {
    BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
        .forEach(System.out::println);
}

Yukarıdaki baskılar

[A, B, C]
[D, E, F]

Kullanım durumumuz için, grupları karıştırmak ve ardından bunları bir akış olarak tutmak istedik - şuna benziyordu:

@Test
public void howScramblingCouldBeDone() {
    BatchingIterator.batchedStreamOf(Stream.of("A","B","C","D","E","F"), 3)
        // the lambda in the map expression sucks a bit because Collections.shuffle acts on the list, rather than returning a shuffled one
        .map(list -> {
            Collections.shuffle(list); return list; })
        .flatMap(List::stream)
        .forEach(System.out::println);
}

Bu, şöyle bir şey çıkarır (rastgele hale getirilmiştir, her seferinde çok farklıdır)

A
C
B
E
D
F

Buradaki gizli sos, her zaman bir akarsu olması, böylece ya bir parti akışı üzerinde çalışabilir ya da her partiye bir şeyler yapıp ardından flatMapbir akışa geri dönebilirsiniz. Daha da iyisi, yukarıda sadece tüm nihai olarak çalışır forEachveya collectdiğer sonlandırma ifadeler PULL'U dere yoluyla veri.

Bu , bir akış üzerinde iteratorözel bir sonlandırma işlemi türü olduğu ve tüm akışın çalışıp belleğe girmesine neden olmadığı ortaya çıktı! Harika bir tasarım için Java 8 çalışanlarına teşekkürler!


Ve toplandığında her partiyi tamamen yinelemeniz ve devam ettirmeniz çok iyi List- parti içi öğelerin yinelemesini erteleyemezsiniz çünkü tüketici tüm bir partiyi atlamak isteyebilir ve eğer tüketmediyseniz öğeleri o zaman çok uzağa atlamazlar. (Bunlardan birini C #'da uyguladım, ancak çok daha kolaydı.)
ErikE

10

Ayrıca RxJava'yı da kullanabilirsiniz :

Observable.from(data).buffer(BATCH_SIZE).forEach((batch) -> process(batch));

veya

Observable.from(lazyFileStream).buffer(500).map((batch) -> process(batch)).toList();

veya

Observable.from(lazyFileStream).buffer(500).map(MyClass::process).toList();

8

Tepegözlere de bir göz atabilirsiniz. , bu kütüphanenin yazarıyım. JOOλ arayüzünü (ve JDK 8 Streams uzantısıyla) uygular, ancak JDK 8 Paralel Akışlardan farklı olarak, Eşzamansız işlemlere odaklanır (Eşzamansız G / Ç çağrılarını potansiyel olarak engelleme gibi). JDK Paralel Akışlar, aksine CPU bağlantılı işlemler için veri paralelliğine odaklanır. Başlık altında Geleceğe dayalı görevlerin toplamlarını yöneterek çalışır, ancak son kullanıcılara standart bir genişletilmiş Akış API'si sunar.

Bu örnek kod, başlamanıza yardımcı olabilir

LazyFutureStream.parallelCommonBuilder()
                .react(data)
                .grouped(BATCH_SIZE)                  
                .map(this::process)
                .run();

Burada gruplama ile ilgili bir eğitim var

Ve burada daha genel bir Eğitim

Kendi İş Parçacığı Havuzunuzu kullanmak için (ki bu muhtemelen G / Ç'yi engellemek için daha uygundur), ile işlemeye başlayabilirsiniz.

     LazyReact reactor = new LazyReact(40);

     reactor.react(data)
            .grouped(BATCH_SIZE)                  
            .map(this::process)
            .run();

3

Paralel akışlarla da çalışan saf Java 8 örneği.

Nasıl kullanılır:

Stream<Integer> integerStream = IntStream.range(0, 45).parallel().boxed();
CsStreamUtil.processInBatch(integerStream, 10, batch -> System.out.println("Batch: " + batch));

Yöntem bildirimi ve uygulaması:

public static <ElementType> void processInBatch(Stream<ElementType> stream, int batchSize, Consumer<Collection<ElementType>> batchProcessor)
{
    List<ElementType> newBatch = new ArrayList<>(batchSize);

    stream.forEach(element -> {
        List<ElementType> fullBatch;

        synchronized (newBatch)
        {
            if (newBatch.size() < batchSize)
            {
                newBatch.add(element);
                return;
            }
            else
            {
                fullBatch = new ArrayList<>(newBatch);
                newBatch.clear();
                newBatch.add(element);
            }
        }

        batchProcessor.accept(fullBatch);
    });

    if (newBatch.size() > 0)
        batchProcessor.accept(new ArrayList<>(newBatch));
}


1

Spliterator kullanarak basit örnek

    // read file into stream, try-with-resources
    try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
        //skip header
        Spliterator<String> split = stream.skip(1).spliterator();
        Chunker<String> chunker = new Chunker<String>();
        while(true) {              
            boolean more = split.tryAdvance(chunker::doSomething);
            if (!more) {
                break;
            }
        }           
    } catch (IOException e) {
        e.printStackTrace();
    }
}

static class Chunker<T> {
    int ct = 0;
    public void doSomething(T line) {
        System.out.println(ct++ + " " + line.toString());
        if (ct % 100 == 0) {
            System.out.println("====================chunk=====================");               
        }           
    }       
}

Bruce'un cevabı daha kapsamlı, ancak bir grup dosyayı işlemek için hızlı ve kirli bir şey arıyordum.


1

bu, tembelce değerlendirilen saf bir java çözümüdür.

public static <T> Stream<List<T>> partition(Stream<T> stream, int batchSize){
    List<List<T>> currentBatch = new ArrayList<List<T>>(); //just to make it mutable 
    currentBatch.add(new ArrayList<T>(batchSize));
    return Stream.concat(stream
      .sequential()                   
      .map(new Function<T, List<T>>(){
          public List<T> apply(T t){
              currentBatch.get(0).add(t);
              return currentBatch.get(0).size() == batchSize ? currentBatch.set(0,new ArrayList<>(batchSize)): null;
            }
      }), Stream.generate(()->currentBatch.get(0).isEmpty()?null:currentBatch.get(0))
                .limit(1)
    ).filter(Objects::nonNull);
}

1

Apache.commons'u kullanabilirsiniz:

ListUtils.partition(ListOfLines, 500).stream()
                .map(partition -> processBatch(partition)
                .collect(Collectors.toList());

Bölümleme bölümü tembel olmayan bir şekilde yapılır, ancak liste bölümlendirildikten sonra akışlarla çalışmanın avantajlarını elde edersiniz (örneğin, paralel akışlar kullanın, filtreler ekleyin, vb.). Diğer cevaplar daha ayrıntılı çözümler önermektedir ancak bazen okunabilirlik ve sürdürülebilirlik daha önemlidir (ve bazen değildir :-))


Kimin olumsuz oy verdiğinden emin değilim ama nedenini anlamak güzel olurdu .. Guava'yı kullanamayan insanlar için diğer cevapları tamamlayan bir cevap verdim
Tal Joffe

Burada bir liste işliyorsunuz, akış değil.
Drakemor

@Drakemor Bir alt liste akışı işliyorum. stream () işlev çağrısına dikkat edin
Tal Joffe

Ama önce onu bir alt liste listesine dönüştürürsünüz, bu gerçek akışlı veriler için doğru şekilde çalışmaz . İşte bölüm referansı: commons.apache.org/proper/commons-collections/apidocs/org/…
Drakemor

1
TBH İddianızı tam olarak anlamadım ama sanırım katılmayabiliriz. Cevabımı buradaki konuşmamızı yansıtacak şekilde düzenledim. Tartışma için teşekkürler
Tal Joffe

1

Reactor kullanılarak kolaylıkla yapılabilir :

Flux.fromStream(fileReader.lines().onClose(() -> safeClose(fileReader)))
            .map(line -> someProcessingOfSingleLine(line))
            .buffer(BUFFER_SIZE)
            .subscribe(apiService::makeHttpRequest);

0

İle Java 8ve com.google.common.collect.Lists, bir şey gibi yapabilirsiniz:

public class BatchProcessingUtil {
    public static <T,U> List<U> process(List<T> data, int batchSize, Function<List<T>, List<U>> processFunction) {
        List<List<T>> batches = Lists.partition(data, batchSize);
        return batches.stream()
                .map(processFunction) // Send each batch to the process function
                .flatMap(Collection::stream) // flat results to gather them in 1 stream
                .collect(Collectors.toList());
    }
}

Burada T, giriş listesindeki öğelerin türü veU çıkış listesindeki öğelerin türü

Ve bunu şu şekilde kullanabilirsiniz:

List<String> userKeys = [... list of user keys]
List<Users> users = BatchProcessingUtil.process(
    userKeys,
    10, // Batch Size
    partialKeys -> service.getUsers(partialKeys)
);
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.