CodeGym /Cursos /JAVA 25 SELF /Subconjuntos da Stream API: distinct, limit, skip

Subconjuntos da Stream API: distinct, limit, skip

JAVA 25 SELF
Nível 30, Lição 2
Disponível

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.

1
Tarefa
JAVA 25 SELF, nível 30, lição 2
Bloqueado
Gerenciando a fila para uma festa exclusiva 🎉
Gerenciando a fila para uma festa exclusiva 🎉
1
Tarefa
JAVA 25 SELF, nível 30, lição 2
Bloqueado
Formação do Top 4 de Métricas Únicas de Vendas 💰
Formação do Top 4 de Métricas Únicas de Vendas 💰
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION