Bietet Java 8 eine gute Möglichkeit, einen Wert oder eine Funktion zu wiederholen?


117

In vielen anderen Sprachen, z. Haskell, es ist einfach, einen Wert oder eine Funktion mehrmals zu wiederholen, z. um eine Liste von 8 Kopien des Wertes 1 zu erhalten:

take 8 (repeat 1)

Aber ich habe dies in Java 8 noch nicht gefunden. Gibt es eine solche Funktion im JDK von Java 8?

Oder alternativ etwas, das einem Bereich wie entspricht

[1..8]

Es scheint ein offensichtlicher Ersatz für eine ausführliche Aussage in Java zu sein

for (int i = 1; i <= 8; i++) {
    System.out.println(i);
}

so etwas haben

Range.from(1, 8).forEach(i -> System.out.println(i))

Obwohl dieses Beispiel nicht viel prägnanter aussieht ... aber hoffentlich ist es besser lesbar.


2
Haben Sie die Streams-API studiert ? Für das JDK sollte dies die beste Wahl sein. Es hat eine Bereichsfunktion , das habe ich bisher gefunden.
Marko Topolnik

1
@MarkoTopolnik Die Streams-Klasse wurde entfernt (genauer gesagt, sie wurde auf mehrere andere Klassen aufgeteilt und einige Methoden wurden vollständig entfernt).
Assylias

3
Sie nennen eine for-Schleife ausführlich! Es ist gut, dass Sie in den Cobol-Tagen nicht da waren. Es dauerte über 10 deklarative Aussagen in Cobol, um aufsteigende Zahlen anzuzeigen. Junge Leute wissen heutzutage nicht zu schätzen, wie gut sie es haben.
Gilbert Le Blanc

1
@GilbertLeBlanc Ausführlichkeit hat nichts damit zu tun. Loops sind nicht zusammensetzbar, Streams jedoch. Schleifen führen zu unvermeidbaren Wiederholungen, während Streams die Wiederverwendung ermöglichen. Als solche sind Streams eine quantitativ bessere Abstraktion als Schleifen und sollten bevorzugt werden.
Alain O'Dea

2
@ GilbertLeBlanc und wir mussten barfuß im Schnee codieren.
Dawood ibn Kareem

Antworten:


154

Für dieses spezielle Beispiel könnten Sie Folgendes tun:

IntStream.rangeClosed(1, 8)
         .forEach(System.out::println);

Wenn Sie einen anderen Schritt als 1 benötigen, können Sie eine Zuordnungsfunktion verwenden, z. B. für einen Schritt von 2:

IntStream.rangeClosed(1, 8)
         .map(i -> 2 * i - 1)
         .forEach(System.out::println);

Oder erstellen Sie eine benutzerdefinierte Iteration und begrenzen Sie die Größe der Iteration:

IntStream.iterate(1, i -> i + 2)
         .limit(8)
         .forEach(System.out::println);

4
Durch das Schließen wird Java-Code zum Besseren vollständig transformiert. Ich freue mich auf diesen Tag ...
Marko Topolnik

1
@jwenting Es kommt wirklich darauf an - normalerweise bei GUI-Inhalten (Swing oder JavaFX), dass aufgrund anonymer Klassen viel Kesselplatte entfernt wird .
Assylias

8
@jwenting Für jeden mit Erfahrung in FP ist Code, der sich um Funktionen höherer Ordnung dreht, ein reiner Gewinn. Für jeden ohne diese Erfahrung ist es Zeit, seine Fähigkeiten zu verbessern - oder das Risiko einzugehen, im Staub zurückgelassen zu werden.
Marko Topolnik

2
@MarkoTopolnik Möglicherweise möchten Sie eine etwas neuere Version des Javadoc verwenden (Sie zeigen auf Build 78, der neueste ist Build 105: download.java.net/lambda/b105/docs/api/java/util/stream/… )
Mark Rotteveel

1
@GraemeMoss Sie könnten immer noch das gleiche Muster ( IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());) verwenden, aber es verwirrt die Sache IMO und in diesem Fall scheint eine Schleife angezeigt zu sein.
Assylias

65

Hier ist eine andere Technik, die ich neulich kennengelernt habe:

Collections.nCopies(8, 1)
           .stream()
           .forEach(i -> System.out.println(i));

Der Collections.nCopiesAufruf erzeugt ein Listenthält nKopien von was auch immer Wert , den Sie bieten. In diesem Fall ist es der Boxwert Integer1. Natürlich wird keine Liste mit nElementen erstellt. Es wird eine "virtualisierte" Liste erstellt, die nur den Wert und die Länge enthält, und jeder Aufruf getinnerhalb des Bereichs gibt nur den Wert zurück. Die nCopiesMethode gibt es schon seit der Einführung des Collections Framework in JDK 1.2. Natürlich wurde in Java SE 8 die Möglichkeit hinzugefügt, aus seinem Ergebnis einen Stream zu erstellen.

Große Sache, eine andere Möglichkeit, dasselbe in ungefähr der gleichen Anzahl von Zeilen zu tun.

Allerdings ist diese Technik schneller als das IntStream.generateund IntStream.iteratenähert sich, und überraschend, es ist auch schneller als der IntStream.rangeAnsatz.

Denn iterateund generatedas Ergebnis ist vielleicht nicht allzu überraschend. Das Streams-Framework (eigentlich die Spliterators für diese Streams) basiert auf der Annahme, dass die Lambdas möglicherweise jedes Mal unterschiedliche Werte generieren und eine unbegrenzte Anzahl von Ergebnissen generieren. Dies macht eine parallele Aufteilung besonders schwierig. Die iterateMethode ist auch in diesem Fall problematisch, da jeder Aufruf das Ergebnis des vorherigen erfordert. So sind die Ströme mit generateund iteratenicht sehr gut tun für wiederholte Konstanten zu erzeugen.

Die relativ schlechte Leistung von rangeist überraschend. Auch dies ist virtualisiert, sodass nicht alle Elemente im Speicher vorhanden sind und die Größe im Voraus bekannt ist. Dies sollte zu einem schnellen und leicht parallelisierbaren Spliterator führen. Aber es lief überraschenderweise nicht sehr gut. Vielleicht liegt der Grund darin, dass rangefür jedes Element des Bereichs ein Wert berechnet und dann eine Funktion darauf aufgerufen werden muss. Diese Funktion ignoriert jedoch nur ihre Eingabe und gibt eine Konstante zurück. Ich bin überrascht, dass dies nicht inline und getötet ist.

Die Collections.nCopiesTechnik muss Boxen / Unboxen durchführen, um die Werte zu verarbeiten, da es keine primitiven Spezialisierungen von gibt List. Da der Wert jedes Mal der gleiche ist, wird er grundsätzlich einmal eingerahmt und von allen nKopien gemeinsam genutzt. Ich vermute, dass das Boxen / Unboxen stark optimiert ist, sogar intrinsisch, und es kann gut inliniert werden.

Hier ist der Code:

    public static final int LIMIT = 500_000_000;
    public static final long VALUE = 3L;

    public long range() {
        return
            LongStream.range(0, LIMIT)
                .parallel()
                .map(i -> VALUE)
                .map(i -> i % 73 % 13)
                .sum();
}

    public long ncopies() {
        return
            Collections.nCopies(LIMIT, VALUE)
                .parallelStream()
                .mapToLong(i -> i)
                .map(i -> i % 73 % 13)
                .sum();
}

Und hier sind die JMH-Ergebnisse: (2,8 GHz Core2Duo)

Benchmark                    Mode   Samples         Mean   Mean error    Units
c.s.q.SO18532488.ncopies    thrpt         5        7.547        2.904    ops/s
c.s.q.SO18532488.range      thrpt         5        0.317        0.064    ops/s

In der ncopies-Version gibt es einiges an Varianz, aber insgesamt scheint sie bequem 20x schneller zu sein als die Range-Version. (Ich wäre durchaus bereit zu glauben, dass ich etwas falsch gemacht habe.)

Ich bin überrascht, wie gut die nCopiesTechnik funktioniert. Intern macht es nicht viel Besonderes, da der Stream der virtualisierten Liste einfach mit implementiert wird IntStream.range! Ich hatte erwartet, dass es notwendig sein würde, einen speziellen Spliterator zu erstellen, damit dies schnell geht, aber es scheint bereits ziemlich gut zu sein.


6
Weniger erfahrene Entwickler könnten verwirrt sein oder in Schwierigkeiten geraten, wenn sie erfahren, dass nCopiesnichts kopiert wird und die "Kopien" alle auf dieses eine Objekt verweisen. Es ist immer sicher, wenn dieses Objekt unveränderlich ist , wie in diesem Beispiel ein Boxed-Primitiv. Sie spielen in Ihrer "Boxed Once" -Anweisung darauf an, aber es könnte hilfreich sein, die Vorbehalte hier explizit hervorzuheben, da dieses Verhalten nicht spezifisch für das automatische Boxen ist.
William Price

1
Das bedeutet also, dass LongStream.rangedas deutlich langsamer ist als IntStream.range? Es ist also gut, dass die Idee, keine IntStream(aber LongStreamfür alle ganzzahligen Typen zu verwenden), weggelassen wurde. Beachten Sie, dass es für den sequentiellen Anwendungsfall überhaupt keinen Grund gibt, Stream zu verwenden: Er Collections.nCopies(8, 1).forEach(i -> System.out.println(i));macht dasselbe, ist Collections.nCopies(8, 1).stream().forEach(i -> System.out.println(i));aber möglicherweise noch effizienterCollections.<Runnable>nCopies(8, () -> System.out.println(1)).forEach(Runnable::run);
Holger,

1
@Holger, diese Tests wurden mit einem sauberen Typprofil durchgeführt, sodass sie nichts mit der realen Welt zu tun haben. Wahrscheinlich LongStream.rangeschlechter, weil es zwei Karten mit LongFunctioninnen hat, während ncopieses drei Karten mit und hat IntFunction, ToLongFunctionund LongFunctiondaher sind alle Lambdas monomorph. Das Ausführen dieses Tests für ein vorverschmutztes Typprofil (das näher am realen Fall liegt) zeigt, dass ncopieses 1,5-mal langsamer ist.
Tagir Valeev

1
Vorzeitige Optimierung FTW
Rafael Bugajewski

1
Der Vollständigkeit halber wäre es schön, einen Benchmark zu sehen, der diese beiden Techniken mit einer einfachen alten forSchleife vergleicht. Ihre Lösung ist zwar schneller als der StreamCode, aber ich vermute, dass eine forSchleife beide mit einem signifikanten Vorsprung schlagen würde.
Typeracer

35

Der Vollständigkeit halber und auch, weil ich mir nicht helfen konnte :)

Das Generieren einer begrenzten Folge von Konstanten kommt dem, was Sie in Haskell sehen würden, ziemlich nahe, nur mit Ausführlichkeit auf Java-Ebene.

IntStream.generate(() -> 1)
         .limit(8)
         .forEach(System.out::println);

() -> 1würde nur Einsen erzeugen, ist das beabsichtigt? Die Ausgabe wäre also 1 1 1 1 1 1 1 1.
Christian Ullenboom

4
Ja, gemäß dem ersten Haskell-Beispiel des OP take 8 (repeat 1). Assylias deckte so ziemlich alle anderen Fälle ab.
Clstrfsck

3
Stream<T>hat auch eine generische generateMethode, um einen unendlichen Stream eines anderen Typs zu erhalten, der auf die gleiche Weise eingeschränkt werden kann.
Zstewart

11

Einmal ist eine Wiederholungsfunktion irgendwo definiert als

public static BiConsumer<Integer, Runnable> repeat = (n, f) -> {
    for (int i = 1; i <= n; i++)
        f.run();
};

Sie können es ab und zu folgendermaßen verwenden, z.

repeat.accept(8, () -> System.out.println("Yes"));

Zu bekommen und gleichwertig mit Haskell

take 8 (repeat 1)

Du könntest schreiben

StringBuilder s = new StringBuilder();
repeat.accept(8, () -> s.append("1"));

2
Dieser ist großartig. Allerdings verändert ich es , die Anzahl der Iteration wieder zur Verfügung zu stellen, durch die sich wandelnde Runnablezu Function<Integer, ?>und dann mit f.apply(i).
Fons

0

Dies ist meine Lösung zur Implementierung der Zeitfunktion. Ich bin ein Junior, also gebe ich zu, dass es nicht ideal sein könnte. Ich würde mich freuen zu hören, wenn dies aus irgendeinem Grund keine gute Idee ist.

public static <T extends Object, R extends Void> R times(int count, Function<T, R> f, T t) {
    while (count > 0) {
        f.apply(t);
        count--;
    }
    return null;
}

Hier einige Anwendungsbeispiele:

Function<String, Void> greet = greeting -> {
    System.out.println(greeting);
    return null;
};

times(3, greet, "Hello World!");
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.