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?
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λ)
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);
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).
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:
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();
}
}
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();
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();
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:
À 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 Stream
s 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!
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));
}
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)
);
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.
Vous pouvez utiliser Apache.commons:
ListUtils.partition(ListOfLines, 500).stream()
.map(partition -> processBatch(partition)
.collect(Collectors.toList());
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);
}