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

Subconjuntos de la Stream API: distinct, limit, skip

JAVA 25 SELF
Nivel 30, Lección 2
Disponible

1. Método distinct: eliminamos duplicados

En tareas reales, a menudo no solo se requiere filtrar y transformar datos, sino también seleccionar de una colección únicamente elementos únicos, limitar el tamaño del resultado o, por el contrario, omitir los primeros elementos. Por ejemplo:

  • Obtener la lista de nombres de usuario únicos.
  • Tomar solo los primeros 10 registros para mostrarlos en la página.
  • Omitir los primeros 5 elementos (por ejemplo, al implementar la navegación por páginas — «mostrar desde la 2.ª página»).

Para estas tareas, la Stream API ofrece métodos específicos: distinct, limit, skip.

¿Cómo funciona distinct?

El método distinct() devuelve un nuevo flujo en el que se han eliminado todos los duplicados de elementos. Los duplicados se determinan mediante los métodos equals y hashCode de la clase correspondiente.

Imagina que eres numismático y te importa que en la colección principal no haya duplicados. Todas las monedas repetidas irán al fondo de intercambio. Precisamente distinct() es capaz de filtrar los duplicados de la colección principal.

Ejemplo: nombres de usuario únicos

List<String> names = List.of(
    "Alicia", "Bob", "Alicia", "Eva", "Bob", "Denis", "Gleb", "Eva"
);

Obtengamos la lista de nombres únicos:

List<String> uniqueNames = names.stream()
    .distinct()
    .collect(Collectors.toList());

System.out.println(uniqueNames);
// Salida: [Alicia, Bob, Eva, Denis, Gleb]

Ejemplo: emails únicos de los usuarios

Supongamos que tenemos la clase User:

public class User {
    String name;
    String email;

    // Constructor, getters, toString() — para mayor comodidad
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    @Override
    public String toString() {
        return name + " <" + email + ">";
    }
}

Lista de usuarios con emails duplicados:

List<User> users = List.of(
    new User("Alicia", "alice@mail.com"),
    new User("Bob", "bob@mail.com"),
    new User("Eva", "eva@mail.com"),
    new User("Alicia2", "alice@mail.com"), // ¡email duplicado!
    new User("Gleb", "gleb@mail.com"),
    new User("Eva2", "eva@mail.com")      // ¡email duplicado!
);

Si llamas simplemente a users.stream().distinct(), los duplicados no se eliminarán, porque en los objetos User, por defecto, los métodos equals y hashCode no están sobrescritos. En este caso, distinct solo funciona para coincidencias por referencia.

Solución: Sobrescribir equals y hashCode de modo que la unicidad se determine 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);
}

Ahora:

List<User> uniqueUsers = users.stream()
    .distinct()
    .collect(Collectors.toList());

uniqueUsers.forEach(System.out::println);
// Alicia <alice@mail.com>
// Bob <bob@mail.com>
// Eva <eva@mail.com>
// Gleb <gleb@mail.com>

Importante: Para tus propias clases, sobrescribe siempre equals y hashCode si quieres que distinct funcione «como es debido».

2. Método limit: limitamos la cantidad de elementos

El método limit(long maxSize) devuelve un nuevo flujo que contiene como mucho los maxSize primeros elementos del flujo de origen.

Analogía: Has ido a una pastelería y quieres probar solo 3 pasteles de 100. limit(3) — y te dan justo los tres primeros; los demás, «hoy no toca».

Ejemplo: los 3 primeros nombres

List<String> firstThree = names.stream()
    .limit(3)
    .collect(Collectors.toList());

System.out.println(firstThree);
// Salida: [Alicia, Bob, Alicia]

Ejemplo con nuestra aplicación: top‑3 de usuarios nuevos

List<User> firstUsers = users.stream()
    .limit(3)
    .collect(Collectors.toList());

firstUsers.forEach(System.out::println);
// Alicia <alice@mail.com>
// Bob <bob@mail.com>
// Eva <eva@mail.com>

Ejemplo con ordenación

Se puede combinar con una ordenación — por ejemplo, tomar el top‑2 de usuarios con el email más corto:

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: omitimos los primeros elementos

El método skip(long n) devuelve un nuevo flujo en el que se omiten los n primeros elementos del flujo de origen.

Ejemplo: omitir los 2 primeros nombres

List<String> afterTwo = names.stream()
    .skip(2)
    .collect(Collectors.toList());

System.out.println(afterTwo);
// Salida: [Alicia, Eva, Bob, Denis, Gleb, Eva]

Ejemplo: navegación por páginas (pagination)

A menudo en un sitio hay que mostrar, por ejemplo, «3 usuarios por página». Para la segunda página hay que omitir los 3 primeros y tomar los 3 siguientes:

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);
// Alicia2 <alice@mail.com>
// Gleb <gleb@mail.com>
// Eva2 <eva@mail.com>

4. Combinación de distinct, limit, skip

Estos métodos se pueden y se deben combinar — según la tarea.

Ejemplo: obtener 2 nombres únicos empezando desde el tercero en orden

List<String> result = names.stream()
    .distinct() // Quitamos duplicados: [Alicia, Bob, Eva, Denis, Gleb]
    .skip(2)    // Omitimos a Alicia y a Bob: [Eva, Denis, Gleb]
    .limit(2)   // Tomamos solo dos: [Eva, Denis]
    .collect(Collectors.toList());

System.out.println(result);
// Salida: [Eva, Denis]

Ejemplo con filtrado

Supongamos que hay que obtener los 2 primeros emails únicos que contienen la 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);
// Salida: [alice@mail.com, eva@mail.com]

5. Práctica: ejercicios de aplicación

Ejercicio 1. Obtener la lista de nombres de usuario únicos cuya longitud sea mayor que 3 caracteres

List<String> longUniqueNames = names.stream()
    .filter(name -> name.length() > 3)
    .distinct()
    .collect(Collectors.toList());

System.out.println(longUniqueNames);
// Por ejemplo: [Alicia, Denis]

Ejercicio 2. Obtener el 3.º y el 4.º email único de la lista de usuarios

List<String> thirdAndFourthEmail = users.stream()
    .map(user -> user.email)
    .distinct()
    .skip(2)
    .limit(2)
    .collect(Collectors.toList());

System.out.println(thirdAndFourthEmail);
// Por ejemplo: [eva@mail.com, gleb@mail.com]

Ejercicio 3. Obtener los 5 primeros números únicos que sean mayores 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);
// Salida: [12, 17, 23, 42, 19]

6. Esquema visual: orden de aplicación de las operaciones

graph TD
    A[Lista original] --> B[filter]
    B --> C[distinct]
    C --> D[skip]
    D --> E[limit]
    E --> F[collect]

Comentario:
Normalmente primero se aplica filter, luego se quitan duplicados con distinct, después se usan skip y limit, y al final — collect. Pero el orden puede cambiarse si la tarea lo requiere.

7. Errores típicos al trabajar con distinct, limit, skip

Error n.º 1: esperar que distinct elimine duplicados por «cualquier» criterio.
En realidad, distinct funciona según el método equals del objeto. Si quieres unicidad por algún campo (por ejemplo, solo por email) y no por todo el objeto, debes o bien sobrescribir equals/hashCode, o usar trucos con Collectors.toMap() o un filtrado adicional.

Error n.º 2: romper el orden de las operaciones.
Si primero aplicas limit y después distinct, los duplicados pueden quedarse — al fin y al cabo limitaste el flujo a los primeros N elementos, y entre ellos puede haber iguales.

Error n.º 3: omitir con skip más elementos de los que hay.
Si intentas omitir más elementos de los que tiene el flujo, el resultado será simplemente una lista vacía — no habrá error, pero el resultado puede sorprender.

Error n.º 4: rendimiento no tenido en cuenta.
Los métodos distinct, limit, skip pueden ser ineficientes en flujos muy grandes, especialmente si el flujo no está ordenado o si los elementos son complejos. En el 99 % de los casos cotidianos no es un problema, pero si trabajas con millones de registros, conviene reflexionarlo.

Error n.º 5: olvidar la lógica sobrescrita de equals/hashCode en tus clases.
Si trabajas con objetos de usuario (por ejemplo, User), sin sobrescribir estos métodos, distinct considerará distintos a los objetos aunque sean «lógicamente» iguales.

1
Tarea
JAVA 25 SELF, nivel 30, lección 2
Bloqueada
Gestión de la cola para una fiesta exclusiva 🎉
Gestión de la cola para una fiesta exclusiva 🎉
1
Tarea
JAVA 25 SELF, nivel 30, lección 2
Bloqueada
Formación del Top-4 de indicadores de ventas únicos 💰
Formación del Top-4 de indicadores de ventas únicos 💰
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION