10

I'm trying to split a list into a list of list where each list has a maximum size of 4.

I would like to know how this is possible to do using lambdas.

Currently the way I'm doing it is as follow:

List<List<Object>> listOfList = new ArrayList<>();

final int MAX_ROW_LENGTH = 4;
int startIndex =0;
while(startIndex <= listToSplit.size() )    
{
    int endIndex = ( ( startIndex+MAX_ROW_LENGTH ) <  listToSplit.size() ) ? startIndex+MAX_ROW_LENGTH : listToSplit.size();
    listOfList.add(new ArrayList<>(listToSplit.subList(startIndex, endIndex)));
    startIndex = startIndex+MAX_ROW_LENGTH;
}

UPDATE

It seems that there isn't a simple way to use lambdas to split lists. While all of the answers are much appreciated, they're also a wonderful example of when lambdas do not simplify things.

Lucas T
  • 3,011
  • 6
  • 29
  • 36
  • 7
    It should be noted that this can be done just using `Lists.partition(origList, 3);`. No lambdas necessary. (Requires Guava though unfortunately). – Carcigenicate Aug 22 '17 at 12:12
  • 2
    Not a direct answer to your question, but when you will drop lambda requirement, use guava Iterables.partition(list,size) for instant fun. – Artur Biesiadowski Aug 22 '17 at 12:13
  • 2
    @azro `.size()` has constant access for an array list doesn't it? I would expect calls to it to be extremely cheap. It probably just grabs a private field. – Carcigenicate Aug 22 '17 at 12:13
  • may be this will help https://stackoverflow.com/a/41500804/3588833 – Wild Fire Aug 22 '17 at 12:16
  • I've actually done just that some time ago... https://stackoverflow.com/questions/45078134/divide-longstream-into-substreams-with-maximal-length/45078426#45078426 – Eugene Aug 22 '17 at 12:16
  • 2
    Also, why the lambda requirement? Even using Clojure, which makes extensive use of Higher Order functions, using a lambda wouldn't make sense. It would just be `(partition size your-list)`. What do you expect the lambda to be used for? – Carcigenicate Aug 22 '17 at 12:17
  • @Eugene that's not very pretty :) – Kayaman Aug 22 '17 at 12:17
  • @Kayaman agree not being pretty - but it parallelizes in a good way. I find pretty != good (you probably do that too). I mean `Spliterator`s might not be pretty too... – Eugene Aug 22 '17 at 12:19
  • 3
    I hope that you don't want to use the lambda because it's fancy. You're free to experiment it, but don't abuse the lambda for purposes that it's not intended to do. Put what you have written in a method and use it. – KarelG Aug 22 '17 at 12:20
  • @Eugene Yeah, but for a simple general purpose no-need-to-parallelize situation that's not a very nice solution. For a specific purpose with lots of data, entirely different situation. – Kayaman Aug 22 '17 at 12:20
  • Your requirement is actually unclear: you are not specifying how you want these sublists to be filled. Here is an example that "dispatches" using modulo: https://stackoverflow.com/questions/45792207/fixed-sublist-count-but-dynamic-members-in-java – GhostCat Aug 22 '17 at 12:25
  • @KarelG, Yes, lambdas are fancy =0) I like the fact that one can do in one line what previously took several lines. I have found lambda expressions to be concise, powerful, and times, cryptic. – Lucas T Aug 22 '17 at 12:55
  • 1
    This might be useful: https://www.baeldung.com/java-list-split – Yellowjacket Apr 01 '22 at 00:09

6 Answers6

6

Try this approach:

static <T> List<List<T>> listSplitter(List<T> incoming, int size) {
    // add validation if needed
    return incoming.stream()
            .collect(Collector.of(
                    ArrayList::new,
                    (accumulator, item) -> {
                        if(accumulator.isEmpty()) {
                            accumulator.add(new ArrayList<>(singletonList(item)));
                        } else {
                            List<T> last = accumulator.get(accumulator.size() - 1);
                            if(last.size() == size) {
                                accumulator.add(new ArrayList<>(singletonList(item)));
                            } else {
                                last.add(item);
                            }
                        }
                    },
                    (li1, li2) -> {
                        li1.addAll(li2);
                        return li1;
                    }
            ));
}
System.out.println(
        listSplitter(
                Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
                4
        )
);

Also note that this code could be optimized, instead of:

new ArrayList<>(Collections.singletonList(item))

use this one:

List<List<T>> newList = new ArrayList<>(size);
newList.add(item);
return newList;
Durgpal Singh
  • 11,481
  • 4
  • 37
  • 49
alex.b
  • 1,448
  • 19
  • 23
  • 1
    This code can be reused if you extract Collector to the separate class/variable/field – alex.b Aug 22 '17 at 15:12
  • 1
    This is pretty neat, and fits exactly what OP asked for. But I profiled it and it's ~5 times slower than the obvious non-lambda sublist-grabbing `for` loop. – slim Aug 23 '17 at 15:02
  • Yes... I tried JMH profiler and it's slower. But have the author noticed that performance was important? Almost most of stream based ops are slower but they were introduced to make code more readable, chainable and ready for simple parallelization. If you extract this Collector as separate you can use it in chained ops like ```java.util.stream.Collectors#collectingAndThen``` for example. – alex.b Aug 23 '17 at 16:56
5

If you REALLY need a lambda it can be done like this. Otherwise the previous answers are better.

    List<List<Object>> lists = new ArrayList<>();
    AtomicInteger counter = new AtomicInteger();
    final int MAX_ROW_LENGTH = 4;
    listToSplit.forEach(pO -> {
        if(counter.getAndIncrement() % MAX_ROW_LENGTH == 0) {
            lists.add(new ArrayList<>());
        }
        lists.get(lists.size()-1).add(pO);
    });
Jotunacorn
  • 496
  • 1
  • 4
  • 12
  • Thank you, @Jotunacom, I don't REALLY need a lambda. I just wondered if there was an easy way of doing it using lambdas. It's ok if there isn't a simple straight forward way of doing things with lambdas. I happen to like them, and I understand they're not the solution for everything. Thank you for your answer, nonetheless. – Lucas T Aug 22 '17 at 13:06
3

Surely the below is sufficient

final List<List<Object>> listOfList = new ArrayList<>(
            listToSplit.stream()
                    .collect(Collectors.groupingBy(el -> listToSplit.indexOf(el) / MAX_ROW_LENGTH))
                    .values()
    );

Stream it, collect with a grouping: this gives a Map of Object -> List, pull the values of the map and pass directly into whatever constructor (map.values() gives a Collection not a List).

tom01
  • 125
  • 3
  • 11
2

The requirement is a bit odd, but you could do:

final int[] counter = new int[] {0};

List<List<Object>> listOfLists = in.stream()
   .collect(Collectors.groupingBy( x -> counter[0]++ / MAX_ROW_LENGTH ))
   .entrySet().stream()
   .sorted(Map.Entry.comparingByKey())
   .map(Map.Entry::getValue)
   .collect(Collectors.toList());

You could probably streamline this by using the variant of groupingBy that takes a mapSupplier lambda, and supplying a SortedMap. This should return an EntrySet that iterates in order. I leave it as an exercise.

What we're doing here is:

  • Collecting your list items into a Map<Integer,Object> using a counter to group. The counter is held in a single-element array because the lambda can only use local variables if they're final.
  • Getting the map entries as a stream, and sorting by the Integer key.
  • Using Stream::map() to convert the stream of Map.Entry<Integer,Object> into a stream of Object values.
  • Collecting this into a list.

This doesn't benefit from any "free" parallelisation. It has a memory overhead in the intermediate Map. It's not particularly easy to read.


However, I wouldn't do this, just for the sake of using a lambda. I would do something like:

for(int i=0; i<in.size(); i += MAX_ROW_LENGTH) {
    listOfList.add(
        listToSplit.subList(i, Math.min(i + MAX_ROW_LENGTH, in.size());
}

(Yours had a defensive copy new ArrayList<>(listToSplit.subList(...)). I've not duplicated it because it's not always necessary - for example if the input list is unmodifiable and the output lists aren't intended to be modifiable. But do put it back in if you decide you need it in your case.)

This will be extremely fast on any in-memory list. You're very unlikely to want to parallelise it.


Alternatively, you could write your own (unmodifiable) implementation of List that's a view over the underlying List<Object>:

public class PartitionedList<T> extends AbstractList<List<T>> {

    private final List<T> source;
    private final int sublistSize;

    public PartitionedList(T source, int sublistSize) {
       this.source = source;
       this.sublistSize = sublistSize;
    }

    @Override
    public int size() {
       return source.size() / sublistSize;
    }

    @Override
    public List<T> get(int index) {
       int sourceIndex = index * sublistSize
       return source.subList(sourceIndex, 
                             Math.min(sourceIndex + sublistSize, source.size());
    }
}

Again, it's up to you whether you want to make defensive copies here.

This will be have equivalent big-O access time to the underlying list.

slim
  • 40,215
  • 13
  • 94
  • 127
  • 1
    Another SO answer does a similar thing to my `PartitionedList`, only with a `Spliterator`: https://stackoverflow.com/a/25507602/7512 – slim Aug 22 '17 at 13:41
2

Perhaps you can use something like that

 BiFunction<List,Integer,List> splitter= (list2, count)->{
            //temporary list of lists
            List<List> listOfLists=new ArrayList<>();

            //helper implicit recursive function
            BiConsumer<Integer,BiConsumer> splitterHelper = (offset, func) -> {
                if(list2.size()> offset+count){
                    listOfLists.add(list2.subList(offset,offset+count));

                    //implicit self call
                    func.accept(offset+count,func);
                }
                else if(list2.size()>offset){
                    listOfLists.add(list2.subList(offset,list2.size()));

                    //implicit self call
                    func.accept(offset+count,func);
                }
            };

            //pass self reference
            splitterHelper.accept(0,splitterHelper);

            return listOfLists;
        };

Usage example

List<Integer> list=new ArrayList<Integer>(){{
            add(1);
            add(2);
            add(3);
            add(4);
            add(5);
            add(6);
            add(7);
            add(8);
            add(8);
        }};

        //calling splitter function
        List listOfLists = splitter.apply(list, 3 /*max sublist size*/);

        System.out.println(listOfLists);

And as a result we have

[[1, 2, 3], [4, 5, 6], [7, 8, 8]]
Vladyslav Nikolaiev
  • 1,819
  • 3
  • 20
  • 39
2

You can use:

ListUtils.partition(List list, int size)

OR

List<List> partition(List list, int size)

Both return consecutive sublists of a list, each of the same size (the final list may be smaller).

Gayan Weerakutti
  • 11,904
  • 2
  • 71
  • 68