Esse é um post muito rápido! Eu tinha estudado isso muito tempo atrás, na época que o Java 8 foi lançado, mas hoje tive uma ideia de usar ele.
Qual é problema a se resolver?
Podemos usar Stream para abstrair um tipo de coleção, com o Stream podemos encapsular o metodo de captura do dado e só expor uma fonte de dados.
Porque eu decidir usar?
Estou implementando uma funcionalidade que consome uma lista de produtos de uma API. Como essa lista é paginada e eu preciso usar em alguns lugares, prefiro criar um Stream, assim toda a lógica de percorrer a lista é encapsulada. Se eu retornasse uma lista, a primeira operação só ocorreria quando toda a lista estivesse carregada na memória. Com o Stream, eu terei pequenas listas na memória e quando ela se esgota vai percorrendo as páginas.
Como implementar?
Vamos abstrair o meu StoreService
, certo? Vou apresentar ele como uma interface (na verdade ele é, só que usando MicroProfile RestClient com Quarkus):
@Path("/")
@RegisterRestClient
public interface StoreService {
@GET
@Path("/produtos")
@Consumes(MediaType.APPLICATION_JSON)
List<Produto> listarProdutos(@QueryParam("limit") int limit,
@QueryParam("offset") int offset)
}
Configurado e validado que está funcionando corretamente é hora de começar a construir o Stream. O próximo passo e se perguntar qual é as caracteristicas principais desse Stream. Para isso recomendo ler a documentação do Spliterator, interface que é o coração de qualquer Stream. Dado as caracteristicas do meu Stream, vou considerar ele imutável, não nulo e com tamanho fixo.
Agora podemos implementar a classe que irá dar vida ao Stream. Para isso devemos basicamente implementar um método, o tryAdvance
, ele deverá consumir um dos itens da lista de produtos e retornar se há ou não mais elementos. Segue abaixo uma tabela com os detalhes da implmentação por método.
Método | Escolhas |
---|---|
tryAdvance |
Irá consumir elementos da pilha. Se a pilha estiver vazia irá pedir uma nova página, se vier menos elementos que o requisitado, irá setar uma flag dizendo que acabou os elementos. |
trySplit |
Sempre retornará null , não será possível fazer processamento paralelo. |
estimateSize |
Será o tamanho da pilha mais uma página, se a última requisição voltar menos itens que uma página, será apenas o tamanho da pilha. |
characteristics |
Irá informar que esse Stream é imutável, com tamanho fixo e não nulo. |
Segue a implementação final:
public class RemoteSpliterator implements Spliterator<Produto> {
private static final Logger logger = LoggerFactory.getLogger(RemoteSpliterator .class);
private statica final int LIMITE = 10; // tamanho da página
private Queue<Produto> produtos;
private boolean temMais;
private int offsetAtual;
private StoreService storeService;
RemoteSpliterator (StoreService storeService) {
produtos = new LinkedList<>();
temMais = true;
offsetAtual = 0;
this.storeService = storeService;
}
@Override
public boolean tryAdvance(Consumer<? super Produto> action) {
if (produtos.isEmpty()) {
if (!temMais) {
return false;
} else {
var produtosRemotos= storeService.listarProdutos(token, LIMITE, offsetAtual);
temMais = produtosRemotos.size() == LIMITE;
offsetAtual += produtosRemotos.size();
logger.info("Produdos lidos do servidor: {}", produtosRemotos);
produtos.addAll(produtosRemotos);
}
}
if (produtos.isEmpty()) {
return false;
} else {
action.accept(produtos.poll());
return true;
}
}
@Override
public Spliterator<Produto> trySplit() {
return null;
}
@Override
public long estimateSize() {
return temMais ? produtos.size() + LIMITE : produtos.size();
}
@Override
public int characteristics() {
return Spliterator.IMMUTABLE | Spliterator.SIZED | Spliterator.NONNULL;
}
}
Para finalizar, precisamos apenas criar o Stream, isso pode se feito chamando o método StreamSupport.stream usando o Spliterator criado como primeiro parâmetro e false
como segundo parâmetro, já que ele não aceita processamento paralelo.
Porque usei LinkedList?
Porque o acesso não será como em um array, mas em pilha. Apenas adicionarei itens no final a removereis itens do começo. Usar ArrayList
tem um custo maior para adicionar e remover, enquanto sua vantagem está no acesso a itens que é em O(1)
.
Conclusão
Para se criar Streams não precisamos de ter todos os dados em mãos, essa é uma ferramenta poderosa que nos permite transformar qualquer fluxo de dados em uma API poderosa que irá agilizar a execução do seu código.
Você não precisa resolver tudo em uma lista e depois criar o Stream.