1. Método distinct: removendo duplicatas
Em problemas reais, muitas vezes é necessário não apenas filtrar e transformar dados, mas também selecionar apenas elementos únicos de uma coleção, limitar o tamanho do resultado ou, ao contrário, pular os primeiros elementos. Por exemplo:
- Obter a lista de nomes de usuários exclusivos.
- Pegar apenas os 10 primeiros registros para exibir na página.
- Pular os 5 primeiros elementos (por exemplo, ao implementar a navegação por páginas — “mostrar a partir da 2ª página”).
Para tais tarefas, a Stream API tem métodos especiais: distinct, limit, skip.
Como funciona o distinct?
O método distinct() retorna um novo stream no qual todas as duplicatas de elementos são removidas. As duplicatas são determinadas pelos métodos equals e hashCode da classe correspondente.
Imagine que você é um numismata, e é importante que não haja duplicatas na coleção principal. Todas as moedas repetidas irão para o fundo de troca. Justamente o distinct() é capaz de filtrar as duplicatas da coleção principal.
Exemplo: nomes de usuários exclusivos
List<String> names = List.of(
"Alice", "Bob", "Alice", "Eva", "Bob", "Denis", "Gleb", "Eva"
);
Vamos obter a lista de nomes únicos:
List<String> uniqueNames = names.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueNames);
// Saída: [Alice, Bob, Eva, Denis, Gleb]
Exemplo: e-mails de usuários exclusivos
Suponha que temos a classe User:
public class User {
String name;
String email;
// Construtor, getters, toString() — para conveniência
public User(String name, String email) {
this.name = name;
this.email = email;
}
@Override
public String toString() {
return name + " <" + email + ">";
}
}
Lista de usuários com e-mails duplicados:
List<User> users = List.of(
new User("Alice", "alice@mail.com"),
new User("Bob", "bob@mail.com"),
new User("Eva", "eva@mail.com"),
new User("Alice2", "alice@mail.com"), // e-mail duplicado!
new User("Gleb", "gleb@mail.com"),
new User("Eva2", "eva@mail.com") // e-mail duplicado!
);
Se você simplesmente chamar users.stream().distinct(), as duplicatas não serão removidas, porque nos objetos User, por padrão, os métodos equals e hashCode não estão sobrescritos. Nesse caso, distinct funciona apenas para coincidências por referência.
Solução: Sobrescreva equals e hashCode de modo que a unicidade seja definida por email.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(email);
}
Agora:
List<User> uniqueUsers = users.stream()
.distinct()
.collect(Collectors.toList());
uniqueUsers.forEach(System.out::println);
// Alice <alice@mail.com>
// Bob <bob@mail.com>
// Eva <eva@mail.com>
// Gleb <gleb@mail.com>
Importante: Para suas próprias classes, sempre sobrescreva equals e hashCode se quiser que o distinct funcione “de forma normal”!
2. Método limit: limitando a quantidade de elementos
O método limit(long maxSize) retorna um novo stream que contém no máximo os primeiros maxSize elementos do stream original.
Analogia: Você foi a uma confeitaria e quer experimentar apenas 3 doces dentre 100. limit(3) — e te dão exatamente os três primeiros; os demais — “infelizmente, não hoje”.
Exemplo: os 3 primeiros nomes
List<String> firstThree = names.stream()
.limit(3)
.collect(Collectors.toList());
System.out.println(firstThree);
// Saída: [Alice, Bob, Alice]
Exemplo com nosso aplicativo: top‑3 novos usuários
List<User> firstUsers = users.stream()
.limit(3)
.collect(Collectors.toList());
firstUsers.forEach(System.out::println);
// Alice <alice@mail.com>
// Bob <bob@mail.com>
// Eva <eva@mail.com>
Exemplo com ordenação
Podemos combinar com ordenação — por exemplo, pegar os top‑2 usuários com o e-mail mais curto:
List<User> top2ShortEmail = users.stream()
.sorted(Comparator.comparingInt(u -> u.email.length()))
.limit(2)
.collect(Collectors.toList());
top2ShortEmail.forEach(System.out::println);
// Bob <bob@mail.com>
// Eva <eva@mail.com>
3. Método skip: pulando os primeiros elementos
O método skip(long n) retorna um novo stream no qual os primeiros n elementos do stream original são ignorados.
Exemplo: pular os 2 primeiros nomes
List<String> afterTwo = names.stream()
.skip(2)
.collect(Collectors.toList());
System.out.println(afterTwo);
// Saída: [Alice, Eva, Bob, Denis, Gleb, Eva]
Exemplo: navegação por páginas (pagination)
Frequentemente, em um site, é preciso mostrar, por exemplo, “3 usuários por página”. Para a segunda página, precisamos pular os 3 primeiros e pegar os 3 seguintes:
int pageSize = 3;
int pageNumber = 2; // Segunda página
List<User> page = users.stream()
.skip(pageSize * (pageNumber - 1))
.limit(pageSize)
.collect(Collectors.toList());
page.forEach(System.out::println);
// Alice2 <alice@mail.com>
// Gleb <gleb@mail.com>
// Eva2 <eva@mail.com>
4. Combinando distinct, limit, skip
Esses métodos podem e devem ser combinados — dependendo da tarefa.
Exemplo: obter 2 nomes únicos, começando pelo terceiro na ordem
List<String> result = names.stream()
.distinct() // Removemos duplicatas: [Alice, Bob, Eva, Denis, Gleb]
.skip(2) // Pulamos Alice e Bob: [Eva, Denis, Gleb]
.limit(2) // Pegamos apenas dois: [Eva, Denis]
.collect(Collectors.toList());
System.out.println(result);
// Saída: [Eva, Denis]
Exemplo com filtragem
Suponha que precisamos obter os 2 primeiros e-mails únicos que contêm a letra “a”:
List<String> emails = users.stream()
.map(user -> user.email)
.filter(email -> email.contains("a"))
.distinct()
.limit(2)
.collect(Collectors.toList());
System.out.println(emails);
// Saída: [alice@mail.com, eva@mail.com]
5. Prática: exercícios de aplicação
Exercício 1. Obter a lista de nomes de usuários exclusivos cuja extensão seja maior que 3 caracteres
List<String> longUniqueNames = names.stream()
.filter(name -> name.length() > 3)
.distinct()
.collect(Collectors.toList());
System.out.println(longUniqueNames);
// Por exemplo: [Alice, Denis]
Exercício 2. Obter o 3º e o 4º e-mails únicos da lista de usuários
List<String> thirdAndFourthEmail = users.stream()
.map(user -> user.email)
.distinct()
.skip(2)
.limit(2)
.collect(Collectors.toList());
System.out.println(thirdAndFourthEmail);
// Por exemplo: [eva@mail.com, gleb@mail.com]
Exercício 3. Obter os primeiros 5 números únicos que são maiores que 10
List<Integer> numbers = List.of(5, 12, 17, 5, 23, 17, 42, 19, 12, 8);
List<Integer> result = numbers.stream()
.filter(n -> n > 10)
.distinct()
.limit(5)
.collect(Collectors.toList());
System.out.println(result);
// Saída: [12, 17, 23, 42, 19]
6. Esquema visual: ordem de aplicação das operações
graph TD
A[Lista original] --> B[filter]
B --> C[distinct]
C --> D[skip]
D --> E[limit]
E --> F[collect]
Comentário:
Geralmente, aplica-se primeiro o filter, depois removem-se as duplicatas com distinct, em seguida usa-se skip e limit, e no final — collect. Mas a ordem pode ser alterada, se a tarefa exigir.
7. Erros típicos ao trabalhar com distinct, limit, skip
Erro nº 1: Esperar que distinct remova duplicatas por “qualquer” critério.
Na verdade, distinct funciona conforme o método equals do objeto. Se você quer unicidade por algum campo (por exemplo, apenas por email), e não pelo objeto inteiro — é preciso ou sobrescrever equals/hashCode, ou usar artifícios com Collectors.toMap() ou filtragem adicional.
Erro nº 2: Quebrar a ordem das operações.
Se você aplicar primeiro limit e depois distinct, duplicatas podem permanecer — afinal, você limitou o stream aos primeiros N elementos, e entre eles podem haver iguais.
Erro nº 3: Pular via skip mais elementos do que existem.
Se você tentar pular mais elementos do que há no stream, o resultado será apenas uma lista vazia — não haverá erro, mas o resultado pode surpreender.
Erro nº 4: Desempenho não considerado.
Os métodos distinct, limit, skip podem ser ineficientes em streams muito grandes, especialmente se o stream for não ordenado ou se os elementos forem complexos. Em 99 % dos casos do dia a dia isso não é um problema, mas se você trabalha com milhões de registros — vale a pena refletir.
Erro nº 5: Esquecer de sobrescrever a lógica de equals/hashCode em suas classes.
Se você trabalha com objetos de domínio (por exemplo, User), sem sobrescrever esses métodos o distinct vai considerar objetos diferentes, mesmo que sejam “logicamente” iguais.
GO TO FULL VERSION