Comment implémenter l'opération "partition" sur Java 8 Stream? Par partition, divisez un flux en sous-flux d'une taille donnée. D'une manière ou d'une autre, il sera identique à Gava Itérateurs .partition () méthode, il est simplement souhaitable que les partitions soient des Streams évalués paresseux plutôt que des List.
Il est impossible de partitionner le flux source arbitraire en lots de taille fixe, car cela gâcherait le traitement en parallèle. Lors du traitement en parallèle, il est possible que vous ne sachiez pas combien d'éléments dans la première sous-tâche après la scission; vous ne pouvez donc pas créer les partitions pour la sous-tâche suivante tant que la première n'est pas entièrement traitée.
Cependant, il est possible de créer le flux de partitions à partir de l'accès aléatoire List
. Cette fonctionnalité est disponible, par exemple, dans ma bibliothèque StreamEx
:
List<Type> input = Arrays.asList(...);
Stream<List<Type>> stream = StreamEx.ofSubLists(input, partitionSize);
Ou si vous voulez vraiment le flux de flux:
Stream<Stream<Type>> stream = StreamEx.ofSubLists(input, partitionSize).map(List::stream);
Si vous ne voulez pas dépendre de bibliothèques tierces, vous pouvez implémenter manuellement cette méthode ofSubLists
:
public static <T> Stream<List<T>> ofSubLists(List<T> source, int length) {
if (length <= 0)
throw new IllegalArgumentException("length = " + length);
int size = source.size();
if (size <= 0)
return Stream.empty();
int fullChunks = (size - 1) / length;
return IntStream.range(0, fullChunks + 1).mapToObj(
n -> source.subList(n * length, n == fullChunks ? size : (n + 1) * length));
}
Cette implémentation semble un peu longue, mais elle prend en compte des cas tels que la taille de liste proche de-MAX_VALUE.
Si vous souhaitez une solution conviviale pour les flux non ordonnés en parallèle (vous ne vous souciez donc pas des éléments de flux qui seront combinés en un seul lot), vous pouvez utiliser le collecteur comme ceci (grâce à @sibnick pour son inspiration):
public static <T, A, R> Collector<T, ?, R> unorderedBatches(int batchSize,
Collector<List<T>, A, R> downstream) {
class Acc {
List<T> cur = new ArrayList<>();
A acc = downstream.supplier().get();
}
BiConsumer<Acc, T> accumulator = (acc, t) -> {
acc.cur.add(t);
if(acc.cur.size() == batchSize) {
downstream.accumulator().accept(acc.acc, acc.cur);
acc.cur = new ArrayList<>();
}
};
return Collector.of(Acc::new, accumulator,
(acc1, acc2) -> {
acc1.acc = downstream.combiner().apply(acc1.acc, acc2.acc);
for(T t : acc2.cur) accumulator.accept(acc1, t);
return acc1;
}, acc -> {
if(!acc.cur.isEmpty())
downstream.accumulator().accept(acc.acc, acc.cur);
return downstream.finisher().apply(acc.acc);
}, Collector.Characteristics.UNORDERED);
}
Exemple d'utilisation:
List<List<Integer>> list = IntStream.range(0,20)
.boxed().parallel()
.collect(unorderedBatches(3, Collectors.toList()));
Résultat:
[[2, 3, 4], [7, 8, 9], [0, 1, 5], [12, 13, 14], [17, 18, 19], [10, 11, 15], [6, 16]]
Ce collecteur est parfaitement thread-safe et produit des lots ordonnés pour un flux séquentiel.
Si vous souhaitez appliquer une transformation intermédiaire à chaque lot, vous pouvez utiliser la version suivante:
public static <T, AA, A, B, R> Collector<T, ?, R> unorderedBatches(int batchSize,
Collector<T, AA, B> batchCollector,
Collector<B, A, R> downstream) {
return unorderedBatches(batchSize,
Collectors.mapping(list -> list.stream().collect(batchCollector), downstream));
}
Par exemple, de cette façon, vous pouvez additionner les nombres de chaque lot à la volée:
List<Integer> list = IntStream.range(0,20)
.boxed().parallel()
.collect(unorderedBatches(3, Collectors.summingInt(Integer::intValue),
Collectors.toList()));
Si vous voulez utiliser le Stream de manière séquentielle, il est possible de partitionner un Stream (ainsi que d’exécuter des fonctions connexes telles que le fenêtrage - ce qui, à mon avis, est ce que vous voulez vraiment dans ce cas). Deux bibliothèques qui prendront en charge le partitionnement pour les flux standard sont cyclops-react (je suis l'auteur) et jOOλ qui cyclops-react s'étend (pour ajouter des fonctionnalités telles que Windowing).
cyclops-streams a une collection de fonctions statiques StreamUtils pour opérer sur Java Streams), ainsi qu'une série de fonctions telles que splitAt, headAndTail, splitBy, partition pour le partitionnement.
Pour insérer un flux dans un flux de flux imbriqués de taille 30, vous pouvez utiliser la méthode window.
Pour ce qui est des PO, en termes de flux, le fractionnement d’un flux en plusieurs flux d’une taille donnée est une opération de fenêtrage (plutôt qu’une opération de partitionnement).
Stream<Streamable<Integer>> streamOfStreams = StreamUtils.window(stream,30);
Il existe une classe d'extension Stream appelée ReactiveSeq qui étend jool.Seq et ajoute une fonctionnalité de fenêtrage qui peut rendre le code un peu plus propre.
ReactiveSeq<Integer> seq;
ReactiveSeq<ListX<Integer>> streamOfLists = seq.grouped(30);
Comme le souligne Tagir ci-dessus, cela ne convient pas aux flux parallèles. Si vous souhaitez créer une fenêtre ou un lot pour un flux que vous souhaitez exécuter de manière multithread. LazyFutureStream in cyclops-react peut être utile (Windowing est sur la liste des tâches à effectuer, mais les anciens traitements par lots sont tout à fait disponibles maintenant).
Dans ce cas, les données de plusieurs threads exécutant le flux seront transmises à une file d'attente sans plusieurs producteurs/consommateurs uniques et les données séquentielles de cette file d'attente pourront être visualisées avant d'être à nouveau distribuées aux threads.
Stream<List<Data>> batched = new LazyReact().range(0,1000)
.grouped(30)
.map(this::process);
Comme Jon Skeet l'a montré dans son commentaire , il semble impossible de rendre les partitions paresseuses. Pour les partitions non paresseuses, j'ai déjà ce code:
public static <T> Stream<Stream<T>> partition(Stream<T> source, int size) {
final Iterator<T> it = source.iterator();
final Iterator<Stream<T>> partIt = Iterators.transform(Iterators.partition(it, size), List::stream);
final Iterable<Stream<T>> iterable = () -> partIt;
return StreamSupport.stream(iterable.spliterator(), false);
}
J'ai trouvé une solution élégante: Iterable parts = Iterables.partition(stream::iterator, size)
La solution la plus élégante et pure Java 8 pour ce problème que j'ai trouvée):
public static <T> List<List<T>> partition(final List<T> list, int batchSize) {
return IntStream.range(0, getNumberOfPartitions(list, batchSize))
.mapToObj(i -> list.subList(i * batchSize, Math.min((i + 1) * batchSize, list.size())))
.collect(toList());
}
//https://stackoverflow.com/questions/23246983/get-the-next-higher-integer-value-in-Java
private static <T> int getNumberOfPartitions(List<T> list, int batchSize) {
return (list.size() + batchSize- 1) / batchSize;
}
Il s’agit d’une solution pure Java) évaluée paresseusement au lieu d’utiliser List.
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);
}
La méthode retourne Stream<List<T>>
Pour plus de flexibilité. Vous pouvez le convertir facilement en Stream<Stream<T>>
Par partition(something, 10).map(List::stream)
.
Je pense que c'est possible avec une sorte de bidouille à l'intérieur:
créer une classe d'utilitaire pour batch:
public static class ConcurrentBatch {
private AtomicLong id = new AtomicLong();
private int batchSize;
public ConcurrentBatch(int batchSize) {
this.batchSize = batchSize;
}
public long next() {
return (id.getAndIncrement()) / batchSize;
}
public int getBatchSize() {
return batchSize;
}
}
et méthode:
public static <T> void applyConcurrentBatchToStream(Consumer<List<T>> batchFunc, Stream<T> stream, int batchSize){
ConcurrentBatch batch = new ConcurrentBatch(batchSize);
//hack Java map: extends and override computeIfAbsent
Supplier<ConcurrentMap<Long, List<T>>> mapFactory = () -> new ConcurrentHashMap<Long, List<T>>() {
@Override
public List<T> computeIfAbsent(Long key, Function<? super Long, ? extends List<T>> mappingFunction) {
List<T> rs = super.computeIfAbsent(key, mappingFunction);
//apply batchFunc to old lists, when new batch list is created
if(rs.isEmpty()){
for(Entry<Long, List<T>> e : entrySet()) {
List<T> batchList = e.getValue();
//todo: need to improve
synchronized (batchList) {
if (batchList.size() == batch.getBatchSize()){
batchFunc.accept(batchList);
remove(e.getKey());
batchList.clear();
}
}
}
}
return rs;
}
};
stream.map(s -> new AbstractMap.SimpleEntry<>(batch.next(), s))
.collect(groupingByConcurrent(AbstractMap.SimpleEntry::getKey, mapFactory, mapping(AbstractMap.SimpleEntry::getValue, toList())))
.entrySet()
.stream()
//map contains only unprocessed lists (size<batchSize)
.forEach(e -> batchFunc.accept(e.getValue()));
}
Voici une solution rapide par AbacusUtil
IntStream.range(0, Integer.MAX_VALUE).split(size).forEach(s -> N.println(s.toArray()));
Disclaimer : Je suis le développeur de AbacusUtil.