Inmutable vs mutable

Kotlin distingue claramente entre colecciones de solo lectura y mutables. Por default, las funciones de creación retornan colecciones inmutables:

// Inmutables — solo lectura, no se pueden agregar ni quitar elementos
val lista = listOf(1, 2, 3)
val mapa = mapOf("a" to 1, "b" to 2)
val conjunto = setOf("Ana", "Carlos")

// Mutables — permiten agregar, quitar y modificar
val listaMutable = mutableListOf(1, 2, 3)
val mapaMutable = mutableMapOf("a" to 1)
val conjuntoMutable = mutableSetOf("Ana")

listaMutable.add(4)          // OK
listaMutable.remove(1)       // OK
lista.add(4)                 // ERROR de compilación

Preferí colecciones inmutablesIgual que con val, empezá con listOf. Si necesitás mutar la colección, usá mutableListOf. Exponer List (inmutable) en las interfaces públicas y mantener MutableList como implementación privada es el patrón correcto — exactamente como hacemos con StateFlow y MutableStateFlow.

List

val productos = listOf("Tablet", "Phone", "Watch", "Tablet")

// Acceso
println(productos[0])          // Tablet
println(productos.first())     // Tablet
println(productos.last())      // Tablet
println(productos.size)        // 4
println(productos.isEmpty())   // false

// Búsqueda
println(productos.contains("Phone"))       // true
println("Watch" in productos)              // true (sintaxis alternativa)
println(productos.indexOf("Tablet"))       // 0
println(productos.lastIndexOf("Tablet"))   // 3

// Sublistas
println(productos.subList(1, 3))  // [Phone, Watch]
println(productos.take(2))        // [Tablet, Phone]
println(productos.drop(2))        // [Watch, Tablet]

// Mutable
val carrito = mutableListOf("Tablet")
carrito.add("Phone")
carrito.add(0, "Watch")   // insertar en posición
carrito.remove("Phone")
carrito[0] = "Laptop"     // modificar por índice
println(carrito)          // [Laptop, Tablet]

Map

val precios = mapOf(
    "Tablet" to 299.0,
    "Phone" to 499.0,
    "Watch" to 199.0
)

// Acceso
println(precios["Tablet"])           // 299.0 (retorna nullable)
println(precios.getValue("Tablet"))  // 299.0 (lanza excepción si no existe)
println(precios.getOrDefault("Laptop", 0.0))  // 0.0

// Iteración
for ((producto, precio) in precios) {
    println("$producto: $$precio")
}

precios.forEach { (producto, precio) ->
    println("$producto: $$precio")
}

// Consultas
println(precios.keys)             // [Tablet, Phone, Watch]
println(precios.values)           // [299.0, 499.0, 199.0]
println(precios.containsKey("TV"))  // false
println(precios.size)             // 3

// Mutable
val stock = mutableMapOf("Tablet" to 10, "Phone" to 5)
stock["Watch"] = 20         // agregar o actualizar
stock.remove("Phone")       // eliminar
stock["Tablet"] = stock.getOrDefault("Tablet", 0) + 1  // incrementar

Set

// Set — colección sin duplicados ni orden garantizado
val categorias = setOf("Android", "Kotlin", "Android", "Compose")
println(categorias)  // [Android, Kotlin, Compose] — sin duplicado

// Operaciones de conjuntos
val a = setOf(1, 2, 3, 4)
val b = setOf(3, 4, 5, 6)

println(a union b)        // [1, 2, 3, 4, 5, 6]
println(a intersect b)    // [3, 4]
println(a subtract b)     // [1, 2]

Operaciones funcionales — el corazón

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

val productos = listOf(
    Producto("Tablet", 299.0, "Electronics", 15),
    Producto("Phone", 499.0, "Electronics", 8),
    Producto("Remera", 29.0, "Ropa", 50),
    Producto("Libro", 19.0, "Libros", 100),
    Producto("Laptop", 999.0, "Electronics", 3)
)

// map — transformar cada elemento
val nombres = productos.map { it.nombre }
// [Tablet, Phone, Remera, Libro, Laptop]

val preciosConIva = productos.map { it.copy(precio = it.precio * 1.21) }

// filter — quedarse con los que cumplen la condición
val caros = productos.filter { it.precio > 100 }
val conStock = productos.filter { it.stock > 0 }

// find / firstOrNull — primer elemento que cumple la condición (o null)
val primerElectronico = productos.find { it.categoria == "Electronics" }
val muyBarato = productos.firstOrNull { it.precio < 10 }  // null

// any / all / none
println(productos.any { it.precio > 500 })   // true
println(productos.all { it.stock > 0 })      // true
println(productos.none { it.precio < 0 })    // true

// count
println(productos.count { it.categoria == "Electronics" })  // 3

// sortedBy / sortedByDescending
val porPrecio = productos.sortedBy { it.precio }
val porPrecioDesc = productos.sortedByDescending { it.precio }

// groupBy — agrupar por criterio, retorna Map
val porCategoria = productos.groupBy { it.categoria }
// { "Electronics" -> [Tablet, Phone, Laptop], "Ropa" -> [Remera], ... }

// sumOf / maxByOrNull / minByOrNull
val totalInventario = productos.sumOf { it.precio * it.stock }
val masBarato = productos.minByOrNull { it.precio }
val masCaro = productos.maxByOrNull { it.precio }

// partition — divide en dos listas según predicado
val (caros2, baratos) = productos.partition { it.precio > 100 }

// flatMap — map + flatten
val tags = productos.flatMap { listOf(it.nombre, it.categoria) }

// distinct / distinctBy
val categoriasSinRepetir = productos.map { it.categoria }.distinct()
val unosPorCategoria = productos.distinctBy { it.categoria }

Encadenamiento de operaciones

Las operaciones se pueden encadenar de forma legible — cada una recibe la salida de la anterior:

// Obtener los nombres de los electrónicos con stock, ordenados por precio, los 3 más baratos
val resultado = productos
    .filter { it.categoria == "Electronics" }
    .filter { it.stock > 0 }
    .sortedBy { it.precio }
    .take(3)
    .map { "${it.nombre} ($${"%.2f".format(it.precio)})" }

println(resultado)
// [Tablet ($299.00), Phone ($499.00), Laptop ($999.00)]

Sequences — lazy evaluation

Las operaciones sobre colecciones normales son eager — cada operación procesa todos los elementos y crea una nueva colección. Para listas grandes, las Sequences son lazy — procesan elemento a elemento:

// Eager — crea 3 listas intermedias
val resultado = (1..1_000_000)
    .filter { it % 2 == 0 }   // nueva lista de 500_000 elementos
    .map { it * it }           // nueva lista de 500_000 elementos
    .take(5)                   // nueva lista de 5 elementos

// Lazy con Sequence — procesa elemento a elemento, para cuando tiene 5
val resultadoLazy = (1..1_000_000).asSequence()
    .filter { it % 2 == 0 }
    .map { it * it }
    .take(5)
    .toList()   // aquí se materializa el resultado

// Para colecciones pequeñas (<1000 elementos): usá la API normal
// Para colecciones grandes o cadenas largas: considerá asSequence()