HashSet: El Club VIP de los Datos (Sin Duplicados)
Introducción: La fiesta exclusiva
¡Hola, futuros arquitectos de software! 🏗️
Hasta ahora hemos visto listas (ArrayList, LinkedList) donde podíamos guardar lo que quisiéramos: tres veces el número 5, diez veces el String "Hola", etc. Son como una hoja de papel donde escribes en orden.
Pero, ¿y si necesitamos controlar la unicidad? Imaginad que estáis programando el sistema de registro de usuarios de Instagram. ¿Puede haber dos usuarios con el mismo email? No. ¿Puede un usuario darle "Like" dos veces a la misma foto? No.
Aquí entra en juego el HashSet.
Pensad en un HashSet como en el portero de una discoteca VIP. Tiene una lista en la mano. Cuando intentas entrar (añadir un dato), el portero mira la lista. Si ya estás dentro, te dice: "Lo siento, ya estás en la fiesta, no puedes entrar dos veces". Si no estás, te deja pasar. Además, dentro de la fiesta no hay orden: la gente se mezcla, no están en fila india.
¡Vamos a aprender a gestionar la exclusividad con Java! 🕺💃
Conceptos Fundamentales
Definición: Set (Conjunto)
Es una colección que NO permite elementos duplicados. Modela la abstracción matemática de un conjunto finito. * Si intentas añadir un elemento que ya existe, la operación simplemente se ignora (o devuelve false).
Definición: HashSet
Es la implementación más popular de la interfaz Set.
* Hash: Utiliza una tabla hash internamente. Esto significa que usa una fórmula matemática (hash function) sobre el objeto para decidir dónde guardarlo en memoria.
* Rendimiento: Es extremadamente rápido (O(1)) para buscar, añadir y borrar. Mucho más que un ArrayList.
* Caos: NO garantiza ningún orden. Lo que insertas primero puede salir el último.
Requisito Vital: hashCode() y equals()
Para que un HashSet funcione con objetos propios (como una clase Alumno), esa clase DEBE tener implementados correctamente los métodos hashCode() y equals(). Si no, el portero de la discoteca no sabrá reconocer si dos alumnos son la misma persona.
Mapa Mental: El Universo Set
Desarrollo y Ejemplos Prácticos
Vamos a destripar los métodos de HashSet basándonos en la documentación y buenas prácticas.
1. Añadir Elementos y Gestionar Duplicados
El método estrella es add. Aquí es donde ocurre la magia del filtrado.
add(E e)
* Intenta añadir el elemento.
* Devuelve true si el elemento NO existía y se añadió.
* Devuelve false si el elemento YA existía (no se añade, no da error, solo avisa).
import java.util.HashSet;
public class RegistroEmails {
public static void main(String[] args) {
// Creamos un HashSet para guardar emails únicos.
// No definimos capacidad inicial, crece solo.
HashSet<String> emails = new HashSet<>();
// 1. Añadimos correos nuevos
boolean r1 = emails.add("alumno@fp.com");
System.out.println("¿Añadido 'alumno'? " + r1); // true
emails.add("profesor@fp.com");
emails.add("director@fp.com");
// 2. Intentamos añadir un DUPLICADO
// El HashSet detecta que el String es idéntico (usando hashCode y equals)
boolean r2 = emails.add("alumno@fp.com");
System.out.println("¿Añadido duplicado? " + r2); // false
// 3. Imprimimos
// Fíjate que "alumno@fp.com" solo aparece UNA vez.
// Y el orden puede no ser el de inserción.
System.out.println("Lista de emails: " + emails);
}
}
2. Comprobar Existencia (La Búsqueda Veloz)
Aquí es donde HashSet humilla a ArrayList. Buscar en un ArrayList de 1 millón de datos requiere recorrerlo (lento). Buscar en un HashSet es instantáneo, gracias al código hash (es como saber la dirección exacta de la casa sin mirar el mapa).
contains(Object o)
* Devuelve true si el elemento está en el conjunto.
isEmpty()
* Devuelve true si no hay elementos.
size()
* Devuelve el número de elementos únicos.
import java.util.HashSet;
public class ControlAcceso {
public static void main(String[] args) {
HashSet<String> idsPermitidos = new HashSet<>();
idsPermitidos.add("ID_001");
idsPermitidos.add("ID_999");
idsPermitidos.add("ID_555");
// --- Verificación Instantánea ---
String usuarioIntentandoEntrar = "ID_999";
// contains es O(1) - Rapidísimo
if (idsPermitidos.contains(usuarioIntentandoEntrar)) {
System.out.println("Acceso Concedido a " + usuarioIntentandoEntrar);
} else {
System.out.println("Acceso Denegado.");
}
// --- Estado del Conjunto ---
System.out.println("Usuarios totales permitidos: " + idsPermitidos.size());
System.out.println("¿Está el sistema vacío? " + idsPermitidos.isEmpty());
}
}
3. Eliminar y Limpiar
remove(Object o)
* Borra el elemento si existe. Devuelve true si lo borró, false si no estaba.
clear()
* Borra todo. Deja el conjunto como un solar.
import java.util.HashSet;
public class GestionInventario {
public static void main(String[] args) {
HashSet<String> carrito = new HashSet<>();
carrito.add("Manzanas");
carrito.add("Peras");
carrito.add("Plátanos");
// 1. Eliminar un elemento específico
boolean borrado = carrito.remove("Peras");
if (borrado) {
System.out.println("Peras eliminadas del carrito.");
} else {
System.out.println("No tenías peras.");
}
// Intentamos borrar algo que no existe
System.out.println("Borrando 'Unicornio': " + carrito.remove("Unicornio")); // false
System.out.println("Carrito actual: " + carrito);
// 2. Vaciar todo (Reset)
carrito.clear();
System.out.println("Tamaño tras clear: " + carrito.size()); // 0
}
}
4. Iteración (El Caos Ordenado)
iterator()
* Devuelve un objeto iterador para recorrer el conjunto.
* ¡ADVERTENCIA!: No confíes en el orden. Hoy puede salir "A, B, C" y mañana "C, A, B". Si necesitas orden, usa LinkedHashSet.
import java.util.HashSet;
import java.util.Iterator;
public class IteracionSet {
public static void main(String[] args) {
HashSet<Integer> loteria = new HashSet<>();
loteria.add(15);
loteria.add(90);
loteria.add(3);
loteria.add(45);
System.out.println("--- Usando Iterator Clásico ---");
// Obtenemos el iterador
Iterator<Integer> it = loteria.iterator();
while(it.hasNext()) {
Integer numero = it.next();
System.out.println("Bola: " + numero);
// El iterador permite borrar de forma segura mientras recorres:
if (numero == 90) {
it.remove(); // Borramos el 90
System.out.println("(El 90 ha sido anulado)");
}
}
System.out.println("--- Usando For-Each (Moderno) ---");
// Internamente usa el iterator
for (Integer num : loteria) {
System.out.print(num + " | ");
}
// Salida probable: 3 | 15 | 45 | (El orden es impredecible)
}
}
5. Clonado (Copia Superficial)
clone()
* Crea una copia superficial (shallow copy) del HashSet.
* Nota: Devuelve un Object, así que hay que hacer casting (HashSet<T>).
* Peligro: Si guardas objetos complejos (como Persona), se copian las referencias, no los objetos en sí. Si modificas la persona en la copia, se modifica en el original.
import java.util.HashSet;
public class ClonadoDemo {
public static void main(String[] args) {
HashSet<String> original = new HashSet<>();
original.add("Java");
original.add("Python");
// Hacemos el clon (casting necesario)
@SuppressWarnings("unchecked")
HashSet<String> copia = (HashSet<String>) original.clone();
System.out.println("Copia: " + copia);
// Modificar la copia no afecta a la estructura del original
copia.add("C++");
copia.remove("Java");
System.out.println("Original tras tocar copia: " + original); // Sigue igual
System.out.println("Copia modificada: " + copia);
}
}
Diagrama de Flujo: ¿Cómo funciona add()?
Este diagrama explica por qué es tan rápido. No recorre la lista, calcula la posición.
graph TD
A["Inicio: set.add 'Objeto'"] --> B["Calcular hashCode del Objeto"]
B --> C["Buscar esa posición en memoria 'Bucket'"]
C --> D["¿Está vacío el Bucket?"]
D -- Sí --> E["Guardar Objeto"]
E --> F["Fin: return true"]
D -- No (Colisión) --> G["Comparar con equals"]
G --> H["¿Son iguales?"]
H -- Sí --> I["Ya existe: No hacer nada"]
I --> J["Fin: return false"]
H -- No --> K["Guardar en el mismo bucket lista enlazada"]
K --> F
Tabla Comparativa: List vs Set
| Característica | ArrayList (List) | HashSet (Set) |
|---|---|---|
| Duplicados | ✅ Permitidos | ❌ Prohibidos |
| Orden | ✅ Mantiene orden de inserción | ❌ Caótico (Sin garantías) |
| Acceso | ✅ Por índice .get(5) |
❌ No tiene índice |
| Búsqueda | 🐢 Lenta O(n) | 🐇 Rapidísima O(1) |
| Uso ideal | Listas de la compra, rankings. | DNI únicos, correos, IDs. |
Ejercicios Reflexivos
- El misterio del orden: Ejecuta el código de iteración varias veces o con diferentes números. ¿Cambia el orden de salida? ¿Por qué crees que Java no gasta energía en ordenarlos?
- Objetos Mutables: Si añades un objeto
Persona("Pepe")a un HashSet, y luego cambias su nombre aPersona("Juan")... ¿Crees que el HashSet podrá encontrarlo concontainsoremove? (Pista: Si cambia el atributo, cambia el hash). - Rendimiento: Si tienes una lista de 10.000 palabras y quieres saber cuántas palabras únicas hay (sin repetir), ¿cómo lo harías en una sola línea de código usando el constructor de
HashSet?
Aplicación en el Mundo Real
- Filtros de Spam:
Los servidores de correo tienen un
HashSetgigante con direcciones IP de spammers conocidos. Cuando llega un correo, calculan el hash de la IP y miran si está en el conjuntocontains(). Es instantáneo. - Juegos Online (Matchmaking):
Imagina una partida de Fortnite. Necesitas 100 jugadores únicos. Usas un
HashSet<Jugador>para ir añadiendo a la sala de espera. Si un jugador pulsa "Unirse" dos veces por error, el Set lo ignora automáticamente. - Etiquetas (Tags) en Redes Sociales: En un post de Instagram o un vídeo de YouTube, las etiquetas (#java, #coding) no se repiten. Si pones #java tres veces, solo se guarda una. La estructura de datos subyacente es un Set.
Para Saber Más
-
Documentación Oficial de HashSet: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/HashSet.html
-
GeeksForGeeks - HashSet in Java: La fuente de inspiración de hoy, con más detalles. https://www.geeksforgeeks.org/java/hashset-in-java/
-
HashCode y Equals explicados: Fundamental si vas a usar Sets con tus propias clases. https://www.baeldung.com/java-equals-hashcode-contracts