web-dev-qa-db-fra.com

Java 8 Stream avec traitement par lots

J'ai un gros fichier qui contient une liste d'éléments.

Je souhaite créer un lot d'éléments, faire une requête HTTP avec ce lot (tous les éléments sont nécessaires en tant que paramètres de la requête HTTP). Je peux le faire très facilement avec une boucle for, mais en tant qu'amoureux de Java 8, je veux essayer d'écrire cela avec le framework Stream de Java 8 (et profiter des avantages d'un traitement paresseux).

Exemple:

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);

Je veux faire quelque chose d'un long la ligne de lazyFileStream.group(500).map(processBatch).collect(toList())

Quelle serait la meilleure façon de faire cela?

70
Andy Dang

Vous pouvez le faire avec jOOλ , une bibliothèque qui étend les flux Java 8 à des cas d'utilisation de flux séquentiels mono-threadés:

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

Dans les coulisses, zipWithIndex() est simplement:

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());
}

... alors que groupBy() est une commodité d'API pour:

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

(Avertissement: je travaille pour l'entreprise derrière jOOλ)

14
Lukas Eder

Pour être complet, voici une solution Guava .

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

Dans la question, la collection est disponible, de sorte qu'un flux n'est pas nécessaire et qu'il peut être écrit ainsi:

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

L'implémentation pure Java-8 est également possible:

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));

Notez que contrairement à JOOL, il peut très bien fonctionner en parallèle (à condition que votre data soit une liste à accès aléatoire).

40
Tagir Valeev

Solution Pure Java 8:

Nous pouvons créer un collecteur personnalisé pour le faire avec élégance, qui prend un batch size et un Consumer pour traiter chaque lot:

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();
    }
}

Vous pouvez éventuellement créer une classe d’utilité auxiliaire:

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);
    }
}

Exemple d'utilisation:

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));

J'ai aussi posté mon code sur GitHub, si quelqu'un veut jeter un coup d'œil:

Lien vers Github

26
rohitvats

J'ai écrit un Spliterator personnalisé pour des scénarios comme celui-ci. Il remplira des listes d'une taille donnée à partir du flux d'entrée. L’avantage de cette approche est qu’elle effectuera un traitement paresseux et fonctionnera avec d’autres fonctions de flux.

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();
    }

}
9
Bruce Hamilton

Vous pouvez également utiliser RxJava :

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

ou

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

ou

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

Vous pouvez également consulter cyclops-react , je suis l’auteur de cette bibliothèque. Il implémente l'interface jOOλ (et par extension, JDK 8 Streams), mais contrairement aux flux parallèles JDK 8, il se concentre sur les opérations asynchrones (telles que le blocage potentiel d'appels d'E/S Async). JDK Parallel Streams, par contraste, se concentre sur le parallélisme des données pour les opérations liées au processeur. Cela fonctionne en gérant des agrégats de tâches basées sur Future sous le capot, mais présente une API Stream standard étendue aux utilisateurs finaux.

Cet exemple de code peut vous aider à démarrer

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

Il y a un tutoriel sur la mise en lots ici

Et un Tutoriel plus général ici

Pour utiliser votre propre pool de threads (ce qui est probablement plus approprié pour bloquer les E/S), vous pouvez commencer le traitement avec

     LazyReact reactor = new LazyReact(40);

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

Nous avons eu un problème similaire à résoudre. Nous voulions prendre un flux plus volumineux que la mémoire système (en parcourant tous les objets d’une base de données) et en randomisant l’ordre le mieux possible. Nous pensions qu’il serait acceptable de mettre en mémoire tampon 10 000 éléments et de les randomiser.

La cible était une fonction qui prenait un flux.

Parmi les solutions proposées ici, il semble y avoir une gamme d'options:

  • Utiliser diverses bibliothèques supplémentaires non-Java 8
  • Commencez avec quelque chose qui n'est pas un flux - par exemple une liste d'accès aléatoire
  • Avoir un flux qui peut être facilement divisé en un séparateur

À l’origine, notre instinct était d’utiliser un collecteur personnalisé, mais cela signifiait abandonner le streaming. La solution de collecte personnalisée ci-dessus est très bonne et nous l’avons presque utilisée.

Voici une solution qui triche en utilisant le fait que Streams peut vous donner une Iterator que vous pouvez utiliser comme une trappe de secours pour vous permettre de faire quelque chose de plus que les flux ne supportent pas. La Iterator est reconvertie en un flux en utilisant un autre fragment de Java 8 StreamSupport sorcery.

/**
 * 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());
        }
    }
}

Voici un exemple simple d'utilisation:

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

Les impressions ci-dessus

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

Pour notre cas d'utilisation, nous voulions mélanger les lots, puis les conserver sous forme de flux - cela ressemblait à ceci:

@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);
}

Cela produit quelque chose comme (c'est aléatoire, si différent à chaque fois)

A
C
B
E
D
F

Le secret réside ici dans le fait qu’il existe toujours un flux, vous pouvez donc opérer sur un flux de lots ou modifier un lot, puis flatMap le redistribuer dans un flux. Mieux encore, tous les éléments ci-dessus ne fonctionnent que sous la forme finale forEach ou collect ou d'autres expressions finalesPULLles données du flux.

Il s’avère que iterator est un type spécial de opération de terminaison sur un flux et ne provoque pas l’exécution et la mémorisation de tout le flux! Merci aux gars de Java 8 pour un design brillant!

5
Ashley Frieze

Exemple pur Java 8 fonctionnant également avec des flux parallèles.

Comment utiliser:

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

La déclaration de méthode et l'implémentation:

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

Avec Java 8 et com.google.common.collect.Lists, vous pouvez faire quelque chose comme:

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());
    }
}

Ici T est le type des éléments de la liste d'entrée et U le type des éléments de la liste de sortie

Et vous pouvez l'utiliser comme ça:

List<String> userKeys = [... list of user keys]
List<Users> users = BatchProcessingUtil.process(
    userKeys,
    10, // Batch Size
    partialKeys -> service.getUsers(partialKeys)
);
0
josebui

Exemple simple d'utilisation de Spliterator

    // 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=====================");               
        }           
    }       
}

La réponse de Bruce est plus complète, mais je cherchais quelque chose de rapide et sale pour traiter un tas de fichiers.

0
rhinmass

Vous pouvez utiliser Apache.commons:

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

c'est une solution Java pure qui est évaluée paresseusement.

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);
}
0
Hei