Wie bekomme ich mit Java 8 Lambda eine Reihe von Elementen aus dem Stream?

73

In einer früheren Frage [ Wie wird in Java 8 dynamisch gefiltert?] Stuart Marks gab eine wundervolle Antwort und stellte mehrere nützliche Dienstprogramme zur Verfügung, um die Auswahl von topN und topPercent aus dem Stream zu handhaben.

Ich werde sie hier aus seiner ursprünglichen Antwort aufnehmen:

@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));
    };
}

Meine Fragen hier sind:

[1] So erhalten Sie Top-Elemente von 3 bis 7 aus einem Stream mit einer bestimmten Anzahl von Elementen. Wenn der Stream also Elemente von A1, A2 .. A10 enthält, rufen Sie an

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

wird {A3, A4, A5, A6, A7} zurückgeben}

Der einfachste Weg, den ich mir vorstellen kann, ist, die Top 7 [T7] vom Original zu bekommen, die Top 3 [T3] vom Original zu bekommen und dann T7 - ​​T3 zu bekommen.

[2] So erhalten Sie Top-Elemente von Top 10% bis Top 30% aus einem Stream mit einer bestimmten Anzahl von Elementen. Wenn der Stream also Elemente von X1, X2 .. X100 enthält, rufen Sie an

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

gibt {X10, X11, X12, ..., X29, X30} zurück

Der einfachste Weg, den ich mir vorstellen kann, ist, die besten 30% [TP30] vom Original zu erhalten, die besten 10% [TP10] vom Original zu erhalten und dann TP30 - TP10 zu erhalten.

Welche besseren Möglichkeiten gibt es, Java 8 Lambda zu verwenden, um die oben genannten Situationen präzise auszudrücken?

Frank
quelle

Antworten:

58

Der Benutzer Skiwi hat den ersten Teil der Frage bereits beantwortet . Der zweite Teil ist:

(2) So erhalten Sie Top-Artikel von Top 10% bis Top 30% aus einem Stream mit einer bestimmten Anzahl von Artikeln ....

Dazu müssen Sie eine ähnliche Technik anwenden wie topPercentin meiner Antwort auf die andere Frage. Das heißt, Sie müssen die Elemente in einer Liste sammeln, um eine Zählung der Elemente zu erhalten, möglicherweise nachdem eine vorgelagerte Filterung durchgeführt wurde.

Sobald Sie die Anzahl haben, berechnen Sie die richtigen Werte für skipund limitbasierend auf der Anzahl und den gewünschten Prozentsätzen. So etwas könnte funktionieren:

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

Natürlich müssen Sie die Fehlerprüfung an fromund durchführen to. Ein subtileres Problem besteht darin, zu bestimmen, wie viele Elemente ausgegeben werden sollen. Wenn Sie beispielsweise zehn Elemente haben, befinden sich diese in Indizes [0..9], die 0%, 10%, 20%, ..., 90% entsprechen. Wenn Sie jedoch nach einem Bereich von 9% bis 11% fragen würden, würde der obige Code überhaupt keine Elemente ausgeben, nicht den mit 10%, wie Sie es vielleicht erwarten würden. Daher ist es wahrscheinlich notwendig, an den prozentualen Berechnungen zu basteln, um die Semantik dessen zu erfüllen, was Sie versuchen zu tun.

Stuart Marks
quelle
Nah genug an dem, wonach ich gesucht habe, werde ich die Details herausarbeiten, danke!
Frank
Ich habe meine Antwort so aktualisiert, dass sie auch eine Form Ihrer Arbeit enthält. Wenn Sie dann Collectors verwenden, könnte dies möglicherweise auch für die ursprüngliche Frage der Kriterien interessant sein.
Skiwi
@skiwi Interessant: Verwenden Sie die Finisher-Funktion eines Sammlers, um die Sammlung wieder in einen Stream umzuwandeln. Ich bin mir nicht sicher, ob es notwendigerweise besser oder schlechter ist, als nur eine lokale Variable zu deklarieren. (Der Lambda-Parameter wird in diesem Fall wie ein lokaler Parameter verwendet.) Dies ist jedoch eine nützliche Technik, die Sie für die Zukunft berücksichtigen sollten.
Stuart Marks
61

Um einen Bereich von a zu erhalten Stream<T>, können Sie skip(long n)zunächst eine festgelegte Anzahl von Elementen überspringen und dann aufrufen, limit(long n)um nur eine bestimmte Anzahl von Elementen zu übernehmen.

Stellen Sie sich einen Stream mit 10 Elementen vor. Um die Elemente 3 bis 7 zu erhalten, rufen Sie normalerweise Folgendes auf List:

list.subList(3, 7);

Jetzt mit a Streammüssen Sie zuerst 3 Elemente überspringen und dann 7 - 3 = 4 Elemente nehmen, damit es wird:

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

Als Variante der Lösung von @StuartMarks zur zweiten Antwort biete ich Ihnen die folgende Lösung an, bei der die Möglichkeit einer Verkettung bestehen bleibt. Sie funktioniert ähnlich wie bei @StuartMarks:

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)))
    );
}

und

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

Dadurch werden die Elemente 10 bis 29 gedruckt.

Es funktioniert mit a Collector<T, ?, Stream<T>>, das Ihre Elemente aus dem Stream aufnimmt, sie in a umwandelt List<T>, dann a erhält Stream<T>, es sortiert und die (richtigen) Grenzen darauf anwendet.

Skiwi
quelle
Wenn Sie die ersten 10% der Artikel überspringen, sind nur noch 90% der Artikel im Stream, wie Sie die Artikel von den ursprünglichen 30% erhalten, da die 30% von den 90% nicht die ursprünglichen 30% sind, bin ich richtig ?
Frank
1
@Frank Sie müssen diese Zahlen im Voraus berechnen.
Skiwi
@Frank Ich habe die Antwort aktualisiert, um auch eine Variante aufzunehmen, die mit der Verkettung von Streams zusammenarbeitet.
Skiwi
Interessant und vielen Dank für Ihre Mühe. Was ist der Unterschied vom Standpunkt des Benutzers aus? Wann welches verwenden? Irgendwelche Effizienz- / Genauigkeitsunterschiede? Was ich sehen kann, sind mehr Schritte und scheint komplexer zu sein. Gibt es eine Möglichkeit, es auf das Formular zu vereinfachen: Kriterium topPercentFromRange (Comparator <Widget> cmp, double from, double to), was intuitiver ist.
Frank
@Frank Ich persönlich würde es vorziehen, Methoden zu verketten, da dies die Grundvoraussetzung für die Verwendung von Streams ist. Ich mag keine statischen Methoden, es sei denn, dies ist absolut notwendig. Das ist meiner Meinung nach der einzige wirkliche Unterschied.
Skiwi