CodeGym /Cursos /JAVA 25 SELF /Collector e Spliterator personalizados

Collector e Spliterator personalizados

JAVA 25 SELF
Nível 33 , Lição 4
Disponível

1. Coletores personalizados: quando e como escrever os seus

Na Java Stream API, para transformar um fluxo em uma coleção ou agregado, utiliza-se a interface Collector. Normalmente você usa coletores prontos da classe Collectors (toList(), toMap(), groupingBy() e outros), mas às vezes é necessário algo especial — então você pode escrever o seu próprio Collector.

Collector é um objeto que descreve como, a partir dos elementos do fluxo, montar o resultado final. Ele define quatro (na verdade, cinco) componentes-chave:

  • supplier — cria um novo contêiner para coletar elementos (por exemplo, uma nova lista ou mapa).
  • accumulator — adiciona o próximo elemento ao contêiner.
  • combiner — combina dois contêineres (importante para streams paralelos!).
  • finisher — transforma o contêiner no resultado final (por exemplo, torna-o imutável ou o converte para outro tipo).
  • characteristics — um conjunto de flags que descrevem as propriedades do coletor (por exemplo, se suporta paralelismo, se altera o tipo do resultado etc.).

Assinatura:

Collector<T, A, R>
  • T — tipo dos elementos do fluxo,
  • A — tipo do acumulador intermediário,
  • R — tipo do resultado.

2. Exemplo: Collector para MultiMap (Map<K, List<V>>)

Suponha que você queira coletar um fluxo de pares Pair<K, V> em um Map<K, List<V>> (multi-mapa), em que cada chave corresponde a uma lista de valores.

Exemplo de implementação:

public static <K, V> Collector<Pair<K, V>, ?, Map<K, List<V>>> toMultiMap() {
    return Collector.of(
        HashMap::new, // supplier
        (map, pair) -> map.computeIfAbsent(pair.key(), k -> new ArrayList<>()).add(pair.value()), // accumulator
        (map1, map2) -> { // combiner
            map2.forEach((k, vList) -> map1.merge(k, vList, (l1, l2) -> { l1.addAll(l2); return l1; }));
            return map1;
        },
        Function.identity(), // finisher
        Collector.Characteristics.UNORDERED
    );
}

Uso:

List<Pair<String, Integer>> pairs = List.of(
    new Pair<>("a", 1), new Pair<>("b", 2), new Pair<>("a", 3)
);

Map<String, List<Integer>> multiMap = pairs.stream().collect(toMultiMap());
// multiMap: {a=[1, 3], b=[2]}

3. Exemplo: Collector para top N elementos

Suponha que você queira coletar um fluxo em uma lista com os N maiores elementos (por exemplo, top 5 em ordem decrescente).

Implementação:

public static <T> Collector<T, ?, List<T>> topN(int n, Comparator<? super T> comparator) {
    return Collector.of(
        () -> new PriorityQueue<>(n, comparator), // supplier
        (pq, t) -> {
            pq.offer(t);
            if (pq.size() > n) pq.poll(); // remove o menor
        },
        (pq1, pq2) -> {
            pq2.forEach(t -> {
                pq1.offer(t);
                if (pq1.size() > n) pq1.poll();
            });
            return pq1;
        },
        pq -> {
            List<T> result = new ArrayList<>(pq);
            result.sort(comparator.reversed()); // em ordem decrescente
            return result;
        },
        Collector.Characteristics.UNORDERED
    );
}

Uso:

List<Integer> top3 = Stream.of(5, 1, 9, 3, 7, 2).collect(topN(3, Comparator.naturalOrder()));
// top3: [9, 7, 5]

4. Quando NÃO vale a pena escrever seu próprio Collector

  • Se for possível expressar a tarefa por meio da combinação de coletores padrão e operações downstream (groupingBy, mapping, flatMapping, collectingAndThen e outros), prefira usá-los.
  • Um Collector próprio é necessário apenas para cenários realmente não padronizados (estrutura de dados especial, agregação complexa, top N, multi-mapas etc.).
  • Não escreva um Collector só por escrever — isso dificulta a manutenção e os testes.

Exemplo:

// Em vez de um Collector próprio para Map<K, Set<V>>:
.collect(Collectors.groupingBy(
    Pair::key,
    Collectors.mapping(Pair::value, Collectors.toSet())
))

5. Spliterator personalizado: por que e como

Spliterator é uma interface especial para percorrer e dividir coleções (ou outras fontes de dados) em partes de forma eficiente, especialmente para processamento paralelo. Diferentemente de um iterador comum, o Spliterator pode “dividir” (split) a coleção em partes independentes para processamento paralelo.

Métodos principais:

  • tryAdvance(Consumer<? super T> action) — processa o próximo elemento.
  • trySplit() — tenta dividir a coleção em duas partes (retorna um novo Spliterator para uma das partes).
  • estimateSize() — estimativa da quantidade restante de elementos.
  • characteristics() — uma máscara de bits com as características (ORDERED, SIZED, SUBSIZED e outros).

trySplit: estratégias de divisão

Divisão balanceada — é importante para streams paralelos: trySplit deve retornar partes de tamanhos aproximadamente iguais para que as threads sejam carregadas de forma uniforme.

Se não houver como dividir (por exemplo, poucos elementos), retorne null.

Exemplo: Spliterator para leitura de arquivo em porções
Suponha que você tenha um arquivo grande e queira processá-lo em porções de 1000 linhas por vez, para não manter tudo em memória.

public class ChunkedLineSpliterator implements Spliterator<List<String>> {
    private final BufferedReader reader;
    private final int chunkSize;

    public ChunkedLineSpliterator(BufferedReader reader, int chunkSize) {
        this.reader = reader;
        this.chunkSize = chunkSize;
    }

    @Override
    public boolean tryAdvance(Consumer<? super List<String>> action) {
        List<String> chunk = new ArrayList<>(chunkSize);
        try {
            String line;
            for (int i = 0; i < chunkSize && (line = reader.readLine()) != null; i++) {
                chunk.add(line);
            }
            if (chunk.isEmpty()) return false;
            action.accept(chunk);
            return true;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public Spliterator<List<String>> trySplit() {
        // Para leitura sequencial de arquivo, a divisão não faz sentido — retornamos null
        return null;
    }

    @Override
    public long estimateSize() {
        return Long.MAX_VALUE; // desconhecido de antemão
    }

    @Override
    public int characteristics() {
        return ORDERED | NONNULL;
    }
}

Uso:

try (BufferedReader reader = Files.newBufferedReader(Path.of("big.txt"))) {
    StreamSupport.stream(new ChunkedLineSpliterator(reader, 1000), false)
        .forEach(chunk -> processChunk(chunk));
}

Características do Spliterator

  • ORDERED — os elementos seguem uma ordem definida (por exemplo, lista).
  • SIZED — a quantidade exata de elementos é conhecida.
  • SUBSIZED — todos os Spliterators obtidos via trySplit também são SIZED.
  • IMMUTABLE — a fonte não muda durante a iteração.
  • CONCURRENT — a fonte suporta modificação paralela segura.
  • DISTINCT, SORTED, NONNULL — propriedades adicionais.

Importante: informar corretamente as características afeta a otimização dos streams.

6. Exemplos

  • Leitura de arquivo em porções (chunks) — permite processar arquivos grandes em partes, sem carregar tudo na memória.
  • Análise sem alocações desnecessárias — se você analisa um fluxo de bytes/caracteres e quer minimizar a criação de objetos temporários, pode implementar um Spliterator que entrega “janelas” ou “fatias” do array original.

Exemplo: Spliterator para análise de CSV por linhas

public class CsvLineSpliterator implements Spliterator<String[]> {
    private final BufferedReader reader;

    public CsvLineSpliterator(BufferedReader reader) {
        this.reader = reader;
    }

    @Override
    public boolean tryAdvance(Consumer<? super String[]> action) {
        try {
            String line = reader.readLine();
            if (line == null) return false;
            action.accept(line.split(","));
            return true;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public Spliterator<String[]> trySplit() {
        return null; // análise sequencial
    }

    @Override
    public long estimateSize() {
        return Long.MAX_VALUE;
    }

    @Override
    public int characteristics() {
        return ORDERED | NONNULL;
    }
}

7. Integração com parallel() — como fazer com segurança

  • Se o seu Spliterator suporta divisão paralela (trySplit não retorna null) e as características incluem SIZED/SUBSIZED, então a Stream API conseguirá paralelizar o processamento de forma eficiente.
  • Para fontes de streaming (arquivos, sockets), normalmente a divisão não é suportada — use streams sequenciais.
  • Para coleções e arrays — implemente divisão balanceada (por exemplo, divida o array ao meio).

Exemplo: Spliterator para array

public class ArraySpliterator<T> implements Spliterator<T> {
    private final T[] array;
    private int start, end;

    public ArraySpliterator(T[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }

    @Override
    public boolean tryAdvance(Consumer<? super T> action) {
        if (start < end) {
            action.accept(array[start++]);
            return true;
        }
        return false;
    }

    @Override
    public Spliterator<T> trySplit() {
        int mid = (start + end) >>> 1;
        if (mid == start) return null;
        ArraySpliterator<T> split = new ArraySpliterator<>(array, start, mid);
        start = mid;
        return split;
    }

    @Override
    public long estimateSize() {
        return end - start;
    }

    @Override
    public int characteristics() {
        return ORDERED | SIZED | SUBSIZED | IMMUTABLE;
    }
}

Uso:

String[] arr = {"a", "b", "c", "d"};
StreamSupport.stream(new ArraySpliterator<>(arr, 0, arr.length), true)
    .forEach(System.out::println);
1
Tarefa
JAVA 25 SELF, nível 33, lição 4
Bloqueado
Organização da biblioteca de linguagens de programação antigas 📜
Organização da biblioteca de linguagens de programação antigas 📜
1
Tarefa
JAVA 25 SELF, nível 33, lição 4
Bloqueado
Desafio zoológico: agrupar animais pelo comprimento do nome 🦁🐱
Desafio zoológico: agrupar animais pelo comprimento do nome 🦁🐱
1
Pesquisa/teste
Otimização do trabalho com coleções, nível 33, lição 4
Indisponível
Otimização do trabalho com coleções
Otimização do trabalho com coleções
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION