How to get a range of items from stream using Java 8 lambda?

Frank picture Frank · Apr 7, 2014 · Viewed 83.5k times · Source

In a previous question [ How to dynamically do filtering in Java 8? ] Stuart Marks gave a wonderful answer, and provided several useful utilities to handle selection of topN and topPercent from stream.

I'll include them here from his original answer :

@FunctionalInterface
public interface Criterion {
    Stream<Widget> apply(Stream<Widget> s);
}

Criterion topN(Comparator<Widget> cmp, long n) {
    return stream -> stream.sorted(cmp).limit(n);
}

Criterion topPercent(Comparator<Widget> cmp, double pct) {
    return stream -> {
        List<Widget> temp =
            stream.sorted(cmp).collect(toList());
        return temp.stream()
                   .limit((long)(temp.size() * pct));
    };
}

My questions here are :

[1] How to get top items from 3 to 7 from a stream with certain amount of items, so if the stream has items from A1, A2 .. A10, the call to

topNFromRange(Comparator<Widget> cmp, long from, long to) = topNFromRange(comparing(Widget::length), 3L, 7L)

will return { A3, A4, A5, A6, A7 }

The simplest way I can think of is get the top 7 [ T7 ] from original, get the top 3 [ T3 ] from original, and then get T7 - T3.

[2] How to get top items from top 10% to top 30% from a stream with certain amount of items, so if the stream has items from X1, X2 .. X100, the call to

topPercentFromRange(Comparator<Widget> cmp, double from, double to) = topNFromRange(comparing(Widget::length), 0.10, 0.30)

will return { X10, X11, X12, ..., X29, X30 }

The simplest way I can think of is get the top 30% [ TP30 ] from original, get the top 10% [ TP10 ] from original, and then get TP30 - TP10.

What are some better ways to use Java 8 Lambda to concisely express the above situations ?

Answer

skiwi picture skiwi · Apr 7, 2014

To get a range from a Stream<T>, you can use skip(long n) to first skip a set number of elements, and then you can call limit(long n) to only take a specific amount of items.

Consider a stream with 10 elements, then to get elements 3 to 7, you would normally call from a List:

list.subList(3, 7);

Now with a Stream, you need to first skip 3 items, and then take 7 - 3 = 4 items, so it becomes:

stream.skip(3).limit(4);

As a variant to @StuartMarks' solution to the second answer, I'll offer you the following solution which leaves the possibility to chain intact, it works similar to how @StuartMarks does it:

private <T> Collector<T, ?, Stream<T>> topPercentFromRangeCollector(Comparator<T> comparator, double from, double to) {
    return Collectors.collectingAndThen(
        Collectors.toList(),
        list -> list.stream()
            .sorted(comparator)
            .skip((long)(list.size() * from))
            .limit((long)(list.size() * (to - from)))
    );
}

and

IntStream.range(0, 100)
        .boxed()
        .collect(topPercentFromRangeCollector(Comparator.comparingInt(i -> i), 0.1d, 0.3d))
        .forEach(System.out::println);

This will print the elements 10 through 29.

It works by using a Collector<T, ?, Stream<T>> that takes in your elements from the stream, transforms them into a List<T>, then obtains a Stream<T>, sorts it and applies the (correct) bounds to it.