Después de haber explorado las listas, es momento de adentrarnos en otro tipo de estructura fundamental dentro de la API de colecciones: los sets. Estos conjuntos, que quizás nos resulten familiares si hemos trabajado con ellos en otros lenguajes o en matemáticas, tienen una característica esencial que los define: almacenan elementos únicos, sin permitir duplicados.
Crear un set es sencillo. Podemos inicializarlo vacío o con una serie de elementos, por ejemplo, 1, 2, 3, 4. Sin embargo, si intentamos añadir un elemento que ya existe, como un 3 o un 2 repetido, el set simplemente ignorará ese intento, manteniendo únicamente los valores únicos. Esto contrasta con las listas, donde sí se permiten duplicados.
Los sets cuentan con diversas operaciones útiles. Algunas son similares a las que ya conocemos de las listas, como isEmpty o nonEmpty. Pero hay una diferencia importante en la operación apply. Cuando usamos la sintaxis set(elemento), en realidad estamos invocando la función contains, que nos devuelve un valor booleano indicando si el elemento está presente en el conjunto o no. Por ejemplo, si tenemos un set con los elementos 1, 2, 3 y preguntamos si contiene el 3, la respuesta será true; si preguntamos por el 4, será false.
En cuanto a modificar sets, es fundamental entender que, por defecto, son inmutables. Esto significa que al añadir un elemento con la operación +, no estamos cambiando el set original, sino que obtenemos un nuevo set que incluye ese elemento, siempre que no sea un duplicado. Por ejemplo, si hacemos:
val set = Set(1, 2, 3)
val nuevoSet = set + 4 // nuevoSet contiene 1, 2, 3, 4
val mismoSet = set + 3 // mismoSet es igual a set, ya que 3 ya estaba
Además, podemos concatenar dos sets con la operación ++, que nos devuelve un nuevo set con la unión de ambos conjuntos, sin importar el orden ni la repetición de elementos. Por ejemplo:
val set1 = Set(1, 2, 3)
val set2 = Set(4, 5, 6)
val unionSet = set1 ++ set2 // unionSet contiene 1, 2, 3, 4, 5, 6
Para eliminar elementos, disponemos de la operación -, que nos devuelve un nuevo set sin el elemento indicado. Si intentamos eliminar un elemento que no está presente, el set resultante será igual al original. También existe la operación --, que permite eliminar varios elementos a la vez, pasando otro set con los elementos a eliminar.
Los sets también ofrecen operaciones clásicas de teoría de conjuntos, como la unión, intersección y diferencia. Por ejemplo, podemos usar métodos como intersect, union y diff para obtener conjuntos resultantes de estas operaciones. Además, existen operadores más cómodos para estas tareas: el operador & representa la intersección, | la unión y &~ la diferencia. Así, si tenemos:
val setA = Set(1, 2, 3)
val setB = Set(3, 4)
val interseccion = setA & setB // contiene 3
val union = setA | setB // contiene 1, 2, 3, 4
val diferencia = setA &~ setB // contiene 1, 2
Por último, es importante destacar que, aunque por defecto los sets son inmutables, también podemos crear sets mutables cuando necesitemos modificar la colección directamente sin crear nuevas instancias. Esto nos permite trabajar con inserciones y eliminaciones que alteran el conjunto original, algo que veremos con más detalle al crear sets mutables y comparar su comportamiento con los inmutables.