vamosrafa
vamosrafa

Reputation: 695

Split list into multiple lists with fixed number of elements in Java 8

I want to something which is similar to the Scala grouped function. Basically, pick 2 elements at a time and process them. Here is a reference for the same:

Split list into multiple lists with fixed number of elements

Lambdas do provide things like groupingBy and partitioningBy but none of them seem to do the same as the grouped function in Scala. Any pointers would be appreciated.

Upvotes: 13

Views: 20826

Answers (7)

M. Justin
M. Justin

Reputation: 21239

The JEP 461: Stream Gatherers Java 22 preview language feature adds built-in support for partitioning a stream into lists of a given size.

// Processes each of the following sub-lists: [[0, 1], [2, 3], [4, 5], [6]]
Stream<List<Integer>> stream = List.of(0, 1, 2, 3, 4, 5, 6).stream()
        .gather(Gatherers.windowFixed(2))
        .forEach((List<Integer> window) -> {
            // Do stuff with the window
        });

This uses the new Stream.gather method with the new built-in Gatherers.windowFixed gatherer to convert the initial Stream<T> to a Stream<List<T>>.

Javadocs

Gatherer:

An intermediate operation that transforms a stream of input elements into a stream of output elements, optionally applying a final action when the end of the upstream is reached. […]

[…]

There are many examples of gathering operations, including but not limited to: grouping elements into batches (windowing functions); de-duplicating consecutively similar elements; incremental accumulation functions (prefix scan); incremental reordering functions, etc. The class Gatherers provides implementations of common gathering operations.

Stream.gather:

Returns a stream consisting of the results of applying the given gatherer to the elements of this stream.

Gatherers.windowFixed

Returns a Gatherer that gathers elements into windows -- encounter-ordered groups of elements -- of a fixed size. If the stream is empty then no window will be produced. The last window may contain fewer elements than the supplied window size.

Example:

// will contain: [[1, 2, 3], [4, 5, 6], [7, 8]]
List<List<Integer>> windows =
    Stream.of(1,2,3,4,5,6,7,8).gather(Gatherers.windowFixed(3)).toList();

Upvotes: 0

ndr_brt
ndr_brt

Reputation: 121

A simple version with java 8 streams api:

static <T> List<List<T>> partition(List<T> list, Integer partitionSize) {
    int numberOfLists = BigDecimal.valueOf(list.size())
        .divide(BigDecimal.valueOf(partitionSize), 0, CEILING)
        .intValue();

    return IntStream.range(0, numberOfLists)
        .mapToObj(it -> list.subList(it * partitionSize, Math.min((it+1) * partitionSize, list.size())))
        .collect(Collectors.toList());
}

Upvotes: 0

haki
haki

Reputation: 401

You can use Guava library.

List<Integer> bigList = ... List<List<Integer>> smallerLists = Lists.partition(bigList, 10);

Upvotes: 27

Holger
Holger

Reputation: 298379

It sounds like a problem that is better handled like a low-level Stream operation just like the ops provided by the Stream API itself. A (relative) simple solution may look like:

public static <T> Stream<List<T>> chunked(Stream<T> s, int chunkSize) {
    if(chunkSize<1) throw new IllegalArgumentException("chunkSize=="+chunkSize);
    if(chunkSize==1) return s.map(Collections::singletonList);
    Spliterator<T> src=s.spliterator();
    long size=src.estimateSize();
    if(size!=Long.MAX_VALUE) size=(size+chunkSize-1)/chunkSize;
    int ch=src.characteristics();
    ch&=Spliterator.SIZED|Spliterator.ORDERED|Spliterator.DISTINCT|Spliterator.IMMUTABLE;
    ch|=Spliterator.NONNULL;
    return StreamSupport.stream(new Spliterators.AbstractSpliterator<List<T>>(size, ch)
    {
        private List<T> current;
        @Override
        public boolean tryAdvance(Consumer<? super List<T>> action) {
            if(current==null) current=new ArrayList<>(chunkSize);
            while(current.size()<chunkSize && src.tryAdvance(current::add));
            if(!current.isEmpty()) {
                action.accept(current);
                current=null;
                return true;
            }
            return false;
        }
    }, s.isParallel());
}

Simple test:

chunked(Stream.of(1, 2, 3, 4, 5, 6, 7), 3)
  .parallel().forEachOrdered(System.out::println);

The advantage is that you do not need a full collection of all items for subsequent stream processing, e.g.

chunked(
    IntStream.range(0, 1000).mapToObj(i -> {
        System.out.println("processing item "+i);
        return i;
    }), 2).anyMatch(list->list.toString().equals("[6, 7]")));

will print:

processing item 0
processing item 1
processing item 2
processing item 3
processing item 4
processing item 5
processing item 6
processing item 7
true

rather than processing a thousand items of IntStream.range(0, 1000). This also enables using infinite source Streams:

chunked(Stream.iterate(0, i->i+1), 2).anyMatch(list->list.toString().equals("[6, 7]")));

If you are interested in a fully materialized collection rather than applying subsequent Stream operations, you may simply use the following operation:

List<Integer> list=Arrays.asList(1, 2, 3, 4, 5, 6, 7);
int listSize=list.size(), chunkSize=2;
List<List<Integer>> list2=
    IntStream.range(0, (listSize-1)/chunkSize+1)
             .mapToObj(i->list.subList(i*=chunkSize,
                                       listSize-chunkSize>=i? i+chunkSize: listSize))
             .collect(Collectors.toList());

Upvotes: 18

mariatsji
mariatsji

Reputation: 81

A recursive solution to transform the list to a list-of-lists would also be possible

int chunkSize = 2;

private <T> List<List<T>> process(List<T> list) {
    if (list.size() > chunkSize) {
        List<T> chunk = list.subList(0, chunkSize);
        List<T> rest = list.subList(chunkSize, list.size());
        List<List<T>> lists = process(rest);
        return concat(chunk, lists);
    } else {
        ArrayList<List<T>> retVal = new ArrayList<>();
        retVal.add(list);
        return retVal;
    }
}

private <T> List<List<T>> concat(List<T> chunk, List<List<T>> rest) {
    rest.add(0, chunk);
    return rest;
}

Upvotes: 1

babanin
babanin

Reputation: 3584

You can create your own collector. Something like this:

class GroupingCollector<T> implements Collector<T, List<List<T>>, List<List<T>>> {
    private final int elementCountInGroup;

    public GroupingCollector(int elementCountInGroup) {
        this.elementCountInGroup = elementCountInGroup;
    }

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

    @Override
    public BiConsumer<List<List<T>>, T> accumulator() {
        return (lists, integer) -> {
            if (!lists.isEmpty()) {
                List<T> integers = lists.get(lists.size() - 1);
                if (integers.size() < elementCountInGroup) {
                    integers.add(integer);
                    return;
                }
            }

            List<T> list = new ArrayList<>();
            list.add(integer);
            lists.add(list);
        };
    }

    @Override
    public BinaryOperator<List<List<T>>> combiner() {
        return (lists, lists2) -> {
            List<List<T>> r = new ArrayList<>();
            r.addAll(lists);
            r.addAll(lists2);
            return r;
        };
    }

    @Override
    public Function<List<List<T>>, List<List<T>>> finisher() {
        return lists -> lists;
    }

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

And then you can use it in a way like this:

    List<List<Integer>> collect = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).collect(new GroupingCollector<>(3));
    System.out.println(collect);

Will print:

[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]

Upvotes: 1

Smutje
Smutje

Reputation: 18163

You could write your own collector finisher, similar to

final List<String> strings = Arrays.asList("Hello", "World", "I", "Am", "You");
final int size = 3;

final List<List<String>> stringLists = strings.stream()
        .collect(Collectors.collectingAndThen(Collectors.toList(), new Function<List<String>, List<List<String>>>() {
            @Override
            public List<List<String>> apply(List<String> strings) {
                final List<List<String>> result = new ArrayList<>();
                int counter = 0;
                List<String> stringsToAdd = new ArrayList<>();

                for (final String string : strings) {
                    if (counter == 0) {
                        result.add(stringsToAdd);
                    } else {
                        if (counter == size) {
                            stringsToAdd = new ArrayList<>();
                            result.add(stringsToAdd);
                            counter = 0;
                        }
                    }

                    ++counter;
                    stringsToAdd.add(string);
                }

                return result;
            }
        }));

System.out.println("stringLists = " + stringLists); // stringLists = [[Hello, World, I], [Am, You]]

Upvotes: 0

Related Questions