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.

13
  • 7
    It should be noted that this can be done just using Lists.partition(origList, 3);. No lambdas necessary. (Requires Guava though unfortunately). Commented Aug 22, 2017 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. Commented Aug 22, 2017 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. Commented Aug 22, 2017 at 12:13
  • 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? Commented Aug 22, 2017 at 12:17
  • 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. Commented Aug 22, 2017 at 12:20

6 Answers 6

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;
Sign up to request clarification or add additional context in comments.

3 Comments

This code can be reused if you extract Collector to the separate class/variable/field
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.
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.
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);
    });

1 Comment

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.
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).

1 Comment

This looks simple but the I guess the complexity of indexOf in O(n) which can be bad if initially list is big
3

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).

Comments

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]]

Comments

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.

1 Comment

Another SO answer does a similar thing to my PartitionedList, only with a Spliterator: stackoverflow.com/a/25507602/7512

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.