Einführung in Spliterator in Java

1. Übersicht

Die in Java 8 eingeführte Spliterator- Schnittstelle kann zum Durchlaufen und Partitionieren von Sequenzen verwendet werden . Es ist ein Basisdienstprogramm für Streams , insbesondere für parallele.

In diesem Artikel werden die Verwendung, Eigenschaften, Methoden und das Erstellen eigener benutzerdefinierter Implementierungen beschrieben.

2. Spliterator- API

2.1. tryAdvance

Dies ist die Hauptmethode zum Durchlaufen einer Sequenz. Die Methode verwendet einen Consumer , der verwendet wird, um Elemente des Spliterators nacheinander zu verbrauchen, und gibt false zurück, wenn keine zu durchlaufenden Elemente vorhanden sind.

Hier sehen wir uns an, wie Sie damit Elemente durchlaufen und partitionieren können.

Nehmen wir zunächst an, wir haben eine ArrayList mit 35000 Artikeln und die Article- Klasse ist definiert als:

public class Article { private List listOfAuthors; private int id; private String name; // standard constructors/getters/setters }

Implementieren wir nun eine Aufgabe, die die Artikelliste verarbeitet und jedem Artikelnamen das Suffix " - veröffentlicht von Baeldung" hinzufügt :

public String call() { int current = 0; while (spliterator.tryAdvance(a -> a.setName(article.getName() .concat("- published by Baeldung")))) { current++; } return Thread.currentThread().getName() + ":" + current; }

Beachten Sie, dass diese Aufgabe die Anzahl der verarbeiteten Artikel ausgibt, wenn die Ausführung abgeschlossen ist.

Ein weiterer wichtiger Punkt ist, dass wir die tryAdvance () -Methode verwendet haben, um das nächste Element zu verarbeiten.

2.2. trySplit

Als nächstes teilen wir Spliterators (daher der Name) und verarbeiten Partitionen unabhängig voneinander.

Die trySplit- Methode versucht, sie in zwei Teile aufzuteilen. Dann verarbeitet der Aufrufer die Elemente und schließlich die zurückgegebene Instanz die anderen, sodass die beiden parallel verarbeitet werden können.

Lassen Sie uns zuerst unsere Liste erstellen:

public static List generateElements() { return Stream.generate(() -> new Article("Java")) .limit(35000) .collect(Collectors.toList()); }

Als nächstes erhalten wir unsere Spliterator- Instanz mit der spliterator () -Methode. Dann wenden wir unsere trySplit () -Methode an:

@Test public void givenSpliterator_whenAppliedToAListOfArticle_thenSplittedInHalf() { Spliterator split1 = Executor.generateElements().spliterator(); Spliterator split2 = split1.trySplit(); assertThat(new Task(split1).call()) .containsSequence(Executor.generateElements().size() / 2 + ""); assertThat(new Task(split2).call()) .containsSequence(Executor.generateElements().size() / 2 + ""); }

Der Aufteilungsprozess verlief wie beabsichtigt und teilte die Datensätze gleichmäßig auf .

2.3. geschätzte Größe

Die geschätzte Größenmethode gibt uns eine geschätzte Anzahl von Elementen:

LOG.info("Size: " + split1.estimateSize());

Dies wird Folgendes ausgeben:

Size: 17500

2.4. hasCharacteristics

Diese API prüft, ob die angegebenen Eigenschaften mit den Eigenschaften des Spliterators übereinstimmen. Wenn wir dann die obige Methode aufrufen, ist die Ausgabe eine int- Darstellung dieser Eigenschaften:

LOG.info("Characteristics: " + split1.characteristics());
Characteristics: 16464

3. Spliterator- Eigenschaften

Es hat acht verschiedene Eigenschaften, die sein Verhalten beschreiben. Diese können als Hinweise für externe Tools verwendet werden:

  • GRÖSSE - wenn es in der Lage ist, eine genaue Anzahl von Elementen mit der EstimateSize () -Methode zurückzugeben
  • SORTIERT - wenn es durch eine sortierte Quelle iteriert
  • SUBSIZED - wenn wir die Instanz Split eine mit trySplit () Methode und erhält Spliterators, die SIZED als auch
  • CONCURRENT - wenn die Quelle gleichzeitig sicher geändert werden kann
  • DISTINCT - wenn für jedes Paar angetroffener Elemente x, y ,! X.equals (y)
  • IMMUTABLE - wenn Elemente, die von der Quelle gehalten werden, nicht strukturell geändert werden können
  • NONNULL - ob die Quelle Nullen enthält oder nicht
  • BESTELLT - wenn eine geordnete Sequenz durchlaufen wird

4. Ein benutzerdefinierter Spliterator

4.1. Wann anpassen?

Nehmen wir zunächst das folgende Szenario an:

Wir haben eine Artikelklasse mit einer Liste von Autoren und dem Artikel, der mehr als einen Autor haben kann. Darüber hinaus betrachten wir einen mit dem Artikel verbundenen Autor, wenn die ID seines verwandten Artikels mit der Artikel-ID übereinstimmt.

Unsere Autorenklasse sieht folgendermaßen aus:

public class Author { private String name; private int relatedArticleId; // standard getters, setters & constructors }

Als Nächstes implementieren wir eine Klasse, um Autoren zu zählen, während ein Strom von Autoren durchlaufen wird. Dann führt die Klasse eine Reduzierung des Streams durch.

Werfen wir einen Blick auf die Klassenimplementierung:

public class RelatedAuthorCounter { private int counter; private boolean isRelated; // standard constructors/getters public RelatedAuthorCounter accumulate(Author author) { if (author.getRelatedArticleId() == 0) { return isRelated ? this : new RelatedAuthorCounter( counter, true); } else { return isRelated ? new RelatedAuthorCounter(counter + 1, false) : this; } } public RelatedAuthorCounter combine(RelatedAuthorCounter RelatedAuthorCounter) { return new RelatedAuthorCounter( counter + RelatedAuthorCounter.counter, RelatedAuthorCounter.isRelated); } }

Jede Methode in der obigen Klasse führt eine bestimmte Operation aus, die beim Durchlaufen gezählt werden soll.

First, the accumulate() method traverse the authors one by one in an iterative way, then combine() sums two counters using their values. Finally, the getCounter() returns the counter.

Now, to test what we’ve done so far. Let’s convert our article's list of authors to a stream of authors:

Stream stream = article.getListOfAuthors().stream();

And implement a countAuthor() method to perform the reduction on the stream using RelatedAuthorCounter:

private int countAutors(Stream stream) { RelatedAuthorCounter wordCounter = stream.reduce( new RelatedAuthorCounter(0, true), RelatedAuthorCounter::accumulate, RelatedAuthorCounter::combine); return wordCounter.getCounter(); }

If we used a sequential stream the output will be as expected “count = 9”, however, the problem arises when we try to parallelize the operation.

Let's take a look at the following test case:

@Test void givenAStreamOfAuthors_whenProcessedInParallel_countProducesWrongOutput() { assertThat(Executor.countAutors(stream.parallel())).isGreaterThan(9); }

Apparently, something has gone wrong – splitting the stream at a random position caused an author to be counted twice.

4.2. How to Customize

To solve this, we need to implement a Spliterator that splits authors only when related id and articleId matches. Here’s the implementation of our custom Spliterator:

public class RelatedAuthorSpliterator implements Spliterator { private final List list; AtomicInteger current = new AtomicInteger(); // standard constructor/getters @Override public boolean tryAdvance(Consumer action) { action.accept(list.get(current.getAndIncrement())); return current.get() < list.size(); } @Override public Spliterator trySplit() { int currentSize = list.size() - current.get(); if (currentSize < 10) { return null; } for (int splitPos = currentSize / 2 + current.intValue(); splitPos < list.size(); splitPos++) { if (list.get(splitPos).getRelatedArticleId() == 0) { Spliterator spliterator = new RelatedAuthorSpliterator( list.subList(current.get(), splitPos)); current.set(splitPos); return spliterator; } } return null; } @Override public long estimateSize() { return list.size() - current.get(); } @Override public int characteristics() { return CONCURRENT; } }

Now applying countAuthors() method will give the correct output. The following code demonstrates that:

@Test public void givenAStreamOfAuthors_whenProcessedInParallel_countProducesRightOutput() { Stream stream2 = StreamSupport.stream(spliterator, true); assertThat(Executor.countAutors(stream2.parallel())).isEqualTo(9); }

Also, the custom Spliterator is created from a list of authors and traverses through it by holding the current position.

Let’s discuss in more details the implementation of each method:

  • tryAdvance passes authors to the Consumer at the current index position and increments its position
  • trySplit defines the splitting mechanism, in our case, the RelatedAuthorSpliterator is created when ids matched, and the splitting divides the list into two parts
  • estimatedSize – is the difference between the list size and the position of currently iterated author
  • characteristics– returns the Spliterator characteristics, in our case SIZED as the value returned by the estimatedSize() method is exact; moreover, CONCURRENT indicates that the source of this Spliterator may be safely modified by other threads

5. Support for Primitive Values

The SpliteratorAPI supports primitive values including double, int and long.

The only difference between using a generic and a primitive dedicated Spliterator is the given Consumer and the type of the Spliterator.

Wenn wir es beispielsweise für einen int- Wert benötigen, müssen wir einen intConsumer übergeben . Darüber hinaus finden Sie hier eine Liste primitiver dedizierter Spliteratoren :

  • OfPrimitive : übergeordnete Schnittstelle für andere Grundelemente
  • OfInt : Ein Spliterator, der auf int spezialisiert ist
  • OfDouble : Ein Spliterator für Double
  • OfLong : Ein Spliterator , der lange gewidmet ist

6. Fazit

In diesem Artikel haben wir die Verwendung von Java 8 Spliterator , Methoden, Merkmale, den Aufteilungsprozess, die primitive Unterstützung und das Anpassen behandelt.

Wie immer finden Sie die vollständige Implementierung dieses Artikels auf Github.