Introduction à Spliterator en Java

Introduction à Spliterator en Java

1. Vue d'ensemble

L'interfaceSpliterator, introduite dans Java 8, peut êtreused for traversing and partitioning sequences. C’est un utilitaire de base pour lesStreams, en particulier les parallèles.

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

2. APISpliterator

2.1. tryAdvance

C'est la méthode principale utilisée pour parcourir une séquence. La méthodetakes a Consumer that’s used to consume elements of the Spliterator one by one sequentially et renvoiefalse s'il n'y a aucun élément à parcourir.

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

Tout d'abord, supposons que nous avons unArrayList avec 35 000 articles et que la classeArticle 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 le suffixe "– published by example” à chaque nom d'article:

public String call() {
    int current = 0;
    while (spliterator.tryAdvance(a -> a.setName(article.getName()
      .concat("- published by example")))) {
        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éthodetryAdvance() pour traiter l'élément suivant.

2.2. trySplit

Ensuite, fractionnonsSpliterators (d’où le nom) et traitons les partitions indépendamment.

La méthodetrySplit tente de la scinder en deux parties. Ensuite, les éléments de traitement de l'appelant et, enfin, l'instance renvoyée traitera les autres, permettant ainsi aux deux d'être traités en parallèle.

Commençons par générer notre liste:

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

Ensuite, nous obtenons notre instanceSpliterator en utilisant la méthodespliterator(). Ensuite, nous appliquons notre méthodetrySplit():

@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. taille estimée

La méthodeestimatedSize nous donne une estimation du nombre 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 desSpliterator. Ensuite, si nous invoquons la méthode ci-dessus, la sortie sera une représentationint de ces caractéristiques:

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

3. Spliterator Characteristics

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 la méthodeestimateSize()

  • SORTED - s'il effectue une itération dans une source triée

  • SUBSIZED - si nous divisons l'instance en utilisant une méthodetrySplit() et obtenons des Spliterators qui sont égalementSIZED

  • CONCURRENT - si la source peut être modifiée en toute sécurité simultanément

  • DISTINCT - si pour chaque paire d'éléments rencontrésx, 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. UnSpliterator personnalisé

4.1. Quand personnaliser

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

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

Notre classeAuthor ressemblera à ceci:

public class Author {
    private String name;
    private int relatedArticleId;

    // standard getters, setters & constructors
}

Ensuite, nous allons implémenter une classe pour compter les auteurs tout en parcourant un flux d’auteurs. Puisthe class will perform a reduction sur le flux.

Voyons 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 pendant le parcours.

Tout d'abord, lesaccumulate() method traverse the authors one by one in an iterative way, puis lescombine() sums two counters using their values. Enfin, legetCounter() renvoie le compteur.

Maintenant, pour tester ce que nous avons fait jusqu’à présent. Convertissons la liste des auteurs de notre article en un flux d'auteurs:

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

Et implémentez uncountAuthor() 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();
}

Si nous avons utilisé un flux séquentiel, la sortie sera comme prévu“count = 9”, cependant, le problème se pose lorsque nous essayons de paralléliser l'opération.

Examinons le scénario de test suivant:

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

Apparemment, quelque chose s'est mal passé - le fait de scinder le flux en une position aléatoire a compté deux fois l'auteur.

4.2. Comment personnaliser

Pour résoudre ce problème, nous devonsimplement a Spliterator that splits authors only when related id and articleId matches. Voici la mise en œuvre de nosSpliterator personnalisés:

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

Maintenant, appliquer la méthodecountAuthors() donnera la sortie correcte. Le code suivant montre que:

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

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

De plus, leSpliterator personnalisé est créé à partir d'une liste d'auteurs et le parcourt en maintenant la position actuelle.

Discutons plus en détail de la mise en œuvre de chaque méthode:

  • *tryAdvance* – passe les auteurs auxConsumer à la position d'index actuelle et incrémente sa position

  • *trySplit* – définit le mécanisme de fractionnement, dans notre cas, leRelatedAuthorSpliterator est créé lorsque les identifiants correspondent, et le fractionnement divise la liste en deux parties

  • estimatedSize - est la différence entre la taille de la liste et la position de l'auteur actuellement itéré

  • characteristics - renvoie les caractéristiquesSpliterator, dans notre casSIZED car la valeur renvoyée par la méthodeestimatedSize() est exacte; de plus,CONCURRENT indique que la source de ceSpliterator peut être modifiée en toute sécurité par d'autres threads

5. Prise en charge des valeurs primitives

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

La seule différence entre l'utilisation d'unSpliterator dédié générique et primitif est leConsumer donné et le type duSpliterator.

Par exemple, lorsque nous en avons besoin pour une valeurint, nous devons passer unintConsumer. De plus, voici une liste deSpliterators dédiés primitifs:

  • OfPrimitive<T, T_CONS, T_SPLITR extends Spliterator.OfPrimitive<T, T_CONS, T_SPLITR>>: interface parent pour les autres primitives

  • OfInt: ASpliterator spécialisé pourint

  • OfDouble: ASpliterator dédié pourdouble

  • OfLong: ASpliterator dédié pourlong

6. Conclusion

Dans cet article, nous avons couvert Java 8Spliterator usage, methods, characteristics, splitting process, primitive support and how to customize it.

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