Java8 sublists of a List<> using lambdas
Asked Answered
C

4

10

I have a problem which I feel would be perfect for streams and/or lambdas. On the other hand I don't want to overcomplicate this, but since will use this specific technique in many variations (run function on a sublist), I would like some ideas about how to get it right from the beginning.

I have a List<Product> productList.

I want to be able to iterate over all sublists in productList. For example all sublists with size=30. This sublist should then be used as an argument to a function.

This is my current, naive, solution:

List<Product> products=...
// This example uses sublists of size 30
for (int i = 0; i < products.size() - 29; i++) {
    // sublist start index is inclusive, but end index is exclusive
    List<Product> sublist = products.subList(i, i + 30);
    Double res = calc(sublist);
}

// an example of a function would be moving average

How would this be implemented using lambdas?

EDIT I tried to come up with the simplest possible example to illustrate the problem. After some comments, I realized that a perfect example is calculating a moving average. First MAVG is calculated on sublist [0..29], second on [1..30], third on [2..31] and so on.

Cutch answered 3/4, 2017 at 19:2 Comment(4)
Using streams and lambdas won't make the code more readable, on the contrary.Frascati
What is the bigger problem you are trying to solve, why do you need sublists of 30? Also you are not making a sublist size 30 but a list that 29 products smaller then the full list.Janus
this was just an example, and above code works (at least the original using same type of solution - this was a simplified example). See EDIT for more context.Cutch
@ZhekaKozlov not really, using that example, I want sublists: ['a','b','c'],['b','c'.'d'],['c','d','e'],['d','e','f'] etc etc, like a "sliding window"Cutch
H
7

Unless I'm missing something obvious...

IntStream.range(0, products.size() - 29)
            .mapToObj(i -> products.subList(i, i + 30))
            .map(list -> calc(list))
            .forEach... // or any other terminal op

Well if you want to run more then a single function to these sublists, like:

double result = calc(sublist)
log(sublist) // void
double res = diffCalc(sublist)

you are probably off staying with the usual for loop.

a map operation does a single action on a sublist, it could be made to do more in a stream, but that will look really ugly IMO.

Hydroxyl answered 3/4, 2017 at 19:11 Comment(8)
I guess this is what I am looking for, however I can't say I understand it. Would you consider this 'state of the art' when calculating a moving average as in my EDIT?Cutch
BTW... the arbitrary constants (29 & 30) was just what I used when the example used sublists of size 30.Cutch
@PeterAndersson It's fairly standard code for iterating over indexes.Loveland
@Hydroxyl no, as you see in my EDIT, a moving average is a better example. Where index 0..29 => avg[0], index 1..30 => agv[1], index 2..31 => avg[2] and so on, A normal moving average.Cutch
^ A moving average can be calculated significantly more efficiently if you just scan the list one by one.Jugular
yes, but as I specified in the question, I want to run several (a lot) more complicated functions in the same kind of way.Cutch
@Jugular moving average detail was added after the answer.Hydroxyl
yes, ATM I dont see any benefit with changing my approach. I just "felt" that applying a lamdba function and some stream magic would make the code more clean but for now that deosnt seem to be the case.Cutch
A
6

If you don't mind using third party libraries, there is a method subLists in StreamEx that does exactly what you want:

List<Product> products = ...
Stream<List<Product>> lists = StreamEx.ofSubLists(products, 30, 1);
Aeromancy answered 3/4, 2017 at 22:48 Comment(1)
This solution implements a fixed window of non-overlapping sublists, e.g. [[0, 1, 2], [3, 4, 5]]. The question is asking for a sliding window of overlapping sublists, e.g. [[0, 1, 2], [1, 2, 3], [2, 3, 4], [3, 4, 5]].Disseminule
I
1

I think that code could be less clear with using lambdas. Maybe plain old if is better that modern lambda?

    int sizeSubList = 30;

    for (int i = 0; i < products.size(); i++)
        calc(products.subList(i, Math.min(i + sizeSubList, products.size())));
Indies answered 3/4, 2017 at 19:54 Comment(1)
Yes I will probably stick with the old way. One problem with your code is however that some lists will be smaller than 30 items, which the hard-coded offsets in my examlpe takes care of. But thanks anyway.Cutch
D
0

The JEP 461: Stream Gatherers Java 22 preview language feature can be used to achieve the desired result:

List<Double> results = productList.stream()
        .gather(Gatherers.windowSliding(30))
        .map(sublist -> calc(sublist))
        .toList();

This uses the new Stream.gather method with the new built-in Gatherers.windowSliding gatherer to convert the map's entry set from Stream<Entry> to a pairwise Stream<List<Entry>>. Each of these two-element lists is then transformed to a two-element String[] using the existing Stream.map method.

Walkthrough

Stream walkthrough using sublist size 3, and a function that averages the values in the sublist:

[
  Product(value = 0),
  Product(value = 1),
  Product(value = 2),
  Product(value = 3),
  Product(value = 4)
]

->

[
  [Product(value = 0), Product(value = 1), Product(value = 2)],
  [Product(value = 1), Product(value = 2), Product(value = 3)],
  [Product(value = 2), Product(value = 3), Product(value = 4)]
]

->

[1, 2, 3]

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

Returns a Gatherer that gathers elements into windows -- encounter-ordered groups of elements -- of a given size, where each subsequent window includes all elements of the previous window except for the least recent, and adds the next element in the stream. […]

Example:

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

// will contain: [[1, 2, 3, 4, 5, 6], [2, 3, 4, 5, 6, 7], [3, 4, 5, 6, 7, 8]]
List<List<Integer>> windows6 =
    Stream.of(1,2,3,4,5,6,7,8).gather(Gatherers.windowSliding(6)).toList();
Disseminule answered 21/12, 2023 at 21:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.