Kotlin: Estructuras de datos

Kotlin ofrece estructuras de datos clave para el desarrollo eficiente de aplicaciones. Descubre cómo implementar y utilizar listas, conjuntos y mapas en Kotlin para optimizar tu código.

Aprende Kotlin GRATIS y certifícate

Las estructuras de datos son componentes fundamentales en la programación, permitiendo organizar y manipular datos de manera eficiente. Kotlin, como lenguaje moderno y versátil, proporciona una amplia variedad de estructuras de datos que facilitan el desarrollo de aplicaciones robustas. En este artículo, exploraremos las principales estructuras de datos en Kotlin y cómo utilizarlas correctamente.

Listas en Kotlin

Las listas son colecciones ordenadas que permiten almacenar elementos en una secuencia. Kotlin ofrece dos tipos principales de listas: listas inmutables y listas mutables.

Listas inmutables

Las listas inmutables no permiten modificar sus elementos después de la creación. Se crean utilizando la función listOf().

val numeros = listOf(1, 2, 3, 4, 5)

Características de las listas inmutables:

  • Lectura segura: al ser inmutables, no hay riesgo de modificar accidentalmente los elementos.
  • Uso compartido: se pueden compartir entre diferentes partes del programa sin preocuparse por modificaciones.

Operaciones con listas inmutables

Aunque no se pueden modificar, es posible realizar varias operaciones que devuelven nuevas listas:

  • Añadir elementos:
  val nuevosNumeros = numeros + 6
  • Eliminar elementos:
  val menosNumeros = numeros - 1

Listas mutables

Las listas mutables permiten modificar su contenido después de la creación. Se crean utilizando la función mutableListOf().

val frutas = mutableListOf("Manzana", "Banana", "Cereza")
frutas.add("Durazno")

Operaciones habituales:

  • Agregar elementos:
  frutas.add("Mango")
  • Eliminar elementos:
  frutas.remove("Banana")
  • Modificar elementos:
  frutas[0] = "Fresa"

Conjuntos en Kotlin

Los conjuntos son colecciones que no permiten elementos duplicados. Son útiles cuando se necesita garantizar la unicidad de los elementos.

Conjuntos inmutables

Se crean utilizando setOf().

val colores = setOf("Rojo", "Verde", "Azul")

Características:

  • Sin duplicados: añadir un elemento ya existente no cambia el conjunto.
  • Orden no garantizado: el orden de los elementos puede no ser el mismo al iterar.

Conjuntos mutables

Se crean con mutableSetOf().

val numerosSet = mutableSetOf(1, 2, 3)
numerosSet.add(4)

Operaciones comunes:

  • Agregar elementos:
  numerosSet.add(2) // No se agregará ya que 2 ya existe
  • Eliminar elementos:
  numerosSet.remove(3)

Mapas en Kotlin

Los mapas almacenan pares clave-valor, permitiendo asociar valores a claves únicas.

Mapas inmutables

Se crean con mapOf().

val capitales = mapOf("España" to "Madrid", "Francia" to "París")

Características:

  • Claves únicas: no puede haber claves duplicadas.
  • Valores asociados: cada clave tiene un valor correspondiente.

Mapas mutables

Se crean con mutableMapOf().

val edades = mutableMapOf("Juan" to 30, "María" to 25)
edades["Pedro"] = 40

Operaciones habituales:

  • Agregar o modificar entradas:
  edades["María"] = 26 // Modifica el valor existente
  • Eliminar entradas:
  edades.remove("Juan")
  • Acceder a valores:
  val edadDeMaría = edades["María"]

Colecciones y nullabilidad

Kotlin maneja la nullabilidad de forma segura, lo que se extiende a las colecciones.

Listas que permiten nulos

Es posible crear listas que contengan elementos nulos:

val listaConNulos = listOf("A", null, "B")

Para manejar nulos al iterar:

for (elemento in listaConNulos) {
    elemento?.let {
        println(it)
    }
}

Operaciones funcionales en colecciones

Kotlin incorpora funciones de estilo funcional para manipular colecciones de manera concisa y expresiva.

Transformaciones con map

La función map transforma cada elemento de una colección y devuelve una nueva colección.

val numeros = listOf(1, 2, 3)
val cuadrados = numeros.map { it * it } // [1, 4, 9]

Filtrado con filter

La función filter selecciona elementos que cumplen una condición.

val numerosPares = numeros.filter { it % 2 == 0 } // [2]

Combinación de operaciones

Se pueden encadenar múltiples operaciones:

val resultado = numeros
    .filter { it > 1 }
    .map { it * 2 }
    .sortedDescending()

Secuencias para procesamiento perezoso

Las secuencias diferencian de las colecciones en que realizan el procesamiento de forma perezosa, lo cual puede ser más eficiente con grandes cantidades de datos.

Creación de secuencias

Se puede convertir una colección en secuencia:

val numerosSecuencia = numeros.asSequence()

O crear una secuencia desde cero:

val secuenciaInfinita = generateSequence(0) { it + 1 }

Uso eficiente de secuencias

Las secuencias son especialmente útiles cuando se encadenan muchas operaciones:

val primerosDiezMil = secuenciaInfinita
    .filter { it % 2 == 0 }
    .map { it * it }
    .take(10000)
    .toList()

En este caso, las operaciones se realizan solo sobre los primeros 10,000 elementos necesarios.

Estructuras de datos personalizadas

Kotlin permite definir estructuras de datos propias mediante clases y otros mecanismos.

Clases de datos

Las clases de datos (data class) son ideales para almacenar información:

data class Producto(val nombre: String, val precio: Double)

Uso en colecciones

Se pueden crear colecciones de objetos:

val productos = listOf(
    Producto("Ordenador", 999.99),
    Producto("Teléfono", 499.49),
    Producto("Tablet", 299.99)
)

Operaciones avanzadas

  • Suma de valores:
  val precioTotal = productos.sumOf { it.precio }
  • Encontrar un elemento:
  val productoCaro = productos.find { it.precio > 500 }
  • Agrupar elementos:
  val productosPorPrecio = productos.groupBy {
      when {
          it.precio < 500 -> "Barato"
          else -> "Caro"
      }
  }

Inmutabilidad y concurrencia

La inmutabilidad es especialmente beneficiosa en entornos concurrentes.

Colecciones inmutables y hilos

Al utilizar colecciones inmutables, se evita la necesidad de sincronización explícita:

val datosCompartidos = listOf(1, 2, 3)

// Acceso seguro desde múltiples hilos

Colecciones en coroutines

Kotlin ofrece soporte para programación asíncrona mediante coroutines, lo que impacta en el uso de colecciones.

Flows y recolección de datos

Los Flow permiten manejar flujos de datos asíncronos.

import kotlinx.coroutines.flow.*

val flujoDatos = flow {
    for (i in 1..5) {
        emit(i)
    }
}

flujoDatos.collect { valor ->
    println(valor)
}

Transformaciones en Flows

Se pueden aplicar operaciones similares a las colecciones:

flujoDatos
    .filter { it % 2 == 0 }
    .map { it * it }
    .collect { println(it) }

Buenas prácticas con colecciones en Kotlin

  • Preferir inmutabilidad: usar colecciones inmutables por defecto.
  • Elegir la estructura adecuada: seleccionar entre listas, conjuntos o mapas según las necesidades.
  • Utilizar operaciones funcionales: aprovechar map, filter, reduce, etc., para escribir código conciso.
  • Considerar secuencias: usar secuencias para procesamiento de grandes datasets.
  • Gestionar la nullabilidad: prestar atención a elementos nulos en colecciones.
Empezar curso de Kotlin

Lecciones de este módulo de Kotlin

Lecciones de programación del módulo Estructuras de datos del curso de Kotlin.

Ejercicios de programación en este módulo de Kotlin

Evalúa tus conocimientos en Estructuras de datos con ejercicios de programación Estructuras de datos de tipo Test, Puzzle, Código y Proyecto con VSCode.