Reputation: 157
I have a list and a batch size. I need to split it into a list of lists where each list has a maximum size of batch size preferably using java8 Stream API.
One important condition is, if I have an empty list []
, I need to get [[]]
.
I am doing this
final AtomicInteger counter = new AtomicInteger();
List<List<Integer>> result = listToSplit.stream()
.collect(Collectors.groupingBy(it -> counter.getAndIncrement() / batchSize))
.values();
But for empty list, it gives [[null]]
Upvotes: 0
Views: 739
Reputation: 6686
If you're open to using a third-party library, you can use Collectors2.chunk()
from Eclipse Collections with a Java Stream.
@Test
public void chunk()
{
List<Integer> listToSplit = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<MutableList<Integer>> chunks = listToSplit.isEmpty() ?
Lists.mutable.with(Lists.mutable.empty()) :
listToSplit.stream().collect(Collectors2.chunk(3));
List<List<Integer>> expected = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9),
Arrays.asList(10));
Assert.assertEquals(expected, chunks);
}
Note: I am a committer for Eclipse Collections.
Upvotes: 0
Reputation: 44398
Few notes first:
The hack using AtomicInteger
or int[0]
is familiar to me since I have already asked 2 questions regarding it years ago when I started using Streams:
As I got more familiar with the core principles of Stream API I realized they should be treated a bit differently than a typical for-loops although you still use IntStream
that might be close to a procedural for-loop
construct. Don't violate the Side-effect principle using Stream API.
One more thing to remind before we get to a solution is that Collectors.groupingBy
is a collector resulting in a Map
and the subsequent call of Map.values()
returns Collection<List<Integer>>
instead of List<List<Integer>>
.
A final note is a reminder, that the solution for an empty input list is not compatible with the behavior with a list that can be perfectly chunked (a list of 15 elements split to lists by 5 elements). So, if an empty array results in [[]]
, then the result [[1,2,3],[4,5,6],[]]
of a list 1,2,3,4,5,6
with batchSize=3
is not acceptable but likely generated with a simple non-branched approach.
With regard to what was said above, you might conclude to this solution, that is not elegant though:
int max = (int) Math.ceil((double) listToSplit.size() / batchSize); // number of chunks
List<List<Integer>> result;
if (listToSplit.isEmpty()) {
result = Arrays.asList(Collections.emptyList()); // results in [[]] if empty
} else {
result = IntStream.range(0, max)
.map(i -> i * batchSize) // initial indices
.mapToObj(i -> listToSplit.subList( // find a sublist
i, Math.min(i + batchSize, listToSplit.size()))) // .. from i*b to (i+1)*b
.collect(Collectors.toList()); // as List<List<Integer>>
}
The conclusion is to stick with the old but gold for-loop
for such use-case. :)
Upvotes: 2
Reputation: 4045
Used IntStream
and list.subList
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class ListOfList {
public static void main(String[] args) {
List<Integer> listToSplit = new ArrayList<>();
// listToSplit.add(1);
// listToSplit.add(1);
// listToSplit.add(1);
// listToSplit.add(1);
double batchSize = 3;
int loops = Math.max(1,(int) Math.ceil(listToSplit.size()/batchSize));
final List<List<Integer>> collect = IntStream.iterate(-1, i -> (int) (i + batchSize)).limit(loops)
.mapToObj(id -> {
int actualCounter = id+1;
int finalLength = (int) Math.min(listToSplit.size(),actualCounter + batchSize);
return listToSplit.subList(actualCounter, finalLength);
}).collect(Collectors.toList());
System.out.println(collect);
}
}
Upvotes: 0