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.
GO TO FULL VERSION