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()