Suche…


Einführung

Ein Stream repräsentiert eine Folge von Elementen und unterstützt verschiedene Arten von Operationen, um Berechnungen an diesen Elementen durchzuführen. Mit Java 8 verfügt die Collection Schnittstelle über zwei Methoden zum Generieren eines Stream : stream() und parallelStream() . Stream Operationen sind entweder Zwischen- oder Endgeräte. Zwischenoperationen geben einen Stream sodass mehrere Zwischenoperationen verkettet werden können, bevor der Stream geschlossen wird. Terminalvorgänge sind entweder ungültig oder liefern ein Nicht-Stream-Ergebnis.

Syntax

  • collection.stream ()
  • Arrays.stream (Array)
  • Stream.iterate (ersterWert, aktuellerWert -> nächsterWert)
  • Stream.generate (() -> Wert)
  • Stream.of (elementOfT [, elementOfT, ...])
  • Stream.empty ()
  • StreamSupport.stream (iterable.spliterator (), false)

Streams verwenden

Ein Stream ist eine Folge von Elementen, auf die sequentielle und parallele Aggregatoperationen ausgeführt werden können. Jeder Stream kann möglicherweise eine unbegrenzte Datenmenge durchfließen. Infolgedessen werden Daten, die von einem Stream empfangen werden, bei ihrer Ankunft einzeln verarbeitet, im Gegensatz zur gesamten Stapelverarbeitung der Daten. In Kombination mit Lambda-Ausdrücken bieten sie eine präzise Möglichkeit, Operationen an Datensequenzen mithilfe eines funktionalen Ansatzes auszuführen.

Beispiel: ( siehe Arbeit auf Ideone )

Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");

fruitStream.filter(s -> s.contains("a"))
           .map(String::toUpperCase)
           .sorted()
           .forEach(System.out::println);

Ausgabe:

APFEL
BANANE
ORANGE
BIRNE

Die durch den obigen Code ausgeführten Operationen können wie folgt zusammengefasst werden:

  1. Erstellen Sie einen Stream<String> - Stream String Stream.of(values) Stream<String> enthält eine sequenziert geordneten Stream von Obst String - Elemente unter Verwendung der statischen Factory - Methode Stream.of(values) .

  2. Die filter() Operation behält nur Elemente bei, die mit einem bestimmten Prädikat übereinstimmen (die Elemente, die beim Prädikat getestet werden, geben true zurück). In diesem Fall bleiben die Elemente erhalten, die ein "a" . Das Prädikat wird als Lambda-Ausdruck angegeben .

  3. Die map() Operation transformiert jedes Element mithilfe einer angegebenen Funktion, die als Mapper bezeichnet wird. In diesem Fall wird jeder Obst- String mithilfe der Methodenreferenz String::toUppercase seiner String Version in Großbuchstaben String::toUppercase .

    Beachten Sie, dass die Operation map() einen Stream mit einem anderen generischen Typ zurückgibt, wenn die Zuordnungsfunktion einen anderen Typ als den Eingabeparameter zurückgibt. Zum Beispiel gibt ein Aufruf von .map(String::isEmpty) in einem Stream<Boolean> Stream<String> einen Stream<Boolean>

  4. Die sorted() Operation sortiert die Elemente des Stream entsprechend ihrer natürlichen Reihenfolge (lexikographisch im Fall von String ).

  5. Schließlich führt die Operation forEach(action) eine Aktion aus, die auf jedes Element des Stream einwirkt und an einen Consumer weitergibt . In diesem Beispiel wird jedes Element einfach auf die Konsole gedruckt. Diese Operation ist eine Terminal-Operation, die es unmöglich macht, sie erneut zu bearbeiten.

    Beachten Sie, dass auf dem Stream definierte Operationen aufgrund der Terminaloperation ausgeführt werden. Ohne eine Terminaloperation wird der Stream nicht verarbeitet. Streams können nicht wiederverwendet werden. Sobald eine Terminaloperation aufgerufen wird, wird das Stream Objekt unbrauchbar.

Verkettete Operationen

Operationen (wie oben gezeigt) werden miteinander verkettet und bilden so eine Abfrage der Daten.


Streams schließen

Beachten Sie, dass ein Stream Allgemeinen nicht geschlossen werden muss. Es müssen nur Streams geschlossen werden, die auf E / A-Kanälen arbeiten. Die meisten Stream Typen arbeiten nicht mit Ressourcen und müssen daher nicht geschlossen werden.

Die Stream Schnittstelle erweitert AutoCloseable . Streams können durch Aufrufen der close Methode oder mithilfe von try-with-resource-Anweisungen geschlossen werden.

Ein Beispiel für den Fall, dass ein Stream geschlossen werden soll, ist das Erstellen eines Stream aus Zeilen aus einer Datei:

try (Stream<String> lines = Files.lines(Paths.get("somePath"))) {
    lines.forEach(System.out::println);
}

Das Stream Interface deklariert auch die Stream.onClose() -Methode, mit der Sie Runnable Handler registrieren können, die beim Runnable des Streams aufgerufen werden. Ein Beispiel für einen Anwendungsfall ist, wo Code, der einen Stream erzeugt, wissen muss, wann er zur Bereinigung verbraucht wird.

public Stream<String>streamAndDelete(Path path) throws IOException {
    return Files.lines(path).onClose(() -> someClass.deletePath(path));
}

Der run-Handler wird nur ausgeführt, wenn die close() -Methode explizit oder implizit durch eine try-with-resources-Anweisung aufgerufen wird.


Bearbeitungsauftrag

Die Verarbeitung eines Stream Objekts kann sequentiell oder parallel sein .

In einem sequentiellen Modus werden die Elemente in der Reihenfolge der Quelle des Stream . Wenn der Stream bestellt wird (z. B. eine SortedMap Implementierung oder eine List ), SortedMap die Verarbeitung garantiert mit der Reihenfolge der Quelle überein. In anderen Fällen sollte jedoch darauf geachtet werden, nicht von der Reihenfolge abzuhängen (siehe: keySet() die Java HashMap keySet() Iterationsreihenfolge konsistent? ).

Beispiel:

List<Integer> integerList = Arrays.asList(0, 1, 2, 3, 42); 

// sequential 
long howManyOddNumbers = integerList.stream()
                                    .filter(e -> (e % 2) == 1)
                                    .count(); 

System.out.println(howManyOddNumbers); // Output: 2

Live auf Ideone

Der parallele Modus ermöglicht die Verwendung mehrerer Threads auf mehreren Kernen, es gibt jedoch keine Garantie für die Reihenfolge, in der Elemente verarbeitet werden.

Wenn mehrere Methoden für einen sequentiellen Stream aufgerufen werden, muss nicht jede Methode aufgerufen werden. Wenn beispielsweise ein Stream gefiltert wird und die Anzahl der Elemente auf eins reduziert wird, erfolgt kein nachfolgender Aufruf einer Methode wie sort . Dies kann die Leistung eines sequentiellen Stream erhöhen - eine Optimierung, die mit einem parallelen Stream nicht möglich ist.

Beispiel:

// parallel
long howManyOddNumbersParallel = integerList.parallelStream()
                                            .filter(e -> (e % 2) == 1)
                                            .count();

System.out.println(howManyOddNumbersParallel); // Output: 2

Live auf Ideone


Unterschiede zu Containern (oder Sammlungen )

Während einige Aktionen sowohl für Container als auch für Streams ausgeführt werden können, dienen sie letztendlich unterschiedlichen Zwecken und unterstützen unterschiedliche Operationen. Container konzentrieren sich mehr darauf, wie die Elemente gespeichert werden und wie auf diese Elemente effizient zugegriffen werden kann. Auf der anderen Seite bietet ein Stream keinen direkten Zugriff und keine Manipulation auf seine Elemente. Es ist mehr der Gruppe von Objekten als einer kollektiven Entität gewidmet und führt Operationen an dieser Entität als Ganzes durch. Stream und Collection sind separate Abstraktionen auf hoher Ebene für diese unterschiedlichen Zwecke.

Sammeln Sie Elemente eines Streams in eine Sammlung

Sammeln mit toList() und toSet()

Elemente aus einem Stream können mit der Stream.collect Operation leicht in einem Container Stream.collect werden:

System.out.println(Arrays
    .asList("apple", "banana", "pear", "kiwi", "orange")
    .stream()
    .filter(s -> s.contains("a"))
    .collect(Collectors.toList())
);
// prints: [apple, banana, pear, orange]

Andere Sammlungsinstanzen, wie z. B. ein Set , können mit anderen integrierten Collectors -Methoden erstellt werden. Beispielsweise sammelt Collectors.toSet() die Elemente eines Stream in einem Set .


Explizite Kontrolle über die Implementierung von List oder Set

Gemäß der Dokumentation von Collectors#toList() und Collectors#toSet() gibt es keine Gewähr für Typ, Veränderlichkeit, Serialisierbarkeit oder Collectors#toSet() der zurückgegebenen List oder des Set .

Für die explizite Steuerung der zurückzugebenden Implementierung kann stattdessen Collectors#toCollection(Supplier) verwendet werden, bei der der angegebene Lieferant eine neue und leere Sammlung zurückgibt.

// syntax with method reference
System.out.println(strings
        .stream()
        .filter(s -> s != null && s.length() <= 3)
        .collect(Collectors.toCollection(ArrayList::new))
);

// syntax with lambda
System.out.println(strings
        .stream()
        .filter(s -> s != null && s.length() <= 3)
        .collect(Collectors.toCollection(() -> new LinkedHashSet<>()))
);

Elemente mit toMap sammeln

Der Collector sammelt Elemente in einer Karte, wobei der Schlüssel die Schüler-ID und der Wert den Schülerwert ist.

  List<Student> students = new ArrayList<Student>(); 
    students.add(new Student(1,"test1"));
    students.add(new Student(2,"test2"));
    students.add(new Student(3,"test3"));
    
    Map<Integer, String> IdToName = students.stream()
        .collect(Collectors.toMap(Student::getId, Student::getName));
    System.out.println(IdToName);

Ausgabe :

{1=test1, 2=test2, 3=test3}

Die Collectors.toMap hat eine andere Implementierung: Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) Die mergeFunction wird hauptsächlich verwendet, um entweder einen neuen Wert auszuwählen oder den alten Wert beizubehalten, wenn der Schlüssel wiederholt wird, wenn ein neues Mitglied in der Map aus einer Liste hinzugefügt wird.

Die mergeFunction sieht häufig wie folgt aus: (s1, s2) -> s1 , um den Wert zu erhalten, der dem wiederholten Schlüssel entspricht, oder (s1, s2) -> s2 , um einen neuen Wert für den wiederholten Schlüssel zu setzen.

Elemente zur Karte der Sammlungen sammeln

Beispiel: von ArrayList nach Map <String, List <>>

Häufig muss eine Liste mit einer Liste aus einer Primärliste erstellt werden. Beispiel: Von einem Schüler der Liste müssen wir eine Liste der Themen für jeden Schüler erstellen.

    List<Student> list = new ArrayList<>();
    list.add(new Student("Davis", SUBJECT.MATH, 35.0));
    list.add(new Student("Davis", SUBJECT.SCIENCE, 12.9));
    list.add(new Student("Davis", SUBJECT.GEOGRAPHY, 37.0));

    list.add(new Student("Sascha", SUBJECT.ENGLISH, 85.0));
    list.add(new Student("Sascha", SUBJECT.MATH, 80.0));
    list.add(new Student("Sascha", SUBJECT.SCIENCE, 12.0));
    list.add(new Student("Sascha", SUBJECT.LITERATURE, 50.0));

    list.add(new Student("Robert", SUBJECT.LITERATURE, 12.0));

    Map<String, List<SUBJECT>> map = new HashMap<>();
    list.stream().forEach(s -> {
                map.computeIfAbsent(s.getName(), x -> new ArrayList<>()).add(s.getSubject());
            });
    System.out.println(map);

Ausgabe:

{ Robert=[LITERATURE], 
Sascha=[ENGLISH, MATH, SCIENCE, LITERATURE], 
Davis=[MATH, SCIENCE, GEOGRAPHY] }

Beispiel: von ArrayList nach Map <String, Map <>>

    List<Student> list = new ArrayList<>();
    list.add(new Student("Davis", SUBJECT.MATH, 1, 35.0));
    list.add(new Student("Davis", SUBJECT.SCIENCE, 2, 12.9));
    list.add(new Student("Davis", SUBJECT.MATH, 3, 37.0));
    list.add(new Student("Davis", SUBJECT.SCIENCE, 4, 37.0));

    list.add(new Student("Sascha", SUBJECT.ENGLISH, 5, 85.0));
    list.add(new Student("Sascha", SUBJECT.MATH, 1, 80.0));
    list.add(new Student("Sascha", SUBJECT.ENGLISH, 6, 12.0));
    list.add(new Student("Sascha", SUBJECT.MATH, 3, 50.0));

    list.add(new Student("Robert", SUBJECT.ENGLISH, 5, 12.0));

    Map<String, Map<SUBJECT, List<Double>>> map = new HashMap<>();

    list.stream().forEach(student -> {
        map.computeIfAbsent(student.getName(), s -> new HashMap<>())
                .computeIfAbsent(student.getSubject(), s -> new ArrayList<>())
                .add(student.getMarks());
    });

    System.out.println(map);

Ausgabe:

{ Robert={ENGLISH=[12.0]}, 
Sascha={MATH=[80.0, 50.0], ENGLISH=[85.0, 12.0]}, 
Davis={MATH=[35.0, 37.0], SCIENCE=[12.9, 37.0]} }

Spickzettel

Tor Code
Sammle zu einer List Collectors.toList()
In einer ArrayList mit vorab zugewiesener Größe sammeln Collectors.toCollection(() -> new ArrayList<>(size))
Sammeln Sie zu einem Set Collectors.toSet()
Sammeln Sie zu einem Set mit besserer Iterationsleistung Collectors.toCollection(() -> new LinkedHashSet<>())
Sammeln Sie zu einem Set<String> Groß- und Kleinschreibung nicht Set<String> Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER))
In einem EnumSet<AnEnum> (beste Leistung für Enums) Collectors.toCollection(() -> EnumSet.noneOf(AnEnum.class))
Sammeln Sie auf einer Map<K,V> mit eindeutigen Schlüsseln Collectors.toMap(keyFunc,valFunc)
Ordnen Sie MyObject.getter () einem eindeutigen MyObject zu Collectors.toMap(MyObject::getter, Function.identity())
Ordnen Sie MyObject.getter () mehreren MyObjects zu Collectors.groupingBy(MyObject::getter)

Unendlich Streams

Es ist möglich, einen Stream zu generieren, der nicht beendet wird. Wenn Sie eine Terminal-Methode für einen infinite Stream aufrufen, tritt der Stream in eine Endlosschleife ein. Die limit Methode eines Stream kann verwendet werden, um die Anzahl der Terme des Stream zu begrenzen, die Java verarbeitet.

In diesem Beispiel wird ein Stream aller natürlichen Zahlen generiert, beginnend mit der Zahl 1. Jeder nachfolgende Begriff des Stream ist um eins höher als der vorherige. Beim Aufruf der Limit-Methode dieses Stream werden nur die ersten fünf Terme des Stream berücksichtigt und gedruckt.

// Generate infinite stream - 1, 2, 3, 4, 5, 6, 7, ...
IntStream naturalNumbers = IntStream.iterate(1, x -> x + 1);

// Print out only the first 5 terms
naturalNumbers.limit(5).forEach(System.out::println);

Ausgabe:

1
2
3
4
5


Eine andere Methode zum Erzeugen eines unendlichen Streams besteht in der Verwendung der Stream.generate- Methode. Diese Methode benötigt ein Lambda vom Typ Supplier .

// Generate an infinite stream of random numbers
Stream<Double> infiniteRandomNumbers = Stream.generate(Math::random);

// Print out only the first 10 random numbers
infiniteRandomNumbers.limit(10).forEach(System.out::println);

Verbrauchende Streams

Ein Stream wird nur dann durchlaufen, wenn eine Terminaloperation wie count() , collect() oder forEach() . Andernfalls wird keine Operation für den Stream ausgeführt.

Im folgenden Beispiel wird dem Stream keine Terminaloperation hinzugefügt, sodass die filter() Operation nicht aufgerufen wird und keine Ausgabe erzeugt wird, da peek() keine Terminaloperation ist .

IntStream.range(1, 10).filter(a -> a % 2 == 0).peek(System.out::println);

Live auf Ideone

Dies ist eine Stream Sequenz mit einem gültigen Terminalbetrieb , daher wird eine Ausgabe erzeugt.

Sie können auch forEach anstelle von peek :

IntStream.range(1, 10).filter(a -> a % 2 == 0).forEach(System.out::println); 

Live auf Ideone

Ausgabe:

2
4
6
8

Nachdem der Terminalbetrieb ausgeführt wurde, wird der Stream verbraucht und kann nicht wiederverwendet werden.


Obwohl ein bestimmtes Stream-Objekt nicht wiederverwendet werden kann, ist es einfach, eine wiederverwendbare Iterable zu erstellen, die an eine Stream-Pipeline delegiert. Dies kann nützlich sein, um eine modifizierte Ansicht eines Live-Datensatzes zurückzugeben, ohne Ergebnisse in einer temporären Struktur sammeln zu müssen.

List<String> list = Arrays.asList("FOO", "BAR");
Iterable<String> iterable = () -> list.stream().map(String::toLowerCase).iterator();

for (String str : iterable) {
    System.out.println(str);
}
for (String str : iterable) {
    System.out.println(str);
}

Ausgabe:

foo
Bar
foo
Bar

Dies funktioniert, weil Iterable eine einzige abstrakte Methode Iterator<T> iterator() deklariert. Das macht es zu einer funktionellen Schnittstelle, die von einem Lambda implementiert wird, das bei jedem Aufruf einen neuen Stream erstellt.


Im Allgemeinen funktioniert ein Stream wie in der folgenden Abbildung dargestellt:

Stream-Betrieb

HINWEIS : Argumentprüfungen werden immer ausgeführt, auch ohne Terminaloperation :

try {
    IntStream.range(1, 10).filter(null);
} catch (NullPointerException e) {
    System.out.println("We got a NullPointerException as null was passed as an argument to filter()");
}

Live auf Ideone

Ausgabe:

Wir haben eine NullPointerException erhalten, da null als Argument an filter () übergeben wurde.

Frequenzkarte erstellen

Der groupingBy(classifier, downstream) -Kollektor ermöglicht das Sammeln von Stream Elementen in einer Map indem jedes Element in einer Gruppe klassifiziert und ein Downstream-Vorgang für die Elemente ausgeführt wird, die in derselben Gruppe klassifiziert sind.

Ein klassisches Beispiel für dieses Prinzip ist die Verwendung einer Map um die Vorkommen von Elementen in einem Stream . In diesem Beispiel ist der Klassifizierer einfach die Identitätsfunktion, die das Element unverändert zurückgibt. Der Downstream-Vorgang zählt die Anzahl der gleichen Elemente mithilfe von counting() .

Stream.of("apple", "orange", "banana", "apple")
      .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
      .entrySet()
      .forEach(System.out::println);

Die nachgelagerte Operation ist selbst ein Collector ( Collectors.counting() ), der Elemente des Typs String verarbeitet und ein Ergebnis des Typs Long . Das Ergebnis des Aufrufs der collect Methode ist Map<String, Long> .

Dies würde die folgende Ausgabe erzeugen:

Banane = 1
orange = 1
Apfel = 2

Paralleler Stream

Hinweis: Bevor Sie sich für einen Stream , sollten Sie sich das Verhalten von ParallelStream vs. Sequential Stream anschauen.

Wenn Sie Stream Vorgänge gleichzeitig ausführen möchten, können Sie eine dieser Methoden verwenden.

List<String> data = Arrays.asList("One", "Two", "Three", "Four", "Five");
Stream<String> aParallelStream = data.stream().parallel();

Oder:

Stream<String> aParallelStream = data.parallelStream();

Rufen Sie einen Terminaloperator auf, um die für den parallelen Stream definierten Vorgänge auszuführen:

aParallelStream.forEach(System.out::println);

(Eine mögliche) Ausgabe vom parallelen Stream :

Drei
Vier
Ein
Zwei
Fünf

Die Reihenfolge kann sich ändern, da alle Elemente parallel verarbeitet werden (was sie möglicherweise schneller macht). Verwenden Sie parallelStream wenn die Bestellung keine Rolle spielt.

Auswirkungen auf die Leistung

Wenn es sich um Netzwerke handelt, können parallele Stream die Gesamtleistung einer Anwendung beeinträchtigen, da alle parallelen Stream einen gemeinsamen Fork-Join-Thread-Pool für das Netzwerk verwenden.

Auf der anderen Seite können parallele Stream die Leistung in vielen anderen Fällen erheblich verbessern, abhängig von der Anzahl der verfügbaren Kerne in der laufenden CPU.

Konvertieren eines optionalen Streams in einen Wertestrom

Möglicherweise müssen Sie einen Stream , der Optional in einen Stream von Werten konvertieren und nur Werte aus dem vorhandenen Optional . (dh ohne null und nicht mit Optional.empty() ).

 Optional<String> op1 = Optional.empty();
 Optional<String> op2 = Optional.of("Hello World");

 List<String> result = Stream.of(op1, op2)
                             .filter(Optional::isPresent)
                             .map(Optional::get)
                             .collect(Collectors.toList());

 System.out.println(result); //[Hello World]

Stream erstellen

Alle java Collection<E> verfügen über die Methoden stream() und parallelStream() , aus denen ein Stream<E> erstellt werden kann:

Collection<String> stringList = new ArrayList<>();
Stream<String> stringStream = stringList.parallelStream();

Ein Stream<E> kann mit einem von zwei Verfahren aus einem Array erstellt werden:

String[] values = { "aaa", "bbbb", "ddd", "cccc" };
Stream<String> stringStream = Arrays.stream(values);
Stream<String> stringStreamAlternative = Stream.of(values);

Der Unterschied zwischen Arrays.stream() und Stream.of() besteht darin, dass Stream.of() einen varargs -Parameter besitzt, sodass er folgendermaßen verwendet werden kann:

Stream<Integer> integerStream = Stream.of(1, 2, 3);

Es gibt auch primitive Stream , die Sie verwenden können. Zum Beispiel:

IntStream intStream = IntStream.of(1, 2, 3);
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);

Diese primitiven Streams können auch mit der Arrays.stream() -Methode erstellt werden:

IntStream intStream = Arrays.stream(new int[]{ 1, 2, 3 });

Es ist möglich, einen Stream aus einem Array mit einem angegebenen Bereich zu erstellen.

int[] values= new int[]{1, 2, 3, 4, 5};
IntStream intStram = Arrays.stream(values, 1, 3);

Beachten Sie, dass jeder primitive Stream mit der boxed Methode in einen Boxed-Type-Stream konvertiert werden kann:

Stream<Integer> integerStream = intStream.boxed();

Dies kann in einigen Fällen hilfreich sein, wenn Sie die Daten erfassen möchten, da der primitive Stream keine collect , für die ein Collector als Argument verwendet wird.

Wiederverwenden von Zwischenvorgängen einer Stromkette

Der Stream wird geschlossen, wenn der Terminalbetrieb aufgerufen wird. Wiederverwenden des Stroms von Zwischenvorgängen, wenn nur der Terminalbetrieb nur variiert. Wir könnten einen Stream-Anbieter erstellen, um einen neuen Stream mit allen bereits eingerichteten Zwischenvorgängen aufzubauen.

Supplier<Stream<String>> streamSupplier = () -> Stream.of("apple", "banana","orange", "grapes", "melon","blueberry","blackberry")
.map(String::toUpperCase).sorted();

  streamSupplier.get().filter(s ->   s.startsWith("A")).forEach(System.out::println);

// APPLE

  streamSupplier.get().filter(s -> s.startsWith("B")).forEach(System.out::println);

  // BANANA
  // BLACKBERRY
  // BLUEBERRY

int[] Arrays können mit Streams in List<Integer> konvertiert werden

int[] ints = {1,2,3};
List<Integer> list = IntStream.of(ints).boxed().collect(Collectors.toList());

Suchen von Statistiken zu numerischen Streams

Java 8 stellt Klassen genannt IntSummaryStatistics , DoubleSummaryStatistics und LongSummaryStatistics , die ein Statusobjekt für die Statistik wie geben sammeln count , min , max , sum und average .

Java SE 8
List<Integer> naturalNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
IntSummaryStatistics stats = naturalNumbers.stream()
                                           .mapToInt((x) -> x)
                                           .summaryStatistics();
System.out.println(stats);

Was führt zu:

Java SE 8
IntSummaryStatistics{count=10, sum=55, min=1, max=10, average=5.500000}

Holen Sie sich ein Stück Strom

Beispiel: Holen Sie sich einen Stream mit 30 Elementen, der das 21. bis einschließlich 50. Element einer Sammlung enthält.

final long n = 20L; // the number of elements to skip
final long maxSize = 30L; // the number of elements the stream should be limited to
final Stream<T> slice = collection.stream().skip(n).limit(maxSize);

Anmerkungen:

  • IllegalArgumentException wird ausgelöst, wenn n negativ ist oder maxSize negativ ist
  • Sowohl skip(long) als auch limit(long) sind Zwischenoperationen
  • Wenn ein Stream weniger als n Elemente enthält, gibt skip(n) einen leeren Stream zurück
  • Sowohl skip(long) als auch limit(long) sind billige Vorgänge bei sequentiellen Stream-Pipelines, können jedoch bei geordneten parallelen Pipelines recht teuer sein

Streams verketten

Variablendeklaration für Beispiele:

Collection<String> abc = Arrays.asList("a", "b", "c");
Collection<String> digits = Arrays.asList("1", "2", "3");
Collection<String> greekAbc = Arrays.asList("alpha", "beta", "gamma");

Beispiel 1 - Verketten Sie zwei Stream

final Stream<String> concat1 = Stream.concat(abc.stream(), digits.stream());

concat1.forEach(System.out::print);
// prints: abc123

Beispiel 2 - Verketten Sie mehr als zwei Stream

final Stream<String> concat2 = Stream.concat(
    Stream.concat(abc.stream(), digits.stream()),
    greekAbc.stream());

System.out.println(concat2.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma

Alternativ zur Vereinfachung der verschachtelten concat() die Stream auch mit flatMap() verkettet werden:

final Stream<String> concat3 = Stream.of(
    abc.stream(), digits.stream(), greekAbc.stream())
    .flatMap(s -> s);
    // or `.flatMap(Function.identity());` (java.util.function.Function)

System.out.println(concat3.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma

Seien Sie beim Stream von Stream aus wiederholten Verkettungen vorsichtig, da der Zugriff auf ein Element eines tief verketteten Stream zu tiefen Aufrufketten oder sogar zu einer StackOverflowException .

IntStream to String

Java verfügt nicht über einen Char-Stream . Wenn Sie also mit String s arbeiten und einen Stream von Character IntStream , besteht die Option, einen IntStream von Codepunkten mithilfe der String.codePoints() -Methode String.codePoints() . So ist IntStream wie IntStream :

public IntStream stringToIntStream(String in) {
  return in.codePoints();
}

Es ist etwas komplizierter, die Konvertierung anders durchzuführen, z. B. IntStreamToString. Das kann wie folgt gemacht werden:

public String intStreamToString(IntStream intStream) {
  return intStream.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
}

Sortieren mit Stream

List<String> data = new ArrayList<>();
data.add("Sydney");
data.add("London");
data.add("New York");
data.add("Amsterdam");
data.add("Mumbai");
data.add("California");

System.out.println(data);

List<String> sortedData = data.stream().sorted().collect(Collectors.toList());

System.out.println(sortedData);

Ausgabe:

[Sydney, London, New York, Amsterdam, Mumbai, California]
[Amsterdam, California, London, Mumbai, New York, Sydney]

Es ist auch möglich, einen anderen Vergleichsmechanismus zu verwenden, da es eine überladene sorted Version gibt, die einen Vergleicher als Argument verwendet.

Sie können auch einen Lambda-Ausdruck zum Sortieren verwenden:

List<String> sortedData2 = data.stream().sorted((s1,s2) -> s2.compareTo(s1)).collect(Collectors.toList());

Dies würde [Sydney, New York, Mumbai, London, California, Amsterdam] ausgeben.

Sie können Comparator.reverseOrder() , um einen Komparator zu haben, der die reverse der natürlichen Reihenfolge durchführt.

List<String> reverseSortedData = data.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());

Ströme von Primitiven

Java bietet spezialisierte Stream für drei Arten von IntStream : IntStream (für int ), LongStream (für long s) und DoubleStream (für double s). Sie sind nicht nur optimierte Implementierungen für ihre jeweiligen Grundelemente, sondern bieten auch mehrere spezifische Terminalmethoden, typischerweise für mathematische Operationen. Z.B:

IntStream is = IntStream.of(10, 20, 30);
double average = is.average().getAsDouble(); // average is 20.0

Sammeln Sie die Ergebnisse eines Streams in einem Array

Analog, um eine Sammlung für einen Stream durch collect() zu erhalten, kann ein Array mit der Stream.toArray() -Methode erhalten werden:

List<String> fruits = Arrays.asList("apple", "banana", "pear", "kiwi", "orange");

String[] filteredFruits = fruits.stream()
    .filter(s -> s.contains("a"))
    .toArray(String[]::new);     

// prints: [apple, banana, pear, orange]
System.out.println(Arrays.toString(filteredFruits));

String[]::new ist eine spezielle Art von Methodenreferenz: eine Konstruktorreferenz.

Das erste Element finden, das einem Prädikat entspricht

Es ist möglich, das erste Element eines Stream zu finden, das einer Bedingung entspricht.

In diesem Beispiel finden wir die erste Integer deren Quadrat über 50000 .

IntStream.iterate(1, i -> i + 1) // Generate an infinite stream 1,2,3,4...
    .filter(i -> (i*i) > 50000) // Filter to find elements where the square is >50000
    .findFirst(); // Find the first filtered element

Dieser Ausdruck gibt ein OptionalInt mit dem Ergebnis zurück.

Beachten Sie, dass Java bei einem unendlichen Stream jedes Element so lange überprüft, bis es ein Ergebnis findet. Bei einem endlichen Stream gibt Java ein leeres OptionalInt zurück, wenn ihm die Elemente ausgehen, das Ergebnis jedoch nicht gefunden wird.

Verwenden von IntStream zum Durchlaufen von Indizes

Stream von Elementen erlauben normalerweise keinen Zugriff auf den Indexwert des aktuellen Elements. Verwenden Sie IntStream.range(start, endExclusive) um über ein Array oder eine ArrayList zu iterieren, während Sie auf Indizes IntStream.range(start, endExclusive) .

String[] names = { "Jon", "Darin", "Bauke", "Hans", "Marc" };

IntStream.range(0, names.length)
    .mapToObj(i -> String.format("#%d %s", i + 1, names[i]))
    .forEach(System.out::println);

Die range(start, endExclusive) -Methode gibt einen anderen ÌntStream und der mapToObj(mapper) gibt einen Stream von String .

Ausgabe:

# 1 Jon
# 2 Darin
# 3 Bauke
# 4 Hans
# 5 Marc

Dies ist der Verwendung einer normalen for Schleife mit einem Zähler sehr ähnlich, jedoch mit dem Vorteil des Pipelining und der Parallelisierung:

for (int i = 0; i < names.length; i++) {
    String newName = String.format("#%d %s", i + 1, names[i]);
    System.out.println(newName);
}

Flachen von Streams mit flatMap ()

Ein Stream von Elementen, die wiederum streamfähig sind, kann zu einem einzigen kontinuierlichen Stream :

Das Array mit der Liste der Elemente kann in eine einzige Liste konvertiert werden.

List<String> list1 =  Arrays.asList("one", "two");
      List<String> list2 =  Arrays.asList("three","four","five");
      List<String> list3 =  Arrays.asList("six");
          List<String> finalList = Stream.of(list1, list2, list3).flatMap(Collection::stream).collect(Collectors.toList());
System.out.println(finalList);

// [one, two, three, four, five, six]

Map mit der Liste der Elemente als Werte kann auf eine kombinierte Liste reduziert werden

Map<String, List<Integer>> map = new LinkedHashMap<>();
map.put("a", Arrays.asList(1, 2, 3));
map.put("b", Arrays.asList(4, 5, 6));

List<Integer> allValues = map.values() // Collection<List<Integer>>
        .stream()                      // Stream<List<Integer>>
        .flatMap(List::stream)         // Stream<Integer>
        .collect(Collectors.toList());

System.out.println(allValues);
// [1, 2, 3, 4, 5, 6]

List der Map kann in einen einzigen kontinuierlichen Stream

List<Map<String, String>> list = new ArrayList<>();
Map<String,String> map1 = new HashMap();
map1.put("1", "one");
map1.put("2", "two");

Map<String,String> map2 = new HashMap();
map2.put("3", "three");
map2.put("4", "four");
list.add(map1);
list.add(map2);


Set<String> output= list.stream()  //  Stream<Map<String, String>>
    .map(Map::values)              // Stream<List<String>>
    .flatMap(Collection::stream)   // Stream<String>
    .collect(Collectors.toSet());  //Set<String>
// [one, two, three,four]

Erstellen Sie eine Karte basierend auf einem Stream

Einfacher Fall ohne doppelte Schlüssel

Stream<String> characters = Stream.of("A", "B", "C");

Map<Integer, String> map = characters
            .collect(Collectors.toMap(element -> element.hashCode(), element -> element));
// map = {65=A, 66=B, 67=C}

Um die Dinge deklarativer zu gestalten, können wir in Function interface - Function.identity() statische Methode verwenden. Wir können dieses Lambda- element -> element durch Function.identity() ersetzen.

Fall, in dem möglicherweise doppelte Schlüssel vorhanden sind

Der Javadoc für Collectors.toMap gibt Folgendes an:

Wenn die zugeordneten Schlüssel Duplikate enthalten (gemäß Object.equals(Object) ), wird eine IllegalStateException ausgelöst, wenn der Auflistungsvorgang ausgeführt wird. Wenn die zugeordneten Tasten Duplikate enthalten können, verwenden toMap(Function, Function, BinaryOperator) stattdessen toMap(Function, Function, BinaryOperator) .

Stream<String> characters = Stream.of("A", "B", "B", "C");

Map<Integer, String> map = characters
            .collect(Collectors.toMap(
                element -> element.hashCode(),
                element -> element,
                (existingVal, newVal) -> (existingVal + newVal)));

// map = {65=A, 66=BB, 67=C}

Der an Collectors.toMap(...) BinaryOperator generiert den Wert, der bei einer Kollision gespeichert werden soll. Es kann:

  • den alten Wert zurückgeben, sodass der erste Wert im Stream Vorrang hat,
  • den neuen Wert zurückgeben, damit der letzte Wert im Stream Vorrang hat, oder
  • kombinieren Sie die alten und neuen Werte

Gruppierung nach Wert

Sie können Collectors.groupingBy wenn Sie das Äquivalent einer kaskadierten "group by" -Operation der Datenbank ausführen müssen. Zur Veranschaulichung wird im Folgenden eine Karte erstellt, in der die Namen der Personen den Nachnamen zugeordnet werden:

List<Person> people = Arrays.asList(
    new Person("Sam", "Rossi"),
    new Person("Sam", "Verdi"),
    new Person("John", "Bianchi"),
    new Person("John", "Rossi"),
    new Person("John", "Verdi")
);

Map<String, List<String>> map = people.stream()
        .collect(
                // function mapping input elements to keys
                Collectors.groupingBy(Person::getName, 
                // function mapping input elements to values,
                // how to store values
                Collectors.mapping(Person::getSurname, Collectors.toList()))
        );

// map = {John=[Bianchi, Rossi, Verdi], Sam=[Rossi, Verdi]}

Live auf Ideone

Zufällige Strings mit Streams erzeugen

Es ist manchmal nützlich, zufällige Strings zu erstellen, z. B. als Sitzungs-ID für einen Web-Service oder als Initialkennwort nach der Registrierung für eine Anwendung. Dies kann leicht mit Stream s erreicht werden.

Zuerst müssen wir einen Zufallszahlengenerator initialisieren. Zur Erhöhung der Sicherheit für den erzeugten String s, ist es eine gute Idee zu verwenden SecureRandom .

Hinweis : Das Erstellen eines SecureRandom ist ziemlich teuer. SecureRandom es sich, dies nur einmal zu tun und setSeed() eine seiner setSeed() Methoden aufzurufen, um es neu zu bestimmen.

private static final SecureRandom rng = new SecureRandom(SecureRandom.generateSeed(20)); 
//20 Bytes as a seed is rather arbitrary, it is the number used in the JavaDoc example

Bei der Erstellung von zufälligen String s möchten wir normalerweise, dass sie nur bestimmte Zeichen (z. B. nur Buchstaben und Ziffern) verwenden. Daher können wir eine Methode erstellen, die einen boolean der später zum Filtern des Stream .

//returns true for all chars in 0-9, a-z and A-Z
boolean useThisCharacter(char c){
    //check for range to avoid using all unicode Letter (e.g. some chinese symbols)
    return c >= '0' && c <= 'z' && Character.isLetterOrDigit(c);
}

Als Nächstes können wir das RNG verwenden, um einen zufälligen String mit einer bestimmten Länge zu generieren, der den Zeichensatz enthält, der unsere useThisCharacter Prüfung besteht.

public String generateRandomString(long length){
    //Since there is no native CharStream, we use an IntStream instead
    //and convert it to a Stream<Character> using mapToObj.
    //We need to specify the boundaries for the int values to ensure they can safely be cast to char
    Stream<Character> randomCharStream = rng.ints(Character.MIN_CODE_POINT, Character.MAX_CODE_POINT).mapToObj(i -> (char)i).filter(c -> this::useThisCharacter).limit(length);

    //now we can use this Stream to build a String utilizing the collect method.
    String randomString = randomCharStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
    return randomString;
}

Verwenden von Streams zum Implementieren mathematischer Funktionen

Stream und vor allem IntStream sind eine elegante Möglichkeit, Summationsterme (∑) zu implementieren. Die Bereiche des Stream können als Grenzen der Summation verwendet werden.

Madhavas Annäherung an Pi ist beispielsweise durch die Formel gegeben (Quelle: wikipedia ): Madhavas Annäherung

Dies kann mit einer beliebigen Genauigkeit berechnet werden. ZB für 101 Begriffe:

double pi = Math.sqrt(12) * 
            IntStream.rangeClosed(0, 100)
                     .mapToDouble(k -> Math.pow(-3, -1 * k) / (2 * k + 1))
                     .sum();

Hinweis: Mit der double Genauigkeit genügt die Auswahl einer oberen Grenze von 29, um ein Ergebnis zu Math.Pi , das sich von Math.Pi .

Verwenden von Streams und Methodenreferenzen zum Schreiben selbstdokumentierender Prozesse

Methodenreferenzen machen ausgezeichneten selbstdokumentierenden Code, und die Verwendung von Methodenreferenzen mit Stream s macht komplizierte Prozesse leicht lesbar und verständlich. Betrachten Sie den folgenden Code:

public interface Ordered {
    default int getOrder(){
        return 0;
    }
}

public interface Valued<V extends Ordered> {
    boolean hasPropertyTwo();
    V getValue();
}

public interface Thing<V extends Ordered> {
    boolean hasPropertyOne();
    Valued<V> getValuedProperty();
}

public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
    List<V> results = new ArrayList<V>();
    for (Thing<V> thing : things) {
        if (thing.hasPropertyOne()) {
            Valued<V> valued = thing.getValuedProperty();
            if (valued != null && valued.hasPropertyTwo()){
                V value = valued.getValue();
                if (value != null){
                    results.add(value);
                }
            }
        }
    }
    results.sort((a, b)->{
        return Integer.compare(a.getOrder(), b.getOrder());
    });
    return results;
}

Diese letzte mit Stream und Methodenreferenzen neu geschriebene Methode ist viel lesbarer und jeder Schritt des Prozesses ist schnell und einfach zu verstehen. Sie ist nicht nur kürzer, sondern zeigt auf einen Blick, welche Schnittstellen und Klassen in jedem Schritt für den Code verantwortlich sind:

public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
    return things.stream()
        .filter(Thing::hasPropertyOne)
        .map(Thing::getValuedProperty)
        .filter(Objects::nonNull)
        .filter(Valued::hasPropertyTwo)
        .map(Valued::getValue)
        .filter(Objects::nonNull)
        .sorted(Comparator.comparing(Ordered::getOrder))
        .collect(Collectors.toList());
}    

Verwenden von Streams of Map.Entry zum Erhalten der Anfangswerte nach dem Mapping

Wenn Sie einen Stream haben, den Sie Map.Entry<K,V> müssen, die ursprünglichen Werte aber auch beibehalten möchten, können Sie den Stream einer Map.Entry<K,V>

public static <K, V> Function<K, Map.Entry<K, V>> entryMapper(Function<K, V> mapper){
    return (k)->new AbstractMap.SimpleEntry<>(k, mapper.apply(k));
}

Dann können Sie Ihren Konverter verwenden, um Stream zu verarbeiten, die sowohl auf die ursprünglichen als auch auf die zugeordneten Werte zugreifen können:

Set<K> mySet;
Function<K, V> transformer = SomeClass::transformerMethod;
Stream<Map.Entry<K, V>> entryStream = mySet.stream()
    .map(entryMapper(transformer));

Sie können diesen Stream wie gewohnt weiterverarbeiten. Dies vermeidet den Aufwand für das Erstellen einer Zwischensammlung.

Stream-Betriebskategorien

Streamoperationen lassen sich in zwei Hauptkategorien, Zwischen- und Terminaloperationen, und zwei Unterkategorien, zustandslos und zustandsbehaftet, unterteilen.


Zwischenoperationen:

Eine Zwischenoperation ist immer faul , wie eine einfache Stream.map . Sie wird erst aufgerufen, wenn der Stream tatsächlich verbraucht ist. Dies kann leicht überprüft werden:

Arrays.asList(1, 2 ,3).stream().map(i -> {
    throw new RuntimeException("not gonna happen");
    return i;
});

Zwischenoperationen sind die allgemeinen Bausteine ​​eines Streams, die nach der Quelle verkettet sind, und normalerweise folgt eine Terminaloperation, die die Streamkette auslöst.


Terminalbetrieb

Terminalvorgänge lösen den Verbrauch eines Streams aus. Einige der häufigsten sind Stream.forEach oder Stream.collect . Sie werden normalerweise nach einer Kette von Zwischenoperationen angeordnet und sind fast immer eifrig .


Zustandslose Operationen

Zustandslosigkeit bedeutet, dass jeder Artikel ohne den Kontext anderer Artikel verarbeitet wird. Zustandslose Operationen ermöglichen eine speichereffiziente Verarbeitung von Streams. Vorgänge wie Stream.map und Stream.filter , für die keine Informationen zu anderen Elementen des Streams erforderlich sind, werden als zustandslos betrachtet.


Stateful Operationen

Statefulness bedeutet, dass die Operation für jedes Element von (einigen) anderen Elementen des Streams abhängt. Dies erfordert, dass ein Zustand erhalten bleibt. Statefulness-Operationen können mit langen oder unendlichen Streams brechen. Vorgänge wie Stream.sorted erfordern die Stream.sorted des gesamten Streams, bevor ein Element Stream.sorted wird, das einen ausreichend langen Stream von Elementen Stream.sorted . Dies kann durch einen langen Stream ( auf eigenes Risiko ) gezeigt werden:

// works - stateless stream
long BIG_ENOUGH_NUMBER = 999999999;
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).forEach(System.out::println);

Dies führt aufgrund von Stream.sorted von Stream.sorted zu einem Out-of-Memory.

// Out of memory - stateful stream
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).sorted().forEach(System.out::println);

Konvertierung eines Iterators in einen Stream

Verwenden Sie Spliterators.spliterator() oder Spliterators.spliteratorUnknownSize() , um einen Iterator in einen Stream zu konvertieren:

Iterator<String> iterator = Arrays.asList("A", "B", "C").iterator();    
Spliterator<String> spliterator = Spliterators.spliteratorUnknownSize(iterator, 0);
Stream<String> stream = StreamSupport.stream(spliterator, false);

Reduktion mit Streams

Reduktion ist der Vorgang, bei dem ein binärer Operator auf jedes Element eines Streams angewendet wird, um einen Wert zu erhalten.

Die sum() Methode eines IntStream ist ein Beispiel für eine Reduzierung. es wird zusätzlich auf jeden Begriff des Streams angewendet, was zu einem endgültigen Wert führt: Summe Reduktion

Dies ist äquivalent zu (((1+2)+3)+4)

Die reduce eines Streams ermöglicht das Erstellen einer benutzerdefinierten Reduzierung. Es ist möglich, die reduce Methode zum Implementieren der sum() -Methode zu verwenden:

IntStream istr;
    
//Initialize istr
    
OptionalInt istr.reduce((a,b)->a+b);

Die Optional Version wird zurückgegeben, damit leere Streams entsprechend behandelt werden können.

Ein weiteres Beispiel für die Reduktion ist die Kombination einer Stream<LinkedList<T>> in einer einzelnen LinkedList<T> :

Stream<LinkedList<T>> listStream;
    
//Create a Stream<LinkedList<T>>
    
Optional<LinkedList<T>> bigList = listStream.reduce((LinkedList<T> list1, LinkedList<T> list2)->{
    LinkedList<T> retList = new LinkedList<T>();
    retList.addAll(list1);
    retList.addAll(list2);
    return retList;
});

Sie können auch ein Identitätselement angeben . Beispielsweise ist das Identitätselement für die Addition 0, da x+0==x . Für die Multiplikation ist das Identitätselement 1, da x*1==x . Im obigen Fall handelt es sich bei dem Identitätselement um eine leere LinkedList<T> . Wenn Sie einer anderen Liste eine leere Liste hinzufügen, ändert sich die Liste, zu der Sie "hinzufügen", nicht:

Stream<LinkedList<T>> listStream;

//Create a Stream<LinkedList<T>>

LinkedList<T> bigList = listStream.reduce(new LinkedList<T>(), (LinkedList<T> list1, LinkedList<T> list2)->{
    LinkedList<T> retList = new LinkedList<T>();
    retList.addAll(list1);
    retList.addAll(list2);
    return retList;
});

Wenn ein Identitätselement bereitgestellt wird, wird der Rückgabewert nicht in ein Optional Element eingeschlossen. Wenn dieses Element in einem leeren Stream aufgerufen wird, wird das Identitätselement von reduz reduce() zurückgegeben.

Der binäre Operator muss auch assoziativ sein , dh (a+b)+c==a+(b+c) . Dies liegt daran, dass die Elemente in beliebiger Reihenfolge reduziert werden können. Die obige Additionsreduktion könnte beispielsweise folgendermaßen durchgeführt werden:

Andere Summenreduzierung

Diese Reduktion entspricht dem Schreiben ((1+2)+(3+4)) . Die Eigenschaft der Assoziativität ermöglicht es auch, dass Java den Stream parallel reduziert. Ein Teil des Streams kann von jedem Prozessor reduziert werden, wobei das Ergebnis jedes Prozessors am Ende kombiniert wird.

Verknüpfen eines Streams mit einem einzelnen String

Ein häufig auftretender Anwendungsfall ist das Erstellen eines String aus einem Stream, bei dem die Stream-Elemente durch ein bestimmtes Zeichen getrennt werden. Collectors.joining() kann die Collectors.joining() -Methode verwendet werden, wie im folgenden Beispiel:

Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");

String result = fruitStream.filter(s -> s.contains("a"))
           .map(String::toUpperCase)
           .sorted()
           .collect(Collectors.joining(", "));
           
System.out.println(result);

Ausgabe:

Apfel, Banane, Orange, Birne

Die Collectors.joining() -Methode kann auch für Prä- und Postfixes sorgen:

String result = fruitStream.filter(s -> s.contains("e"))
           .map(String::toUpperCase)
           .sorted()
           .collect(Collectors.joining(", ", "Fruits: ", "."));
           
System.out.println(result);

Ausgabe:

Früchte: APPLE, ORANGE, BIRN.

Live auf Ideone



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow