Einführung in Spliterator in Java

Einführung in Spliterator in Java

1. Überblick

Die in Java 8 eingeführteSpliterator-Schnittstelle kannused for traversing and partitioning sequences sein. Es ist ein Basisdienstprogramm fürStreams, insbesondere für parallele.

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

2. Spliterator API

2.1. tryAdvance

Dies ist die Hauptmethode zum Durchlaufen einer Sequenz. Die Methodetakes a Consumer that’s used to consume elements of the Spliterator one by one sequentially und gibtfalse zurück, wenn keine zu durchlaufenden Elemente vorhanden sind.

Hier sehen wir uns an, wie man damit Elemente durchquert und partitioniert.

Nehmen wir zunächst an, dass wirArrayList mit 35000 Artikeln haben und dass die KlasseArticlewie folgt definiert ist:

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 "– published by example”" hinzufügt:

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

    return Thread.currentThread().getName() + ":" + current;
}

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

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

2.2. trySplit

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

Die MethodetrySplitversucht, sie in zwei Teile aufzuteilen. Dann verarbeiten die aufrufenden Prozesselemente 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 unsereSpliterator-Instanz mit derspliterator()-Methode. Dann wenden wir unseretrySplit()-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 + ""); }

The splitting process worked as intended and divided the records equally.

2.3. geschätzte Größe

DieestimatedSize-Methode 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 Merkmale mit den Eigenschaften vonSpliterator. übereinstimmen. Wenn wir dann die obige Methode aufrufen, ist die Ausgabe eineint-Darstellung dieser Merkmale:

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

3. Spliterator Characteristics

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

  • SIZED, wenn mit der MethodeestimateSize() eine genaue Anzahl von Elementen zurückgegeben werden kann

  • SORTED - wenn eine sortierte Quelle durchlaufen wird

  • SUBSIZED - Wenn wir die Instanz mit einertrySplit()-Methode teilen und Spliteratoren erhalten, die ebenfallsSIZED sind

  • CONCURRENT - wenn die Quelle gleichzeitig sicher geändert werden kann

  • DISTINCT - wenn für jedes Paar angetroffener Elementex, 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

  • ORDERED - wenn eine geordnete Sequenz durchlaufen wird

4. Ein benutzerdefiniertesSpliterator

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 auf den Artikel bezogenen Autor, wenn die ID seines zugehörigen Artikels mit der Artikel-ID übereinstimmt.

UnsereAuthor-Klasse 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 wir einen Strom von Autoren durchlaufen. Dann istthe class will perform a reductionim Stream.

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, um beim Durchlaufen zu zählen.

Zuerst dieaccumulate() method traverse the authors one by one in an iterative way, danncombine() sums two counters using their values. Schließlich gibtgetCounter() den Zähler zurück.

Testen Sie jetzt, was wir bisher gemacht haben. Konvertieren wir die Autorenliste unseres Artikels in einen Autorenstrom:

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

Und implementiere eincountAuthor() 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();
}

Wenn wir einen sequentiellen Stream verwendet haben, ist die Ausgabe wie erwartet“count = 9”. Das Problem tritt jedoch auf, wenn wir versuchen, die Operation zu parallelisieren.

Schauen wir uns den folgenden Testfall an:

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

Anscheinend ist etwas schiefgegangen - das Aufteilen des Streams an einer zufälligen Position führte dazu, dass ein Autor zweimal gezählt wurde.

4.2. So passen Sie an

Um dies zu lösen, benötigen wirimplement a Spliterator that splits authors only when related id and articleId matches. Hier ist die Implementierung unserer benutzerdefiniertenSpliterator:

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

Wenn Sie nun die MethodecountAuthors() anwenden, erhalten Sie die richtige Ausgabe. Der folgende Code zeigt, dass:

@Test
public void
  givenAStreamOfAuthors_whenProcessedInParallel_countProducesRightOutput() {
    Stream stream2 = StreamSupport.stream(spliterator, true);

    assertThat(Executor.countAutors(stream2.parallel())).isEqualTo(9);
}

Außerdem wird das benutzerdefinierteSpliterator aus einer Liste von Autoren erstellt und durchläuft es, indem die aktuelle Position gehalten wird.

Lassen Sie uns die Implementierung der einzelnen Methoden genauer erläutern:

  • *tryAdvance* – übergibt die Autoren anConsumer an der aktuellen Indexposition und erhöht ihre Position

  • *trySplit* – definiert den Aufteilungsmechanismus. In unserem Fall wirdRelatedAuthorSpliterator erstellt, wenn die IDs übereinstimmen, und die Aufteilung teilt die Liste in zwei Teile

  • estimatedSize - ist der Unterschied zwischen der Listengröße und der Position des aktuell iterierten Autors

  • characteristics - gibt die Eigenschaften vonSpliterator zurück, in unserem FallSIZED, da der von der MethodeestimatedSize() zurückgegebene Wert genau ist; Darüber hinaus gibtCONCURRENT an, dass die Quelle diesesSpliterator von anderen Threads sicher geändert werden kann

5. Unterstützung für primitive Werte

DieSpliterator API supports primitive values including double, int and long.

Der einzige Unterschied zwischen der Verwendung eines generischen und eines primitiven dediziertenSpliterator besteht in den angegebenenConsumer und dem Typ derSpliterator.

Wenn wir es beispielsweise für einenint-Wert benötigen, müssen wir einenintConsumer übergeben. Außerdem finden Sie hier eine Liste primitiver dedizierterSpliterators:

  • OfPrimitive<T, T_CONS, T_SPLITR extends Spliterator.OfPrimitive<T, T_CONS, T_SPLITR>>: übergeordnete Schnittstelle für andere Grundelemente

  • OfInt: ASpliterator, spezialisiert aufint

  • OfDouble: ASpliterator fürdouble

  • OfLong: ASpliterator fürlong

6. Fazit

In diesem Artikel haben wir Java 8Spliterator usage, methods, characteristics, splitting process, primitive support and how to customize it. behandelt

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