Introdução ao Spliterator em Java

Introdução ao Spliterator em Java

1. Visão geral

A interfaceSpliterator, introduzida no Java 8, pode serused for traversing and partitioning sequences. É um utilitário básico paraStreams, especialmente os paralelos.

Neste artigo, vamos cobrir seu uso, características, métodos e como criar nossas próprias implementações personalizadas.

2. APISpliterator

2.1. tryAdvance

Este é o principal método usado para percorrer uma sequência. O métodotakes a Consumer that’s used to consume elements of the Spliterator one by one sequentiallye retornafalse se não houver elementos a serem percorridos.

Aqui, veremos como usá-lo para atravessar e particionar elementos.

Primeiro, vamos supor que temos umArrayList com 35000 artigos e que a classeArticle é definida como:

public class Article {
    private List listOfAuthors;
    private int id;
    private String name;

    // standard constructors/getters/setters
}

Agora, vamos implementar uma tarefa que processa a lista de artigos e adiciona um sufixo “– published by example” para cada nome de artigo:

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

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

Observe que esta tarefa gera o número de artigos processados ​​quando termina a execução.

Outro ponto chave é que usamos o métodotryAdvance() para processar o próximo elemento.

2.2. trySplit

Em seguida, vamos dividirSpliterators (daí o nome) e processar as partições de forma independente.

O métodotrySplit tenta dividi-lo em duas partes. Em seguida, o chamador processa os elementos e, finalmente, a instância retornada processa as outras, permitindo que as duas sejam processadas em paralelo.

Vamos gerar nossa lista primeiro:

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

A seguir, obtemos nossa instânciaSpliterator usando o métodospliterator(). Em seguida, aplicamos nosso métodotrySplit():

@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. tamanho estimado

O métodoestimatedSize nos dá um número estimado de elementos:

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

Isso produzirá:

Size: 17500

2.4. hasCharacteristics

Esta API verifica se as características fornecidas correspondem às propriedades deSpliterator.. Então, se invocarmos o método acima, a saída será uma representaçãoint dessas características:

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

3. Spliterator Characteristics

Possui oito características diferentes que descrevem seu comportamento. Elas podem ser usadas como dicas para ferramentas externas:

  • SIZED se for capaz de retornar um número exato de elementos com o métodoestimateSize()

  • SORTED - se estiver iterando por meio de uma fonte classificada

  • SUBSIZED - se dividirmos a instância usando um métodotrySplit() e obtermos Divisores que sãoSIZED também

  • CONCURRENT - se a fonte puder ser modificada com segurança simultaneamente

  • DISTINCT - se para cada par de elementos encontradosx, y, !x.equals(y)

  • IMMUTABLE - se os elementos mantidos pela fonte não puderem ser modificados estruturalmente

  • NONNULL - se a fonte contém nulos ou não

  • ORDERED - se iterando sobre uma sequência ordenada

4. UmSpliterator personalizado

4.1. Quando personalizar

Primeiro, vamos assumir o seguinte cenário:

Temos uma classe de artigos com uma lista de autores, e o artigo que pode ter mais de um autor. Além disso, consideramos um autor relacionado ao artigo se o ID do artigo relacionado corresponder ao ID do artigo.

Nossa classeAuthor será parecida com esta:

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

    // standard getters, setters & constructors
}

Em seguida, implementaremos uma classe para contar autores enquanto percorremos um fluxo de autores. Entãothe class will perform a reduction no stream.

Vamos dar uma olhada na implementação da 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);
    }
}

Cada método na classe acima executa uma operação específica para contar enquanto estiver percorrendo.

Primeiro, oaccumulate() method traverse the authors one by one in an iterative way, depoiscombine() sums two counters using their values. Finalmente, ogetCounter() retorna o contador.

Agora, para testar o que fizemos até agora. Vamos converter a lista de autores do nosso artigo em um fluxo de autores:

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

E implemente umcountAuthor() 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();
}

Se usarmos um fluxo sequencial, a saída será a“count = 9” esperada, entretanto, o problema surge quando tentamos paralelizar a operação.

Vamos dar uma olhada no seguinte caso de teste:

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

Aparentemente, algo deu errado - dividir o fluxo em uma posição aleatória fez com que um autor fosse contado duas vezes.

4.2. Como personalizar

Para resolver isso, precisamosimplement a Spliterator that splits authors only when related id and articleId matches. Aqui está a implementação de nossoSpliterator personalizado:

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

Agora, a aplicação do métodocountAuthors() dará a saída correta. O código a seguir demonstra que:

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

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

Além disso, oSpliterator personalizado é criado a partir de uma lista de autores e percorre-a mantendo a posição atual.

Vamos discutir com mais detalhes a implementação de cada método:

  • *tryAdvance* – passa os autores paraConsumer na posição de índice atual e incrementa sua posição

  • *trySplit* – define o mecanismo de divisão, no nosso caso, oRelatedAuthorSpliterator é criado quando os ids correspondem, e a divisão divide a lista em duas partes

  • estimatedSize - é a diferença entre o tamanho da lista e a posição do autor iterado atualmente

  • characteristics - retorna as características deSpliterator, no nosso casoSIZED pois o valor retornado pelo métodoestimatedSize() é exato; além disso,CONCURRENT indica que a fonte desteSpliterator pode ser modificada com segurança por outros threads

5. Suporte para valores primitivos

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

A única diferença entre usar umSpliterator dedicado genérico e um primitivo é oConsumer fornecido e o tipo deSpliterator.

Por exemplo, quando precisamos dele para um valorint, precisamos passar umintConsumer. Além disso, aqui está uma lista deSpliterators dedicados primitivos:

  • OfPrimitive<T, T_CONS, T_SPLITR extends Spliterator.OfPrimitive<T, T_CONS, T_SPLITR>>: interface pai para outras primitivas

  • OfInt: ASpliterator especializado paraint

  • OfDouble: ASpliterator dedicado paradouble

  • OfLong: ASpliterator dedicado paralong

6. Conclusão

Neste artigo, abordamos Java 8Spliterator usage, methods, characteristics, splitting process, primitive support and how to customize it.

Como sempre, a implementação completa deste artigo pode ser encontradaover on Github.