Introduction à Spliterator en Java

1. Vue d'ensemble

L' interface Spliterator , introduite dans Java 8, peut être utilisée pour parcourir et partitionner des séquences . C'est un utilitaire de base pour les flux , en particulier les parallèles.

Dans cet article, nous aborderons son utilisation, ses caractéristiques, ses méthodes et comment créer nos propres implémentations personnalisées.

2. API Spliterator

2.1. tryAdvance

Il s'agit de la méthode principale utilisée pour parcourir une séquence. La méthode prend un Consumer qui est utilisé pour consommer des éléments du Spliterator un par un séquentiellement et retourne false s'il n'y a aucun élément à traverser.

Ici, nous allons voir comment l'utiliser pour traverser et partitionner des éléments.

Tout d'abord, supposons que nous avons une ArrayList avec 35000 articles et que la classe Article est définie comme:

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

Maintenant, implémentons une tâche qui traite la liste des articles et ajoute un suffixe « - publié par Baeldung» à chaque nom d'article:

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

Notez que cette tâche génère le nombre d'articles traités à la fin de l'exécution.

Un autre point clé est que nous avons utilisé la méthode tryAdvance () pour traiter l'élément suivant.

2.2. trySplit

Ensuite, divisons les Spliterators (d'où le nom) et traitons les partitions indépendamment.

La méthode trySplit tente de le diviser en deux parties. Ensuite, l'appelant traite les éléments, et enfin, l'instance retournée traitera les autres, permettant aux deux d'être traités en parallèle.

Générons d'abord notre liste:

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

Ensuite, nous obtenons notre instance Spliterator en utilisant la méthode spliterator () . Ensuite, nous appliquons notre méthode trySplit () :

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

Le processus de fractionnement a fonctionné comme prévu et a divisé les enregistrements de manière égale .

2.3. taille estimée

La estimatedSize méthode nous donne un nombre estimé d'éléments:

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

Cela produira:

Size: 17500

2.4. hasCaractéristiques

Cette API vérifie si les caractéristiques données correspondent aux propriétés du Spliterator. Ensuite, si nous invoquons la méthode ci-dessus, la sortie sera une représentation int de ces caractéristiques:

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

3. Caractéristiques du séparateur

Il a huit caractéristiques différentes qui décrivent son comportement. Ceux-ci peuvent être utilisés comme conseils pour les outils externes:

  • SIZED - s'il est capable de renvoyer un nombre exact d'éléments avec laméthode devisSize ()
  • TRIÉ - s'il itère à travers une source triée
  • SUBSIZED - si nous divisons l'instanceaideun trySplit () méthode et obtenir Spliterators qui sont de taille aussi bien
  • CONCURRENT - si la source peut être modifiée en toute sécurité simultanément
  • DISTINCT - si pour chaque paire d'éléments rencontrés x, y,! X.equals (y)
  • IMMUTABLE - si les éléments détenus par la source ne peuvent pas être modifiés structurellement
  • NONNULL - si la source contient des valeurs nulles ou non
  • ORDERED - si itération sur une séquence ordonnée

4. Un séparateur personnalisé

4.1. Quand personnaliser

Tout d'abord, supposons le scénario suivant:

Nous avons une classe d'article avec une liste d'auteurs et l'article qui peut avoir plus d'un auteur. De plus, nous considérons un auteur lié à l'article si l'identifiant de son article correspondant correspond à l'identifiant de l'article.

Notre classe Author ressemblera à ceci:

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

Ensuite, nous implémenterons une classe pour compter les auteurs tout en parcourant un flux d'auteurs. Ensuite, la classe effectuera une réduction sur le flux.

Jetons un coup d'œil à l'implémentation de la classe:

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

Chaque méthode de la classe ci-dessus effectue une opération spécifique à compter lors de la traversée.

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.

Par exemple, lorsque nous en avons besoin pour une valeur int , nous devons passer un intConsumer . De plus, voici une liste de Spliterators dédiés primitifs :

  • OfPrimitif : interface parent pour les autres primitives
  • OfInt : un Spliterator spécialisé pour int
  • OfDouble : un Spliterator dédié au double
  • OfLong : un Spliterator dédié depuis longtemps

6. Conclusion

Dans cet article, nous avons couvert l'utilisation de Java 8 Spliterator , les méthodes, les caractéristiques, le processus de fractionnement, la prise en charge des primitives et comment le personnaliser.

Comme toujours, la mise en œuvre complète de cet article peut être trouvée sur Github.