17

Given a java.util.List with n elements and a desired page size m, I want to transform it to a map containing n/m+n%m elements. Each map element shall contain m elements.

Here's an example with integers:

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

    // What is the equivalent Java 8 code to create the map below from my list?

    Map<Integer, List<Integer>> map = new HashMap<>();
    map.put(0, Arrays.asList(1,2,3));
    map.put(1, Arrays.asList(4,5,6));
    map.put(2, Arrays.asList(7,8,9));
    map.put(3, Arrays.asList(10));

Is this possible, using Java 8?

Duncan Jones
  • 67,400
  • 29
  • 193
  • 254
adragomir
  • 457
  • 4
  • 16
  • 33
  • 1
    What have you tried so far? Please read [How do I ask a good question?](http://stackoverflow.com/help/how-to-ask). – DavidPostill Mar 26 '15 at 08:25
  • So, I looked into Collectors::partitioningBy but that splits a list given a predicate. I asked this because I don't know where to start in Java 8 to achieve this. – adragomir Mar 26 '15 at 08:42
  • 1
    @user3030447 Are you sure you want a `Map` and not a `Map>`? You can always convert the list to a commarised string during a presentation phase... – Duncan Jones Mar 26 '15 at 08:44
  • I could go a long with Map> also :) – adragomir Mar 26 '15 at 09:01
  • @user3030447 Based on your last comment, I took the liberty of making your question more generic so it will be helpful to a wider audience. – Duncan Jones Mar 26 '15 at 09:46
  • 1
    Look at the end of [this answer](http://stackoverflow.com/a/28211518/2711488)… – Holger Mar 26 '15 at 10:12
  • @Holger C'mon, you're on all the Java 8 questions... next time I will just try to find a duplicate in your set of answers :-D – Alexis C. Mar 26 '15 at 10:24
  • 2
    @Alexis C.: it isn’t an exact duplicate due to the collection into a `Map` but linking the questions will help future searchers. – Holger Mar 26 '15 at 10:43

3 Answers3

19

You could use IntStream.iterate combined with the toMap collector and the subList method on List (thanks to Duncan for the simplifications).

import static java.util.stream.Collectors.toMap;
import static java.lang.Math.min;

...

static Map<Integer, List<Integer>> partition(List<Integer> list, int pageSize) {
    return IntStream.iterate(0, i -> i + pageSize)
          .limit((list.size() + pageSize - 1) / pageSize)
          .boxed()
          .collect(toMap(i -> i / pageSize,
                         i -> list.subList(i, min(i + pageSize, list.size()))));
}

You first calculate the numbers of keys you need in the map. This is given by (list.size() + pageSize - 1) / pageSize (this will be the limit of the stream).

Then you create a Stream that creates the sequence 0, pageSize, 2* pageSize, ....

Now for each value i you grab the corresponding subList which will be our value (you need an additional check for the last subList for not getting out of bounds) for which you map the corresponding key which will be the sequence 0/pageSize, pageSize/pageSize, 2*pageSize/pageSize that you divide by pageSize to get the natural sequence 0, 1, 2, ....

The pipeline can be safely run in parallel (you may need to use the toConcurrentMap collector instead). As Brian Goetz commented (thanks for reminding me that), iterate is not worth if you want to parallelize the stream, so here's a version with range.

return IntStream.range(0, (list.size() + pageSize - 1) / pageSize)
                .boxed()
                .collect(toMap(i -> i ,
                               i -> list.subList(i * pageSize, min(pageSize * (i + 1), list.size()))));

So as with your example (a list of 10 elements with a page size of 3), you'll get the following sequence:

0, 3, 6, 9, 12, 15, ... that you limit to (10 + 3 - 1) / 3 = 12 / 3 = 4, which let the sequence 0, 3, 6, 9. Now each value is mapped to its corresponding sublist:

0 / pageSize = 0 -> list.subList(0, min(0 + pageSize, 10)) = list.subList(0, 3);
3 / pageSize = 1 -> list.subList(3, min(3 + pageSize, 10)) = list.subList(3, 6);
6 / pageSize = 2 -> list.subList(6, min(6 + pageSize, 10)) = list.subList(6, 9);
9 / pageSize = 3 -> list.subList(9, min(9 + pageSize, 10))  = list.subList(6, 10);
                                      ^
                                      |
                        this is the edge-case for the last sublist to
                        not be out of bounds


If you really want a Map<Integer, String> you could replace the value mapper function with
import static java.util.stream.Collectors.joining;

...

i -> list.subList(i, min(i + pageSize, list.size()))
         .stream()
         .map(Object::toString)
         .collect(joining(","))

which just collect the elements separated by a comma into a single String.

Community
  • 1
  • 1
Alexis C.
  • 91,686
  • 21
  • 171
  • 177
  • This is the output {0=[1, 2, 3], 1=[4, 5, 6], 2=[7, 8, 9], 3=[10]} for first version. Alexis you are a BLAST baby, thank you for showing me. I will understand the code and improve my knowledge :) Wish you all the best – adragomir Mar 26 '15 at 09:25
  • 1
    @user3030447 I added how it behaves with your corresponding example. – Alexis C. Mar 26 '15 at 09:26
  • 3
    Good solution. You can simplify your limit to: `.limit((list.size() + pageSize - 1) / pageSize)`, since both values are positive (see [this answer](http://stackoverflow.com/a/7446742/474189)). The value mapping method could also simplify to `i -> list.subList(i, Math.min(i + pageSize, list.size()))` – Duncan Jones Mar 26 '15 at 09:30
  • 4
    You can run it in parallel, but starting the pipeline with IntStream.iterate() will destroy any parallelism you get (this is a fundamentally sequential source.) Far better to use IntStream.range, which does the same thing as your iteration, and splits much better. Then you don't need to use limit, which also parallelizes poorly due to its fundamental dependence on encounter order. – Brian Goetz Mar 26 '15 at 14:17
7

Simple solution using Guava: com.google.common.collect.Lists#partition:

    List<List<Integer>> partition = Lists.partition(list, 3); //<- here
    Map map = IntStream.range(0, partition.size()).boxed().collect(Collectors.toMap(
                    Function.identity(),
                    i -> partition.get(i)));
卢声远 Shengyuan Lu
  • 31,208
  • 22
  • 85
  • 130
0

As noted in the comments this also works if the list is not a natural sequence of integers. You would have to use a generated IntStream then and refer to the elements in list by index.

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Map<Integer, String> map = IntStream
    .range(0, list.size())
    .boxed()
    .collect(groupingBy(
        i -> i / 3, //no longer i-1 because we start with 0
        mapping(i -> list.get((int) i).toString(), joining(","))
        ));

//result: {0="1,2,3", 1="4,5,6", 2="7,8,9", 3="10"}

We start with an IntStream representing the indices of the list.

groupingBy groups the elements by some classifier. In your case it groups x elements per page.

mapping applies a mapping function to the elements and collects them afterwards. The mapping is necessary because joiningonly accepts CharSequence. joining itself joins the elements by using an arbitrary delimiter.

a better oliver
  • 26,330
  • 2
  • 58
  • 66
  • This works nicely if you have a natural sequence in the list, but with let's say the list [1, -1, 5, 2] and a page size of 2, you won't get the desired results. – Alexis C. Mar 26 '15 at 09:36
  • @AlexisC. That's true, but in that case you can use a generated `IntStream` and refer to the original list by index. The algorithm wouldn't change though. – a better oliver Mar 26 '15 at 09:44
  • @zeroflagL You could remove `.limit(list.size())` as `range` already generates a finite stream :-). Also if the underlying list is a `LinkedList`, `get` will reduce the overall performance (although I'm not sure it's a big concern there). – Alexis C. Mar 26 '15 at 09:59
  • @AlexisC. Of course. I used `iterate` before. Thanks. – a better oliver Mar 26 '15 at 09:59
  • @zeroflagL Rather than editing your answer to point out the first part is broken, just remove the first part and show us your best solution. – Duncan Jones Mar 26 '15 at 13:37
  • @Duncan It's far from being broken. It works as intended with the provided data. And, please, mind your tone. SO is supposed to be a friendly place :) – a better oliver Mar 26 '15 at 14:00
  • @zeroflagL I think my tone was just fine. I don't need to pepper my comments with platitudes to be polite; this is not a chat room. If you think your first answer had merit, don't remove it just because I asked you to! – Duncan Jones Mar 26 '15 at 14:03
  • @Duncan _... I asked you to_ (sic!) – a better oliver Mar 26 '15 at 14:18
  • @zeroflagL I don't understand your [last comment](http://stackoverflow.com/questions/29273705/how-to-paginate-a-list-of-objects-in-java-8/29275010?noredirect=1#comment46759449_29275010) – Duncan Jones Mar 26 '15 at 14:24