Java
Tutorial Java: Conjuntos
Aprende la interfaz Set y la clase HashSet en Java para manejar colecciones sin duplicados y operaciones eficientes.
Aprende Java y certifícateLa interfaz Set
La interfaz Set es uno de los componentes fundamentales del Framework Collections de Java. Representa una colección de elementos que no permite duplicados, lo que la hace ideal para almacenar conjuntos de valores únicos.
A diferencia de las listas, un Set no mantiene un orden específico de elementos (con algunas excepciones como LinkedHashSet) y no proporciona acceso posicional a los elementos. La principal característica que define a un Set es que garantiza la unicidad de todos sus elementos.
Características principales de Set
- Sin duplicados: Cualquier intento de añadir un elemento que ya existe en el conjunto será ignorado.
- No indexado: No se puede acceder a los elementos por su posición.
- Operaciones de conjunto: Permite realizar operaciones matemáticas de conjuntos como unión, intersección y diferencia.
Métodos principales
La interfaz Set hereda de Collection y proporciona los siguientes métodos esenciales:
- add(E e): Añade un elemento al conjunto si no está presente.
- remove(Object o): Elimina un elemento específico del conjunto.
- contains(Object o): Verifica si el conjunto contiene un elemento específico.
- size(): Devuelve el número de elementos en el conjunto.
- isEmpty(): Verifica si el conjunto está vacío.
- clear(): Elimina todos los elementos del conjunto.
Implementaciones comunes
Java proporciona varias implementaciones de la interfaz Set:
- HashSet: Implementación basada en tablas hash, ofrece rendimiento constante para operaciones básicas.
- TreeSet: Implementación basada en árboles, mantiene los elementos ordenados.
- LinkedHashSet: Mantiene el orden de inserción mientras proporciona la unicidad de HashSet.
Creación y uso básico de un Set
Veamos cómo crear y utilizar un Set en Java:
import java.util.Set;
import java.util.HashSet;
public class EjemploSet {
public static void main(String[] args) {
// Creación de un conjunto
Set<String> frutas = new HashSet<>();
// Añadir elementos
frutas.add("Manzana");
frutas.add("Plátano");
frutas.add("Naranja");
frutas.add("Manzana"); // Este elemento no se añadirá por ser duplicado
// Mostrar el tamaño del conjunto
System.out.println("Tamaño del conjunto: " + frutas.size()); // Mostrará 3, no 4
// Recorrer el conjunto
System.out.println("Elementos del conjunto:");
for (String fruta : frutas) {
System.out.println(fruta);
}
// Verificar si un elemento existe
boolean contieneManzana = frutas.contains("Manzana");
System.out.println("¿Contiene Manzana? " + contieneManzana);
// Eliminar un elemento
frutas.remove("Plátano");
System.out.println("Después de eliminar Plátano: " + frutas);
}
}
Operaciones de conjuntos
Una de las ventajas de usar Sets es la posibilidad de realizar operaciones matemáticas de conjuntos:
import java.util.Set;
import java.util.HashSet;
public class OperacionesConjuntos {
public static void main(String[] args) {
// Crear dos conjuntos
Set<Integer> conjunto1 = new HashSet<>();
conjunto1.add(1);
conjunto1.add(2);
conjunto1.add(3);
Set<Integer> conjunto2 = new HashSet<>();
conjunto2.add(3);
conjunto2.add(4);
conjunto2.add(5);
// Crear copias para no modificar los originales
Set<Integer> union = new HashSet<>(conjunto1);
union.addAll(conjunto2); // Unión: {1, 2, 3, 4, 5}
Set<Integer> interseccion = new HashSet<>(conjunto1);
interseccion.retainAll(conjunto2); // Intersección: {3}
Set<Integer> diferencia = new HashSet<>(conjunto1);
diferencia.removeAll(conjunto2); // Diferencia: {1, 2}
// Mostrar resultados
System.out.println("Conjunto 1: " + conjunto1);
System.out.println("Conjunto 2: " + conjunto2);
System.out.println("Unión: " + union);
System.out.println("Intersección: " + interseccion);
System.out.println("Diferencia (conjunto1 - conjunto2): " + diferencia);
}
}
Uso práctico de Set
Los conjuntos son especialmente útiles en situaciones donde necesitamos:
- Eliminar duplicados de una colección existente
- Verificar la pertenencia de elementos de forma eficiente
- Representar conjuntos matemáticos como grupos de usuarios, permisos, etc.
Veamos un ejemplo práctico de eliminación de duplicados:
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class EliminarDuplicados {
public static void main(String[] args) {
// Lista con elementos duplicados
List<String> listaConDuplicados = new ArrayList<>();
listaConDuplicados.add("Madrid");
listaConDuplicados.add("Barcelona");
listaConDuplicados.add("Valencia");
listaConDuplicados.add("Madrid");
listaConDuplicados.add("Sevilla");
listaConDuplicados.add("Barcelona");
System.out.println("Lista original: " + listaConDuplicados);
// Eliminar duplicados usando un Set
Set<String> conjuntoSinDuplicados = new HashSet<>(listaConDuplicados);
// Convertir de nuevo a lista si es necesario
List<String> listaSinDuplicados = new ArrayList<>(conjuntoSinDuplicados);
System.out.println("Lista sin duplicados: " + listaSinDuplicados);
}
}
Consideraciones de rendimiento
Al elegir una implementación de Set, es importante considerar:
- HashSet: Ofrece rendimiento O(1) para operaciones básicas, pero no garantiza orden.
- TreeSet: Mantiene elementos ordenados, pero las operaciones son O(log n).
- LinkedHashSet: Mantiene orden de inserción con rendimiento similar a HashSet.
Para la mayoría de los casos de uso, HashSet es la implementación más eficiente y la elección por defecto cuando no se necesita un orden específico.
Ejemplo de uso con objetos personalizados
Para usar objetos personalizados en un Set, es importante sobrescribir los métodos equals() y hashCode() para garantizar la unicidad correcta:
import java.util.HashSet;
import java.util.Set;
public class EjemploSetObjetos {
public static void main(String[] args) {
Set<Estudiante> estudiantes = new HashSet<>();
estudiantes.add(new Estudiante(1, "Ana"));
estudiantes.add(new Estudiante(2, "Carlos"));
estudiantes.add(new Estudiante(1, "Ana")); // Mismo ID, no debería añadirse
System.out.println("Número de estudiantes: " + estudiantes.size()); // Debería ser 2
for (Estudiante e : estudiantes) {
System.out.println(e);
}
}
}
class Estudiante {
private int id;
private String nombre;
public Estudiante(int id, String nombre) {
this.id = id;
this.nombre = nombre;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Estudiante otro = (Estudiante) obj;
return id == otro.id; // Consideramos iguales si tienen el mismo ID
}
@Override
public int hashCode() {
return id; // El hashCode basado en el ID
}
@Override
public String toString() {
return "Estudiante{id=" + id + ", nombre='" + nombre + "'}";
}
}
La interfaz Set es una herramienta fundamental en Java para manejar colecciones de elementos únicos, ofreciendo una forma eficiente de eliminar duplicados y realizar operaciones de conjuntos matemáticos.
La clase HashSet
HashSet es la implementación más común y utilizada de la interfaz Set en Java. Esta clase almacena sus elementos en una tabla hash, lo que permite un acceso muy rápido a los datos y garantiza que no existan elementos duplicados dentro de la colección.
Características principales de HashSet
- Rendimiento eficiente: Las operaciones básicas como add, remove y contains se ejecutan en tiempo constante (O(1)) en promedio.
- Sin orden garantizado: No mantiene ningún orden específico de los elementos.
- Permite null: A diferencia de algunas otras colecciones, HashSet permite almacenar un único elemento null.
- No sincronizada: No es segura para uso en entornos multihilo sin sincronización externa.
Creación de un HashSet
Existen varias formas de crear un HashSet en Java:
// Crear un HashSet vacío
HashSet<String> ciudades = new HashSet<>();
// Crear un HashSet con capacidad inicial específica
HashSet<Integer> numeros = new HashSet<>(20);
// Crear un HashSet a partir de otra colección
ArrayList<String> listaPaises = new ArrayList<>();
listaPaises.add("España");
listaPaises.add("Francia");
listaPaises.add("Italia");
HashSet<String> paises = new HashSet<>(listaPaises);
Operaciones básicas con HashSet
Veamos las operaciones más comunes que podemos realizar con un HashSet:
import java.util.HashSet;
public class OperacionesHashSet {
public static void main(String[] args) {
HashSet<String> lenguajes = new HashSet<>();
// Añadir elementos
lenguajes.add("Java");
lenguajes.add("Python");
lenguajes.add("JavaScript");
lenguajes.add("C++");
// Intentar añadir un duplicado (será ignorado)
boolean añadido = lenguajes.add("Java");
System.out.println("¿Se añadió Java de nuevo? " + añadido); // Mostrará: false
// Verificar si contiene un elemento
System.out.println("¿Contiene Python? " + lenguajes.contains("Python")); // true
// Eliminar un elemento
lenguajes.remove("C++");
// Obtener el tamaño
System.out.println("Número de lenguajes: " + lenguajes.size()); // 3
// Recorrer el conjunto
System.out.println("Lenguajes de programación:");
for (String lenguaje : lenguajes) {
System.out.println("- " + lenguaje);
}
// Vaciar el conjunto
lenguajes.clear();
System.out.println("¿Está vacío? " + lenguajes.isEmpty()); // true
}
}
Funcionamiento interno de HashSet
Internamente, HashSet utiliza un HashMap para almacenar sus elementos. Cada elemento que se añade al HashSet se guarda como una clave en el HashMap subyacente, con un objeto ficticio como valor.
Cuando añadimos un elemento a un HashSet:
- Se calcula el código hash del elemento usando el método hashCode()
- Este código hash determina en qué "cubeta" (bucket) se almacenará el elemento
- Si ya existe un elemento en esa ubicación, se usa el método equals() para verificar si son iguales
- Si son iguales, el nuevo elemento no se añade (evitando duplicados)
Rendimiento de HashSet
El rendimiento de HashSet depende de dos factores principales:
- La capacidad inicial (número de buckets)
- El factor de carga (determina cuándo se redimensiona la tabla hash)
Por defecto, HashSet tiene un factor de carga de 0.75, lo que significa que cuando el 75% de su capacidad está ocupada, la tabla hash se redimensiona automáticamente.
// Crear un HashSet con capacidad inicial y factor de carga personalizados
HashSet<Double> valores = new HashSet<>(100, 0.8f);
Ejemplo práctico: Filtrar elementos duplicados
Un uso común de HashSet es eliminar elementos duplicados de una lista:
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
public class FiltrarDuplicados {
public static void main(String[] args) {
// Lista con elementos duplicados
List<Integer> numerosConDuplicados = new ArrayList<>();
numerosConDuplicados.add(10);
numerosConDuplicados.add(20);
numerosConDuplicados.add(10);
numerosConDuplicados.add(30);
numerosConDuplicados.add(20);
numerosConDuplicados.add(40);
System.out.println("Lista original: " + numerosConDuplicados);
// Filtrar duplicados con HashSet
HashSet<Integer> numerosSinDuplicados = new HashSet<>(numerosConDuplicados);
System.out.println("Lista sin duplicados: " + numerosSinDuplicados);
}
}
Uso de HashSet con objetos personalizados
Para usar objetos personalizados en un HashSet de manera efectiva, es crucial implementar correctamente los métodos equals() y hashCode(). Estos métodos determinan cómo se comparan los objetos y cómo se distribuyen en la tabla hash.
import java.util.HashSet;
import java.util.Objects;
public class HashSetConObjetos {
public static void main(String[] args) {
HashSet<Producto> productos = new HashSet<>();
productos.add(new Producto(1, "Laptop", 999.99));
productos.add(new Producto(2, "Teléfono", 599.99));
productos.add(new Producto(1, "Laptop", 1099.99)); // Mismo ID, precio diferente
System.out.println("Número de productos: " + productos.size()); // Mostrará 2
for (Producto p : productos) {
System.out.println(p);
}
}
}
class Producto {
private int id;
private String nombre;
private double precio;
public Producto(int id, String nombre, double precio) {
this.id = id;
this.nombre = nombre;
this.precio = precio;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Producto producto = (Producto) o;
return id == producto.id; // Consideramos iguales si tienen el mismo ID
}
@Override
public int hashCode() {
return Objects.hash(id); // Generamos el hash basado solo en el ID
}
@Override
public String toString() {
return "Producto{id=" + id + ", nombre='" + nombre + "', precio=" + precio + "}";
}
}
Comparación con otras implementaciones de Set
Es importante entender las diferencias entre HashSet y otras implementaciones de Set para elegir la más adecuada según tus necesidades:
- HashSet: Más rápido para la mayoría de operaciones, pero no mantiene ningún orden.
- LinkedHashSet: Mantiene el orden de inserción, con un rendimiento ligeramente inferior a HashSet.
- TreeSet: Mantiene los elementos ordenados, pero con operaciones más lentas (O(log n)).
import java.util.*;
public class ComparacionSets {
public static void main(String[] args) {
// Crear tres tipos diferentes de Set
Set<String> hashSet = new HashSet<>();
Set<String> linkedHashSet = new LinkedHashSet<>();
Set<String> treeSet = new TreeSet<>();
// Añadir elementos en el mismo orden a cada Set
for (Set<String> set : Arrays.asList(hashSet, linkedHashSet, treeSet)) {
set.add("C");
set.add("A");
set.add("B");
set.add("E");
set.add("D");
}
// Mostrar el orden de los elementos en cada Set
System.out.println("HashSet (sin orden garantizado): " + hashSet);
System.out.println("LinkedHashSet (orden de inserción): " + linkedHashSet);
System.out.println("TreeSet (orden natural): " + treeSet);
}
}
Casos de uso comunes para HashSet
HashSet es especialmente útil en los siguientes escenarios:
- Verificación de unicidad: Cuando necesitas asegurarte de que no hay duplicados.
- Operaciones de conjuntos: Para realizar uniones, intersecciones o diferencias entre colecciones.
- Búsquedas rápidas: Cuando necesitas verificar rápidamente si un elemento existe.
- Eliminación de duplicados: Como forma eficiente de eliminar elementos repetidos de otra colección.
import java.util.HashSet;
public class CasosUsoHashSet {
public static void main(String[] args) {
// Verificar si hay elementos duplicados en un array
String[] palabras = {"hola", "mundo", "java", "mundo", "programación"};
HashSet<String> conjuntoPalabras = new HashSet<>();
boolean hayDuplicados = false;
for (String palabra : palabras) {
if (!conjuntoPalabras.add(palabra)) {
System.out.println("Palabra duplicada encontrada: " + palabra);
hayDuplicados = true;
}
}
if (!hayDuplicados) {
System.out.println("No se encontraron palabras duplicadas");
}
}
}
HashSet proporciona una implementación eficiente y flexible de la interfaz Set, siendo la opción ideal cuando necesitas una colección que garantice elementos únicos y un rendimiento óptimo en las operaciones de búsqueda, inserción y eliminación.
Otros ejercicios de programación de Java
Evalúa tus conocimientos de esta lección Conjuntos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Streams: match
Gestión de errores y excepciones
CRUD en Java de modelo Customer sobre un ArrayList
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
API java.nio 2
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
CRUD en Java de modelo Customer sobre un HashMap
Interfaces
Enumeraciones Enums
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
CRUD de productos en Java
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Herencia
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Mapas
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
Todas las lecciones de Java
Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Java
Introducción Y Entorno
Configuración De Entorno Java
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Recursión
Sintaxis
Arrays Y Matrices
Sintaxis
Excepciones
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Sobrecarga De Métodos
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
La Clase Scanner
Programación Orientada A Objetos
Métodos De La Clase String
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Records
Programación Orientada A Objetos
Pattern Matching
Programación Orientada A Objetos
Inferencia De Tipos Con Var
Programación Orientada A Objetos
Enumeraciones Enums
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Clases Sealed
Programación Orientada A Objetos
Listas
Framework Collections
Conjuntos
Framework Collections
Mapas
Framework Collections
Funciones Lambda
Programación Funcional
Interfaz Funcional Consumer
Programación Funcional
Interfaz Funcional Predicate
Programación Funcional
Interfaz Funcional Supplier
Programación Funcional
Interfaz Funcional Function
Programación Funcional
Métodos Referenciados
Programación Funcional
Creación De Streams
Programación Funcional
Operaciones Intermedias Con Streams: Map()
Programación Funcional
Operaciones Intermedias Con Streams: Filter()
Programación Funcional
Operaciones Intermedias Con Streams: Distinct()
Programación Funcional
Operaciones Finales Con Streams: Collect()
Programación Funcional
Operaciones Finales Con Streams: Min Max
Programación Funcional
Operaciones Intermedias Con Streams: Flatmap()
Programación Funcional
Operaciones Intermedias Con Streams: Sorted()
Programación Funcional
Operaciones Finales Con Streams: Reduce()
Programación Funcional
Operaciones Finales Con Streams: Foreach()
Programación Funcional
Operaciones Finales Con Streams: Count()
Programación Funcional
Operaciones Finales Con Streams: Match
Programación Funcional
Api Optional
Programación Funcional
Transformación
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Mapeo
Programación Funcional
Streams Paralelos
Programación Funcional
Agrupación Y Partición
Programación Funcional
Filtrado Y Búsqueda
Programación Funcional
Api Java.nio 2
Entrada Y Salida Io
Fundamentos De Io
Entrada Y Salida Io
Leer Y Escribir Archivos
Entrada Y Salida Io
Httpclient Moderno
Entrada Y Salida Io
Clases De Nio2
Entrada Y Salida Io
Api Java.time
Api Java.time
Localtime
Api Java.time
Localdatetime
Api Java.time
Localdate
Api Java.time
Executorservice
Concurrencia
Virtual Threads (Project Loom)
Concurrencia
Future Y Completablefuture
Concurrencia
Spring Framework
Frameworks Para Java
Micronaut
Frameworks Para Java
Maven
Frameworks Para Java
Gradle
Frameworks Para Java
Lombok Para Java
Frameworks Para Java
Quarkus
Frameworks Para Java
Ecosistema Jakarta Ee De Java
Frameworks Para Java
Introducción A Junit 5
Testing
Certificados de superación de Java
Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la interfaz Set y sus características principales.
- Aprender a crear y manipular conjuntos usando Set y HashSet.
- Realizar operaciones matemáticas de conjuntos como unión, intersección y diferencia.
- Entender el funcionamiento interno y rendimiento de HashSet.
- Implementar correctamente equals() y hashCode() para usar objetos personalizados en conjuntos.