DevTools

Cheatsheet Kotlin

Linguagem moderna para Android, backend e multiplataforma

Volver a los lenguajes
Kotlin
95 tarjetas encontradas
Categorías:
Versiones:

Básico e Tipos


11 cards
Hello World
fun main() {
    println("¡Hola, Mundo!")
}

// Sin clase obligatoria (diferente de Java)
// main no necesita args ni tipo de retorno

El punto de entrada es la función main — no necesita clase envolvente ni tipo de retorno declarado. println escribe una línea en stdout. El compilador lo infiere todo automáticamente.

Null safety
var nombre: String? = null // tipo nullable

val len = nombre?.length     // safe call (null si null)
val tam = nombre?.length ?: 0 // elvis: default si null
val n = nombre!!.length      // fuerza (NPE si null)

// Se ejecuta solo si no es null:
nombre?.let { println(it) }

El sistema de tipos elimina NullPointerException en compilación. El ? marca tipos nullable, el ?. encadena llamadas con seguridad y el ?: (elvis) proporciona defaults. let{} ejecuta bloques solo cuando el valor existe.

Operadores
// Aritméticos:
+ - * / %
val r = 10 / 3   // 3 (entero)

// Comparación y lógicos:
==  !=  >  <  >=  <=
&&  ||  !

// Range y pertenencia:
1..10        // range
5 in 1..10   // true

// Asignación compuesta:
+=  -=  *=  /=  %=

// No hay ++/-- prefijo ambiguo

La división entera trunca. El operador in verifica pertenencia a ranges y colecciones. == compara por valor (como equals en Java); === compara referencias. No hay conversiones automáticas en expresiones mixtas.

Variables (val y var)
val nombre = "Kotlin" // inmutable (read-only)
var edad = 10         // mutable
edad = 11             // OK

val pi: Double = 3.14 // tipo explícito

// Inferencia de tipo:
val x = 42            // Int
val s = "texto"       // String

Usa val por defecto (inmutable) para evitar bugs; usa var solo cuando el valor deba cambiar. El tipo se infiere a partir del valor, pero puede anotarse explícitamente con : Tipo.

Arrays
val nums = arrayOf(1, 2, 3, 4, 5)
val zeros = IntArray(5) { 0 }
val pares = Array(5) { i -> i * 2 }

println(nums[0])    // 1
println(nums.size)  // 5
nums[0] = 10        // mutable

// Iterar:
for (n in nums) println(n)

Los arrays tienen tamaño fijo tras la creación. IntArray/DoubleArray son versiones optimizadas sin boxing. La lambda del constructor inicializa cada posición. Para colecciones dinámicas prefiere List/MutableList.

lateinit y lazy
// lateinit: inicialización tardía (var)
class Activity {
    lateinit var db: Database

    fun onCreate() {
        db = Database.abrir()
    }
}

// lazy: se calcula en el 1.er acceso (val)
val config: Config by lazy {
    Config.cargarDeArchivo()
}

lateinit declara propiedades no nulas inicializadas después (ej: inyección de dependencias) — acceder antes lanza excepción. by lazy aplaza el cálculo de un val hasta el primer acceso (thread-safe por defecto).

Tipos de datos
val entero: Int = 42
val largo: Long = 100_000_000L
val decimal: Double = 3.14
val flag: Boolean = true
val letra: Char = 'A'
val texto: String = "Kotlin"
val nada: Unit = Unit

// Underscore para legibilidad:
val millon = 1_000_000

Kotlin trata los primitivos como objetos (sin distinción int/Integer como en Java). El underscore en números mejora la legibilidad. Unit equivale al void de Java pero es un tipo real.

Type aliases
typealias UserId = Long
typealias Callback = (String) -> Unit
typealias Tabla = Map<String, List<Any>>

fun registrar(id: UserId, cb: Callback) {
    cb("usuario $id registrado")
}

registrar(42L) { println(it) }

typealias crea nombres legibles para tipos complejos — genial para callbacks, IDs y mapas anidados. No crea un tipo nuevo (es solo un atajo), pero mejora mucho la legibilidad de las firmas.

Nothing y Any
// Any: super-tipo de todos
val obj: Any = 42
val obj2: Any = "texto"

// Nothing: función que nunca retorna
fun fallar(msg: String): Nothing {
    throw IllegalStateException(msg)
}

// Útil en expresiones:
val x = valor ?: fallar("sin valor")
// aquí x es no-null

Any es el super-tipo de todos los tipos (como Object en Java). Nothing indica que una función nunca retorna normalmente (lanza excepción o bucle infinito) — ayuda al compilador a inferir nullability tras ?:.

Strings y templates
val nombre = "Mundo"
val saludo = "¡Hola, $nombre!"
val template = "2 + 2 = ${2 + 2}"

// Multilínea (triple-quoted):
val multi = """
    |Línea 1
    |Línea 2
""".trimMargin()

// Sin escape de comillas:
val json = """{"nombre": "$nombre"}"""

Los string templates con $ evitan la concatenación; para expresiones usa ${}. Las triple-quoted strings (\"\"\") preservan el formato y son ideales para SQL/JSON — trimMargin() elimina la indentación.

Conversiones de tipos
val x = 42
val s = x.toString()    // Int -> String
val n = "123".toInt()   // String -> Int
val d = x.toDouble()    // Int -> Double
val l = x.toLong()      // Int -> Long

// Sin conversiones implícitas:
// val d: Double = x  // ¡ERROR!
val d2: Double = x.toDouble()

// toIntOrNull (seguro):
val inv = "abc".toIntOrNull() // null

Kotlin no hace conversiones numéricas implícitas — usa toInt(), toDouble(), etc. explícitamente. toIntOrNull() devuelve null en vez de lanzar excepción si el string no es un número válido.

Controlo de Fluxo


10 cards
If como expresión
// if retorna valor:
val max = if (a > b) a else b

// Con bloques:
val msg = if (nota >= 10) {
    println("Aprobado")
    "OK"
} else {
    "Reprobado"
}

// No existe operador ternario ?:
// Usa if-else como expresión

En Kotlin el if es una expresión que retorna un valor — sustituye al operador ternario (que no existe). El último statement de cada bloque es el valor retornado. Hace el código más declarativo.

While y do-while
var n = 10
while (n > 0) {
    n--
}

// do-while se ejecuta al menos 1 vez:
do {
    val input = readLine()
    println("Leído: $input")
} while (input != "salir")

El while verifica la condición antes de ejecutar (puede no ejecutarse nunca). El do-while se ejecuta al menos una vez — ideal para menús y validación de input donde necesitas leer antes de verificar.

Smart casts
fun procesar(obj: Any) {
    if (obj is String) {
        // obj ya es String aquí:
        println(obj.length)
        println(obj.uppercase())
    }

    if (obj !is List<*>) return
    // obj es List a partir de aquí:
    println(obj.size)
}

El compilador hace smart cast automático tras verificaciones is/!is — dentro del bloque, la variable se trata como el tipo testeado sin cast manual. Solo funciona con val locales o propiedades inmutables.

When
val resultado = when (x) {
    1 -> "uno"
    2, 3 -> "dos o tres"
    in 4..10 -> "entre 4 y 10"
    !in 20..30 -> "fuera de 20-30"
    else -> "otro"
}

// when es expresión (retorna valor)
// else es obligatorio si no es exhaustivo

El when sustituye al switch de Java con patrones más potentes: múltiples valores separados por coma, ranges con in, negación con !in. Es una expresión que retorna valor.

Break, continue y labels
for (i in 1..10) {
    if (i == 3) continue // salta el 3
    if (i == 7) break    // termina en el 7
    print(i)             // 1 2 4 5 6
}

// Labels para bucles anidados:
outer@ for (i in 1..3) {
    for (j in 1..3) {
        if (i * j > 4) break@outer
    }
}

continue salta a la siguiente iteración, break termina el bucle. Los labels (outer@) permiten controlar bucles anidados — break@outer sale directamente del bucle exterior.

return, break en lambdas
fun buscar(lista: List<Int>) {
    lista.forEach {
        if (it == 3) return  // ¡retorna de la función!
    }
}

// Label para retornar solo del lambda:
lista.forEach loop@{
    if (it == 3) return@loop // solo de este lambda
    println(it)
}

// Nombre implícito del lambda:
lista.forEach {
    if (it > 5) return@forEach
}

Un return dentro de un lambda retorna de la función envolvente (non-local return). Para retornar solo del lambda usa labels (return@forEach). Esto es esencial para controlar el flujo en funciones de orden superior.

When avanzado
// Sin argumento (cadena if-else):
val msg = when {
    x < 0 -> "negativo"
    x == 0 -> "cero"
    else -> "positivo"
}

// Con tipos (smart cast):
fun tipo(obj: Any) = when (obj) {
    is String -> "Texto: ${obj.length}"
    is Int -> "Número: $obj"
    is List<*> -> "Lista: ${obj.size}"
    else -> "?"
}

El when sin argumento funciona como cadena if-else más legible. Con is, Kotlin hace smart cast automático — dentro del branch, obj ya se trata como el tipo testeado sin cast manual.

Ranges y progresiones
val r1 = 1..10        // IntRange (inclusivo)
val r2 = 1 until 10   // 1..9 (excluye el último)
val r3 = 'a'..'z'     // CharRange
val r4 = 10 downTo 1  // decreciente

if (5 in r1) println("contiene")
for (c in 'a'..'f') print(c)  // abcdef

val lista = (1..5).toList()   // [1,2,3,4,5]

Los ranges son objetos Iterable que soportan iteración y verificación de pertenencia (in). until excluye el último elemento (útil para índices). Funcionan con Int, Long y Char.

Bucles for y ranges
for (i in 1..5) print(i)         // 1 2 3 4 5
for (i in 5 downTo 1) print(i)   // 5 4 3 2 1
for (i in 0..10 step 2) print(i) // 0 2 4 6 8 10
for (i in 1 until 5) print(i)    // 1 2 3 4

// Con índice y valor:
for ((i, v) in lista.withIndex()) {
    println("$i: $v")
}

Los ranges (..) crean secuencias inclusivas. downTo invierte la dirección, step define el incremento y until excluye el último. withIndex() da acceso al índice y al valor simultáneamente.

Expresiones (try/when)
// try como expresión:
val n = try {
    "42".toInt()
} catch (e: NumberFormatException) {
    0
}

// when como expresión:
val code = when (status) {
    "ok" -> 200
    else -> 500
}

// if como expresión:
val max = if (a > b) a else b

A diferencia de Java, if/when/try son expresiones que retornan valores en Kotlin. Esto elimina variables temporales y hace el código más declarativo. El último statement de cada bloque es el valor.

Funções e Lambdas


11 cards
Declaración de funciones
fun suma(a: Int, b: Int): Int {
    return a + b
}

// Cuerpo de expresión (retorno inferido):
fun doble(x: Int) = x * 2

// Sin retorno útil (Unit):
fun saludar(nombre: String) {
    println("¡Hola, $nombre!")
}

Las funciones con cuerpo de expresión (=) son más concisas y el tipo de retorno se infiere. El Unit (sin retorno) puede omitirse. Los tipos de los parámetros son siempre obligatorios.

Extension functions
fun String.capitalize(): String =
    replaceFirstChar { it.uppercase() }

fun Int.isEven(): Boolean = this % 2 == 0

fun List<Int>.segundo(): Int? =
    if (size >= 2) this[1] else null

// Uso:
"kotlin".capitalize() // "Kotlin"
4.isEven()            // true
listOf(1, 2).segundo() // 2

Las extension functions añaden métodos a clases existentes sin herencia ni modificación del código fuente. Se resuelven estáticamente en compilación. Ideales para utilidades y APIs más expresivas vía fun Tipo.método().

Operator overloading
data class Vec(val x: Int, val y: Int)

operator fun Vec.plus(o: Vec) =
    Vec(x + o.x, y + o.y)

operator fun Vec.times(n: Int) =
    Vec(x * n, y * n)

val v = Vec(1, 2) + Vec(3, 4)  // Vec(4, 6)
val d = Vec(1, 2) * 3          // Vec(3, 6)

El operator overloading permite usar +, -, *, [] con tipos personalizados. Basta marcar la función con operator y usar el nombre convencional (plus, times, get).

Default y named args
fun greet(nombre: String, prefijo: String = "Hola") =
    "$prefijo, $nombre!"

greet("Ana")                // "Hola, Ana!"
greet("Ana", "Buenos días") // "Buenos días, Ana!"

// Named arguments:
fun config(host: String, port: Int = 80, ssl: Boolean = false) {}
config("localhost", ssl = true) // port default 80

Los parámetros con default eliminan sobrecargas — la misma función sirve para varias llamadas. Los named args mejoran la legibilidad y permiten saltar parámetros con default, especificando solo los necesarios.

Higher-order functions
// Recibe función:
fun operacion(a: Int, b: Int, op: (Int, Int) -> Int): Int {
    return op(a, b)
}
operacion(3, 4) { x, y -> x + y } // 7

// Retorna función:
fun multiplicador(factor: Int): (Int) -> Int =
    { n -> n * factor }

val triple = multiplicador(3)
triple(5) // 15

Las higher-order functions reciben o retornan otras funciones — la base de la programación funcional. El tipo (Int, Int) -> Int describe la firma. Permiten crear factories, estrategias y composición de comportamiento.

Varargs y spread
// vararg: N argumentos del mismo tipo
fun suma(vararg nums: Int) = nums.sum()
suma(1, 2, 3, 4, 5) // 15

// Spread operator (*):
val arr = intArrayOf(1, 2, 3)
suma(*arr) // 6

// vararg con otros parámetros:
fun log(nivel: String, vararg msgs: String) {
    msgs.forEach { println("[$nivel] $it") }
}
log("INFO", "arrancó", "listo")

vararg acepta N argumentos del mismo tipo (internamente es Array). El spread operator (*) pasa un array como vararg. Puede combinarse con parámetros normales usando named args.

Lambdas
val suma = { a: Int, b: Int -> a + b }
val doble: (Int) -> Int = { it * 2 }
val nada: () -> Unit = { println("hola") }

// it: nombre implícito (1 parámetro)
listOf(1, 2, 3).forEach { println(it) }

// Múltiples parámetros:
val mult = { x: Int, y: Int -> x * y }
mult(3, 4) // 12

Las lambdas son funciones anónimas de primera clase. it es el nombre implícito cuando hay un solo parámetro. El tipo (Int) -> Int describe la firma. Son la base de las operaciones de colecciones y DSLs.

Scope functions
// let: transforma + null-safety
val len = nombre?.let { it.trim().length }

// apply: configura (retorna el propio objeto)
val p = Persona().apply { nombre = "Ana"; edad = 25 }

// run: bloque con resultado
val info = p.run { "$nombre tiene $edad" }

// also: efecto secundario
lista.also { println("Tam: ${it.size}") }

// with: operaciones en el objeto
with(p) { println(nombre); println(edad) }

Las scope functions eliminan variables temporales. let para transformaciones null-safe, apply para configuración/builders, run para computación, also para efectos secundarios y with para múltiples operaciones en el mismo objeto.

Funciones infix y suspend
// infix: llamada sin punto ni paréntesis
infix fun Int.mas(x: Int) = this + x
val r = 5 mas 3   // 8

// "to" es infix (crea Pair):
val par = "a" to 1  // Pair("a", 1)

// suspend: función de coroutine
suspend fun fetchDatos(): String {
    delay(1000)
    return "datos"
}

Las funciones infix (1 parámetro, miembro o extensión) pueden llamarse sin punto ni paréntesis — to es el ejemplo más común. suspend marca funciones que pueden suspenderse en coroutines (solo llamables desde otra suspend o coroutine).

Trailing lambda
// Lambda como último argumento:
listOf(1, 2, 3).forEach({ println(it) })

// Trailing lambda (fuera de los paréntesis):
listOf(1, 2, 3).forEach { println(it) }

// Si es el único argumento:
listOf(1, 2, 3).map { it * 2 }

// Varios lambdas:
fun build(init: Builder.() -> Unit, validate: () -> Boolean) {}
build({ /* config */ }) { /* valida */ }

Cuando el lambda es el último argumento, puede ir fuera de los paréntesis (trailing lambda) — esencial para APIs fluidas y DSLs. Si es el único argumento, los paréntesis pueden omitirse por completo.

Inline functions
// inline copia el lambda al call site:
inline fun medirTiempo(bloque: () -> Unit) {
    val inicio = System.currentTimeMillis()
    bloque()
    println("${System.currentTimeMillis() - inicio}ms")
}

// noinline: no hacer inline de este lambda
inline fun foo(a: () -> Unit, noinline b: () -> Unit) {}

// crossinline: prohíbe non-local return
inline fun bar(crossinline b: () -> Unit) {}

inline copia el cuerpo del lambda al call site, eliminando la asignación de objetos — crucial en bucles. noinline excluye un lambda específico; crossinline prohíbe non-local returns cuando el lambda se llama en otro contexto.

Classes e Objetos


11 cards
Clases y constructores
class Persona(val nombre: String, var edad: Int) {
    var email: String = ""

    init {
        require(edad >= 0) { "Edad inválida" }
    }

    // Constructor secundario:
    constructor(nombre: String) : this(nombre, 0)
}

val p = Persona("Ana", 25)

El primary constructor en la declaración de la clase es conciso — val/var crean propiedades automáticamente. El bloque init valida en la creación. Los constructores secundarios delegan en el primario con this().

Interfaces
interface Nadador {
    fun nadar(): String

    // Método con implementación default:
    fun flotar(): String = "flotando"
}

interface Volador {
    fun volar(): String
}

class Pato : Nadador, Volador {
    override fun nadar() = "nadando"
    override fun volar() = "volando"
}

Las interfaces pueden tener métodos abstractos y métodos con implementación default. Una clase puede implementar múltiples interfaces (pero heredar de una sola clase). Si hay conflicto de defaults, usa super<Interface>.

Companion object
class User private constructor(val nombre: String) {
    companion object Factory {
        fun create(json: String): User = User(json)
        fun guest() = User("Invitado")
        const val MAX = 100
    }
}

// Acceso vía nombre de la clase:
val u = User.create("{...}")
val g = User.guest()
println(User.MAX)

companion object sustituye los miembros static de Java — se accede vía ClassName.miembro(). Ideal para factories, constantes (const val) y métodos utilitarios. Puede tener nombre o ser anónimo.

Propiedades
class Rectangulo(val ancho: Int, val alto: Int) {
    // Propiedad calculada:
    val area: Int
        get() = ancho * alto

    // Con setter personalizado:
    var nombre: String = ""
        set(value) {
            field = value.trim()
        }
}

val r = Rectangulo(3, 4)
println(r.area) // 12 (calculado)

Las propiedades pueden tener get() personalizado (calculadas) y set() con lógica. El field es la backing field (el valor real). No hay métodos getter/setter boilerplate como en Java — todo es propiedad.

Sealed classes
sealed class Resultado<out T> {
    data class Exito<T>(val datos: T) : Resultado<T>()
    data class Error(val msg: String) : Resultado<Nothing>()
    object Cargando : Resultado<Nothing>()
}

fun tratar(r: Resultado<String>) = when (r) {
    is Resultado.Exito -> "Datos: ${r.datos}"
    is Resultado.Error -> "Error: ${r.msg}"
    Resultado.Cargando -> "..."
} // exhaustivo — sin else

sealed class restringe la jerarquía a un conjunto fijo de subclases. El compilador verifica la exhaustividad en el when — si añades un caso nuevo, todos los when rompen en compilación. Ideal para estados y resultados.

Delegation (by)
interface Printer { fun print(msg: String) }

class ConsolePrinter : Printer {
    override fun print(msg: String) = println(msg)
}

// Delega toda la interfaz:
class Logger(printer: Printer) : Printer by printer

val log = Logger(ConsolePrinter())
log.print("hola") // imprime en la consola

La delegation (by) implementa el patrón de delegación sin boilerplate — la clase reenvía todas las llamadas de la interfaz al objeto delegado. Puedes sobrescribir métodos específicos si es necesario.

Data classes
data class User(val nombre: String, val email: String, val edad: Int)

val u1 = User("Ana", "ana@mail.com", 25)
val u2 = u1.copy(nombre = "Maria") // objeto nuevo

println(u1)        // User(nombre=Ana, ...)
println(u1 == u2)  // false (compara valores)

// Destructuring:
val (nombre, email) = u1

data class genera automáticamente equals(), hashCode(), toString(), copy() y componentN(). Ideales para modelos, DTOs y estados. El copy() "modifica" inmutables creando una copia con campos cambiados.

Enum classes
enum class Estado(val color: String) {
    ACTIVO("#00FF00"),
    INACTIVO("#FF0000"),
    PENDIENTE("#FFAA00");

    fun descripcion() = "Estado: $name ($color)"
}

val e = Estado.ACTIVO
println(e.color)      // #00FF00
println(e.ordinal)    // 0
val todos = Estado.values()

Los enums en Kotlin son clases completas — pueden tener propiedades, métodos e implementar interfaces. Cada constante es una instancia accedida por Estado.ACTIVO. Útiles para estados y conjuntos fijos; values() lista todos.

Nested e inner classes
class Exterior {
    val x = 10

    // Nested (sin referencia al exterior):
    class Nested {
        fun hola() = "nested"
    }

    // Inner (accede al exterior):
    inner class Inner {
        fun hola() = "x = $x"
    }
}

val n = Exterior.Nested()
val i = Exterior().Inner()

Las clases anidadas (nested) no tienen referencia a la instancia exterior (como static en Java). Las clases inner mantienen la referencia y acceden a los miembros del exterior. Usa inner solo cuando realmente necesites el contexto externo.

Herencia (open)
open class Animal(val nombre: String) {
    open fun sonido(): String = "..."
}

class Pato(nombre: String) : Animal(nombre) {
    override fun sonido() = "¡Quack!"
}

// super:
class Perro(nombre: String) : Animal(nombre) {
    override fun sonido() = "¡Guau! " + super.sonido()
}

Las clases en Kotlin son final por defecto — usa open para permitir herencia. Los métodos también necesitan open para sobrescribirse con override. El super llama a la implementación de la superclase.

Object (singleton)
// Singleton thread-safe:
object AppConfig {
    val apiUrl = "https://api.ejemplo.com"
    fun getVersion() = "1.0.0"
}

// Uso:
println(AppConfig.apiUrl)

// Object expression (anónimo):
val listener = object : OnClickListener {
    override fun onClick() = println("clicado")
}

object crea singletons thread-safe con lazy initialization (una única instancia). Sustituye clases utilitarias con métodos estáticos. Las object expressions crean instancias anónimas de interfaces (como clases anónimas en Java).

Coleções


11 cards
Listas
val inmutable = listOf(1, 2, 3, 4, 5)
val mutable = mutableListOf(1, 2, 3)

mutable.add(4)
mutable.removeAt(0)
mutable[0] = 10

println(inmutable.first())  // 1
println(inmutable.last())   // 5
println(inmutable.size)     // 5

listOf() crea listas inmutables (read-only) — la API no expone métodos de modificación. mutableListOf() permite add/remove/set. Prefiere inmutables por defecto para evitar efectos secundarios.

Ordenación
val nums = listOf(5, 2, 8, 1, 9)

val asc = nums.sorted()            // [1,2,5,8,9]
val desc = nums.sortedDescending() // [9,8,5,2,1]

// Por propiedad:
val porEdad = personas.sortedBy { it.edad }
val porNombre = personas.sortedByDescending { it.nombre }

// Comparador personalizado:
val custom = nums.sortedWith(compareBy({ it % 2 }, { it }))

sorted() devuelve una lista nueva ordenada (no modifica la original). sortedBy ordena por una propiedad extraída. sortedWith acepta un Comparator personalizado. compareBy compone múltiples criterios.

flatMap y flatten
val listas = listOf(listOf(1, 2), listOf(3, 4), listOf(5))

val plana = listas.flatten()     // [1,2,3,4,5]
val flat = listas.flatMap { it } // [1,2,3,4,5]

// Transformar y aplanar:
val palabras = frases.flatMap { it.split(" ") }

// Palabras únicas de todas las frases:
val unicas = frases
    .flatMap { it.split(" ") }
    .distinct()

flatMap transforma cada elemento en una colección y lo aplana todo en una única lista. flatten() aplana listas anidadas directamente. Es ideal para extraer y combinar elementos de estructuras anidadas.

Map y Set
val mapa = mapOf("a" to 1, "b" to 2, "c" to 3)
println(mapa["a"])              // 1
println(mapa.getOrDefault("z", 0)) // 0

val mutMap = mutableMapOf<String, Int>()
mutMap["d"] = 4

val set = setOf(1, 2, 2, 3, 3) // {1, 2, 3}
println(3 in set) // true

Map almacena pares clave-valor con acceso O(1) por clave. Set garantiza elementos únicos — los duplicados se ignoran. Ambos tienen versiones mutables. El to crea el Pair usado en la construcción de mapas.

Búsqueda
val nums = listOf(5, 2, 8, 1, 9)

val hallado = nums.find { it > 7 }   // 8 (primero)
val último = nums.findLast { it < 5 } // 1
val índice = nums.indexOf(8)         // 2

val existe = nums.any { it > 8 }     // true
val todos = nums.all { it > 0 }      // true
val ninguno = nums.none { it < 0 }   // true

val cuenta = nums.count { it % 2 == 0 } // 2

find devuelve el primer elemento que satisface el predicado (o null). any/all/none verifican condiciones booleanas. count cuenta los que pasan. indexOf da la posición de un valor.

distinct, chunked y zip
val unicos = listOf(1, 1, 2, 3, 3).distinct() // [1,2,3]

val grupos = (1..7).chunked(3)
// [[1,2,3], [4,5,6], [7]]

val a = listOf("a", "b", "c")
val b = listOf(1, 2, 3)
val pares = a.zip(b) // [(a,1), (b,2), (c,3)]

val (x, y) = listOf(1, 2, 3).partition { it % 2 == 0 }
// x=[2], y=[1,3]

distinct() elimina duplicados. chunked(n) divide en sublistas de tamaño fijo (útil para paginación). zip combina dos listas en pares. partition divide en dos listas (pasan/no pasan el predicado).

map y filter
val nums = listOf(1, 2, 3, 4, 5, 6)

val dobles = nums.map { it * 2 }
// [2, 4, 6, 8, 10, 12]

val pares = nums.filter { it % 2 == 0 }
// [2, 4, 6]

val nombres = personas.map { it.nombre }

val mayores = personas.filter { it.edad >= 18 }

map transforma cada elemento manteniendo el tamaño; filter selecciona los que pasan el predicado. Ambas devuelven listas nuevas (no modifican la original). Son las operaciones más usadas en colecciones.

Agrupación
val personas = listOf(
    Persona("Ana", 25), Persona("Ray", 30), Persona("Mia", 25)
)

val porEdad = personas.groupBy { it.edad }
// {25=[Ana,Mia], 30=[Ray]}

val nombres = personas.associateBy { it.nombre }
// {"Ana"=Persona(...), ...}

val pares = personas.associate { it.nombre to it.edad }
// {"Ana"=25, "Ray"=30, "Mia"=25}

groupBy agrupa elementos por clave en un Map<K, List<V>>. associateBy crea un Map indexado por clave. associate construye pares clave-valor arbitrarios. Esenciales para transformar listas en mapas.

joinToString y operaciones de texto
val nums = listOf(1, 2, 3, 4, 5)

val csv = nums.joinToString(", ")
// "1, 2, 3, 4, 5"

val custom = nums.joinToString(
    separator = " | ",
    prefix = "[",
    postfix = "]"
) { it.toString() }
// "[1 | 2 | 3 | 4 | 5]"

val palabras = "a b c".split(" ")  // [a, b, c]
val junto = palabras.joinToString("") // "abc"

joinToString() convierte colecciones en string con separador, prefijo y postfix personalizables, y un transformador por elemento. split() hace lo inverso. Son ideales para construir CSVs, logs y output formateado.

reduce, fold y sum
val nums = listOf(1, 2, 3, 4, 5)

val suma = nums.reduce { acc, n -> acc + n } // 15
val total = nums.sum()                        // 15

// fold: con valor inicial (tipos diferentes):
val texto = nums.fold("") { acc, n -> "$acc$n " }

val producto = nums.fold(1) { acc, n -> acc * n } // 120

reduce acumula todos los elementos en un único valor (el primero es el inicial). fold acepta un valor inicial explícito y permite tipos diferentes entre acumulador y elementos. sum() es un atajo para sumar números.

Sequences (lazy)
// Eager: crea listas intermedias
val r1 = lista.filter { it > 5 }.map { it * 2 }.take(3)

// Lazy: procesa elemento a elemento
val r2 = lista.asSequence()
    .filter { it > 5 }
    .map { it * 2 }
    .take(3)
    .toList() // solo aquí procesa

// Infinita:
val naturales = generateSequence(1) { it + 1 }
val primeros = naturales.take(10).toList()

Las Sequences procesan elemento a elemento (lazy) en vez de crear listas intermedias en cada operación. Esencial para colecciones grandes o infinitas — solo computa lo necesario cuando se llama la operación terminal (toList()).

Avançado


11 cards
Coroutines básico
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000) // suspende sin bloquear
        println("Mundo")
    }
    println("Hola")
}
// Hola (inmediato) -> Mundo (tras 1s)

Las coroutines son funciones suspendibles que permiten concurrencia sin threads bloqueados. launch inicia una coroutine fire-and-forget. delay suspende sin bloquear el thread — ideal para I/O, timers y red.

Excepciones en coroutines
val handler = CoroutineExceptionHandler { _, e ->
    println("Error: ${e.message}")
}

CoroutineScope(Dispatchers.Default).launch(handler) {
    throw RuntimeException("falló")
}

// try/catch con async:
val r = async { operacion() }
try {
    r.await()
} catch (e: Exception) {
    println("error: $e")
}

Los errores en launch se propagan al handler de excepción (CoroutineExceptionHandler). En async, la excepción se relanza en el await() — usa try/catch. Un CoroutineScope cancela los hijos en caso de fallo (structured concurrency).

Reflection
import kotlin.reflect.full.*

class Persona(val nombre: String, val edad: Int)

val p = Persona("Ana", 25)
val klass = p::class

println(klass.simpleName)        // "Persona"
klass.memberProperties.forEach {
    println("${it.name} = ${it.get(p)}")
}

// Function reference:
val fn = ::println
fn("hola")

Reflection (con kotlin-reflect) inspecciona clases, propiedades y funciones en runtime. ::class obtiene el KClass, memberProperties lista las propiedades. Las function references (::println) pasan funciones como valores.

async / await
fun main() = runBlocking {
    val r1 = async { fetchDatos1() }
    val r2 = async { fetchDatos2() }

    // Espera ambos en paralelo:
    val total = r1.await() + r2.await()
    println("Total: $total")
}

suspend fun fetchDatos1(): Int {
    delay(1000); return 42
}

async inicia una coroutine que devuelve un Deferred (future). await() suspende hasta que el resultado esté disponible. Múltiples async se ejecutan en paralelo — ideal para llamadas de red concurrentes que reducen el tiempo total.

DSL building
class HtmlBuilder {
    val elements = mutableListOf<String>()
    fun div(c: String) { elements.add("<div>$c</div>") }
    fun p(c: String) { elements.add("<p>$c</p>") }
}

fun html(block: HtmlBuilder.() -> Unit): String {
    val b = HtmlBuilder().apply(block)
    return b.elements.joinToString("\n")
}

val page = html {
    div("Título")
    p("Contenido")
}

Las DSLs en Kotlin usan extension lambdas (Receiver.() -> Unit) para crear APIs declarativas. El objeto receptor se convierte en el this implícito dentro del bloque. Se usa en Gradle, Ktor, Exposed y Jetpack Compose.

Structured concurrency
fun main() = runBlocking {
    // coroutineScope: espera a todos los hijos
    coroutineScope {
        launch { tarea1() }
        launch { tarea2() }
    } // solo continúa cuando ambas terminan

    // supervisorScope: un fallo no cancela hermanos
    supervisorScope {
        val a = launch { throw Exception() }
        val b = launch { tarea2() } // continúa
    }
}

Structured concurrency garantiza que las coroutines tienen ciclo de vida definido. coroutineScope espera a todos los hijos y propaga fallos. supervisorScope aísla fallos — que un hijo falle no cancela a los hermanos. Esencial para código robusto.

Dispatchers
import kotlinx.coroutines.*

fun main() = runBlocking {
    // CPU-bound:
    val r = withContext(Dispatchers.Default) {
        calculoPesado()
    }

    // I/O (red, disco):
    val datos = withContext(Dispatchers.IO) {
        leerArchivo()
    }

    // UI (Android):
    withContext(Dispatchers.Main) { actualizarUi() }
}

Los dispatchers definen en qué thread pool corre la coroutine. Dispatchers.Default para CPU-bound, Dispatchers.IO para I/O (red/disco), Dispatchers.Main para la UI (Android). withContext cambia de dispatcher sin crear nueva coroutine.

Delegates (lazy/observable)
// lazy: calcula 1 vez en el primer acceso
val config: Config by lazy {
    Config.cargarDeArchivo()
}

// observable: notifica cambios
var nombre: String by Delegates.observable("") { _, old, new ->
    println("$old -> $new")
}

// vetoable: puede rechazar
var edad: Int by Delegates.vetoable(0) { _, _, new ->
    new >= 0
}

by lazy pospone una computación cara hasta el primer acceso (singleton thread-safe). observable notifica tras cada cambio. vetoable permite rechazar valores inválidos antes de aplicar. Todos eliminan boilerplate de getter/setter.

Channel (comunicación)
import kotlinx.coroutines.channels.Channel

fun main() = runBlocking {
    val channel = Channel<Int>()

    launch {
        for (i in 1..5) channel.send(i * i)
        channel.close()
    }

    for (valor in channel) {
        println(valor) // 1, 4, 9, 16, 25
    }
}

Channel permite comunicación entre coroutines (productor/consumidor). send() envía (suspende si está lleno), receive() recibe (suspende si está vacío). close() termina el canal. Iterar sobre el channel recibe hasta que se cierre.

Flow (reactivo)
import kotlinx.coroutines.flow.*

fun numeros(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(100)
        emit(i)
    }
}

runBlocking {
    numeros()
        .filter { it % 2 == 0 }
        .map { it * 10 }
        .collect { println(it) } // 20, 40
}

Flow es la API reactiva de Coroutines — emite múltiples valores asíncronamente. Es cold (solo ejecuta cuando se llama collect). Soporta operadores como filter, map, take — similar a RxJava pero integrado.

Annotations
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Log(val nivel: String = "INFO")

@Log(nivel = "DEBUG")
fun operacion() { }

// Annotations de la stdlib:
@Deprecated("usa nuevo()", ReplaceWith("nuevo()"))
fun antiguo() {}

@JvmStatic
fun utilitario() {}

Las annotations añaden metadatos al código. @Target define dónde se aplica, @Retention si está disponible en runtime. La stdlib tiene @Deprecated (con sugerencia de sustitución) y @JvmStatic/@JvmOverloads para interop con Java.

OO e Genéricos


10 cards
Visibility modifiers
class Ejemplo {
    public val a = 1     // público (default)
    private val b = 2    // solo en esta clase
    protected val c = 3  // clase + subclases
    internal val d = 4   // solo en este módulo
}

// Top-level:
private fun helper() {}  // solo en este fichero
internal val config = 1  // solo en este módulo

Kotlin tiene cuatro modificadores: public (default), private (clase/fichero), protected (clase + subclases) e internal (visible en el módulo). Todo es público por defecto — restringe con private lo máximo posible.

reified e inline
// reified mantiene el tipo en runtime:
inline fun <reified T> tipoDe(x: Any): Boolean =
    x is T

tipoDe<String>("hola") // true
tipoDe<Int>("hola")    // false

// Uso práctico:
inline fun <reified T> Gson.fromJson(json: String): T =
    fromJson(json, T::class.java)

Normalmente los tipos genéricos se borran en runtime (type erasure). reified (solo con inline) preserva el tipo, permitiendo verificaciones is T y obtener T::class. Muy usado en serialización y reflection.

Value classes (inline)
@JvmInline
value class Email(val valor: String) {
    init { require("@" in valor) { "email inválido" } }
    fun dominio() = valor.substringAfter("@")
}

val e = Email("ana@mail.com")
println(e.dominio()) // mail.com

// En runtime es solo String (sin overhead de objeto)

value class (inline class) envuelve un único valor sin overhead de objeto en runtime — type safety de un tipo distinto con rendimiento de un primitivo. Ideal para IDs, emails y newtypes con validación en el init.

Abstract classes
abstract class Forma {
    abstract fun area(): Double

    // Método concreto:
    fun describir() = "Área: ${area()}"
}

class Círculo(val radio: Double) : Forma() {
    override fun area() = Math.PI * radio * radio
}

// No se puede instanciar:
// val f = Forma() // ERROR

Las clases abstract no pueden instanciarse y pueden tener métodos abstractos (sin cuerpo) que las subclases implementan. Combinan herencia con contrato — útiles cuando hay código compartido pero comportamiento específico por subtipo.

Type checks y casts
fun procesar(obj: Any) {
    if (obj is String) {
        println(obj.uppercase()) // smart cast
    }

    // Cast explícito:
    val s = obj as? String  // seguro (null si falla)
    val n = obj as Int      // lanza ClassCastException

    // Star projection:
    fun tamano(lista: List<*>) = lista.size
}

is verifica el tipo con smart cast automático. as? es el cast seguro (devuelve null si falla); as lanza excepción. List<*> (star projection) acepta listas de cualquier tipo cuando no se necesita el tipo concreto.

Builder pattern
class PedidoBuilder {
    var item: String = ""
    var cantidad: Int = 1
    var urgente: Boolean = false
    fun build() = Pedido(item, cantidad, urgente)
}

fun pedido(block: PedidoBuilder.() -> Unit): Pedido =
    PedidoBuilder().apply(block).build()

val p = pedido {
    item = "Libro"
    cantidad = 2
    urgente = true
}

El builder pattern en Kotlin usa extension lambda (Builder.() -> Unit) con apply — crea una DSL declarativa para configurar objetos. Más legible que constructores con muchos parámetros opcionales.

Generics
// Clase genérica:
class Box<T>(val value: T)
val intBox = Box(42)       // Box<Int> inferido
val strBox = Box("texto")

// Función genérica:
fun <T> primero(lista: List<T>): T? =
    lista.firstOrNull()

// Constraint:
fun <T : Comparable<T>> mayor(a: T, b: T): T =
    if (a > b) a else b

Los generics permiten código reutilizable con type safety. El compilador infiere tipos en la mayoría de los casos. Los constraints (T : Comparable<T>) restringen los tipos aceptados a quienes implementan una interfaz.

equals y hashCode
class Punto(val x: Int, val y: Int) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Punto) return false
        return x == other.x && y == other.y
    }

    override fun hashCode(): Int = 31 * x + y
}

// ¡data class genera esto automáticamente!
data class Punto2(val x: Int, val y: Int)

Sobrescribe equals() y hashCode() juntos para comparación por valor (necesario en Set/Map). === compara referencias. Prefiere data class, que genera ambos automáticamente.

Varianza (in/out)
// out (covarianza): solo produce T
class Productor<out T>(val item: T) {
    fun obtener(): T = item
}
val p: Productor<Any> = Productor("texto") // OK

// in (contravarianza): solo consume T
class Consumidor<in T> {
    fun consumir(item: T) { println(item) }
}
val c: Consumidor<String> = Consumidor<Any>() // OK

out (covarianza) significa que la clase solo produce TProductor<String> es un Productor<Any>. in (contravarianza) solo consume T. Permite subtipado seguro en genéricos (PECS: Producer Extends, Consumer Super).

Sealed interfaces
sealed interface Evento {
    data class Clic(val x: Int, val y: Int) : Evento
    data class Tecla(val código: Int) : Evento
    object Cierre : Evento
}

fun tratar(e: Evento) = when (e) {
    is Evento.Clic -> "clic en ${e.x},${e.y}"
    is Evento.Tecla -> "tecla ${e.código}"
    Evento.Cierre -> "cierre"
} // exhaustivo

sealed interface (Kotlin 1.5+) combina interfaces con jerarquía cerrada — las implementaciones se conocen en compilación, permitiendo when exhaustivo. Más flexible que sealed class porque una clase puede implementar varias.

Erros e Null Safety


10 cards
Try / Catch
// try es expresión:
val n = try {
    "42".toInt()
} catch (e: NumberFormatException) {
    0
} finally {
    println("intento hecho")
}

// Múltiples catch:
try {
    operacion()
} catch (e: IllegalArgumentException) {
    println("argumento: ${e.message}")
} catch (e: Exception) {
    println("otro: $e")
}

try es una expresión que devuelve valor — el último statement del bloque try o catch es el resultado. El finally se ejecuta siempre. Puede haber múltiples catch por tipo de excepción.

Safe calls encadenados
// Encadenamiento seguro:
val ciudad = persona?.direccion?.ciudad?.nombre

// Equivalente sin safe call (verboso):
// if (persona != null && persona.direccion != null ...)

// Con let (solo ejecuta si no-null):
persona?.direccion?.let { dir ->
    println(dir.ciudad)
    enviarCorreo(dir)
}

// Combina con elvis:
val nombre = persona?.nombre ?: "Desconocido"

Los safe calls (?.) se encadenan — si cualquier elemento es null, el resultado es null sin NullPointerException. Combinados con let ejecutan bloques solo cuando toda la cadena existe. Sustituye null-checks anidados.

Operador !! (not-null)
var nombre: String? = "Kotlin"

// Fuerza non-null (peligroso):
val len = nombre!!.length

// Si es null -> NullPointerException
// nombre = null
// nombre!!.length // ¡CRASH!

// Prefiere siempre alternativas seguras:
val a = nombre?.length ?: 0       // elvis
val b = nombre?.let { it.length } // let

El operador !! convierte un tipo nullable en non-null, lanzando NullPointerException si es null. Es un "sé lo que hago" — evítalo. Prefiere ?., ?: o let para código verdaderamente null-safe.

Throw y excepciones
fun dividir(a: Int, b: Int): Int {
    if (b == 0) {
        throw IllegalArgumentException("división por cero")
    }
    return a / b
}

// throw es expresión (tipo Nothing):
fun validar(x: Int): Int =
    if (x < 0) throw IllegalArgumentException("negativo")
    else x

throw lanza excepciones (todas son Throwable). En Kotlin no hay checked exceptions — no es obligatorio declarar ni capturar. throw es una expresión de tipo Nothing, útil en expresiones if/elvis.

require y check
// require: valida argumentos (IllegalArgumentException)
fun crearUser(nombre: String, edad: Int) {
    require(nombre.isNotBlank()) { "nombre vacío" }
    require(edad in 0..150) { "edad inválida: $edad" }
}

// check: valida estado (IllegalStateException)
fun iniciar() {
    check(!iniciado) { "ya iniciado" }
    iniciado = true
}

// assert (solo en debug):
assert(x > 0) { "x debe ser positivo" }

require() valida argumentos de funciones (lanza IllegalArgumentException). check() valida el estado del objeto (lanza IllegalStateException). Ambos aceptan un mensaje lazy. Hacen las precondiciones explícitas y documentadas.

Platform types
// El código Java puede devolver null:
// String s = javaLib.getText(); // puede ser null

// Kotlin lo trata como platform type (T!):
val s = javaLib.getText() // String!
// Compila, pero puede dar NPE en runtime

// Hazlo explícito:
val seguro: String? = javaLib.getText()
val len = seguro?.length ?: 0

// @Nullable/@NotNull en Java ayudan

Los platform types (T!) vienen de código Java sin anotaciones de nullability — Kotlin no sabe si pueden ser null. La responsabilidad es del programador. Anota como String? explícitamente y usa safe calls al interactuar con Java.

Custom exceptions
class SaldoInsuficienteException(
    val saldo: Double,
    val valor: Double
) : Exception("Saldo $saldo insuficiente para $valor")

fun retirar(saldo: Double, valor: Double) {
    if (valor > saldo) {
        throw SaldoInsuficienteException(saldo, valor)
    }
}

try {
    retirar(100.0, 150.0)
} catch (e: SaldoInsuficienteException) {
    println(e.saldo)
}

Crea excepciones personalizadas extendiendo Exception (recuperable) o RuntimeException. Añade propiedades relevantes (como saldo) para contexto. Mantén la jerarquía de excepciones simple y específica.

Result type
val resultado: Result<Int> = runCatching {
    "42".toInt()
}

resultado.onSuccess { println("Ok: $it") }
resultado.onFailure { println("Error: $it") }

// Obtener valor:
val n = resultado.getOrDefault(0)
val n2 = resultado.getOrNull()
val n3 = resultado.getOrThrow()

// Encadenar:
val doble = resultado.map { it * 2 }

Result<T> encapsula éxito o fallo sin lanzar excepción. runCatching{} captura excepciones en un Result. onSuccess/onFailure tratan cada caso; map transforma el éxito. Alternativa funcional a try/catch.

Operador elvis
// Default si null:
val len = nombre?.length ?: 0

// Con throw:
fun obtenerNombre(): String {
    return input?.trim()
        ?: throw IllegalStateException("sin nombre")
}

// Encadenado:
val valor = a ?: b ?: c ?: "default"

// Con return:
fun procesar(x: Int?) {
    val n = x ?: return // early return si null
    println(n * 2)
}

El operador elvis (?:) proporciona un valor alternativo cuando el lado izquierdo es null. Se combina con throw (lanza si null), return (early exit) y encadenamiento. Es la herramienta central de la null-safety idiomática.

runCatching
// En vez de try/catch:
val valor = runCatching {
    api.fetchDatos()
}.getOrElse { error ->
    println("Fallo: $error")
    datosLocales()
}

// Transformar resultado:
val procesado = runCatching { parse(input) }
    .map { it.uppercase() }
    .recover { "default" }
    .getOrThrow()

runCatching{} ejecuta un bloque y captura cualquier excepción en un Result. getOrElse proporciona alternativa en caso de fallo; recover transforma el error en valor. Permite pipelines funcionales sin try/catch verboso.

Ferramentas e Idioms


10 cards
Idioms esenciales
// Singleton:
object Recurso { }

// Inmutable por defecto:
val lista = listOf(1, 2, 3)

// Expresiones en todo:
val max = if (a > b) a else b

// Data class para modelos:
data class User(val nombre: String)

// Scope functions:
val s = StringBuilder().apply { append("hola") }

Idioms Kotlin: prefiere val (inmutable), usa data class para modelos, expresiones en vez de statements, object para singletons y scope functions para eliminar variables temporales. Código conciso y expresivo.

Tests
import kotlin.test.*

class CalculadoraTest {
    @Test
    fun `suma dos números`() {
        val r = suma(2, 3)
        assertEquals(5, r)
    }

    @Test
    fun `lanza en división por cero`() {
        assertFailsWith<IllegalArgumentException> {
            dividir(1, 0)
        }
    }

    @BeforeTest fun setup() { }
    @AfterTest fun cleanup() { }
}

Kotlin Test (kotlin.test) proporciona aserciones multiplataforma. Los tests pueden tener nombres con espacios entre backticks. assertEquals verifica igualdad, assertFailsWith verifica excepciones. @BeforeTest/@AfterTest hacen setup/cleanup.

Rendimiento y boxing
// Primitivos optimizados:
val arr = IntArray(1000) { it }  // sin boxing
val lista = List(1000) { it }    // Int? boxed

// inline evita asignación de lambdas:
inline fun repetir(n: Int, bloque: () -> Unit) {
    for (i in 0 until n) bloque()
}

// Sequences para colecciones grandes:
val r = datos.asSequence()
    .filter { it > 0 }
    .map { it * 2 }
    .take(10)
    .toList()

Usa IntArray/DoubleArray para evitar boxing. inline elimina la asignación de objetos lambda en bucles. Las Sequences evitan listas intermedias en cadenas de operaciones. Prefiere val e inmutabilidad para optimizaciones del compilador.

Stdlib útil
// Strings:
"hola".uppercase()       // "HOLA"
"a,b,c".split(",")       // [a, b, c]
"  x  ".trim()           // "x"

// Números:
val r = (1..10).random()
val n = 3.14159.roundToInt() // 3

// Colecciones:
listOf(1, 2, 3).shuffled()
listOf(1, 2, 3).reversed()
"a" to 1                 // Pair

La stdlib de Kotlin es rica: uppercase/split/trim para strings, random/roundToInt para números, shuffled/reversed para colecciones. El to crea Pair. Reduce mucho el código utilitario.

Coroutines test
import kotlinx.coroutines.test.*

@Test
fun testAsync() = runTest {
    // delay se salta automáticamente:
    val resultado = fetchDatos() // suspend
    assertEquals("datos", resultado)
}

@Test
fun testConcurrente() = runTest {
    val r1 = async { operacion1() }
    val r2 = async { operacion2() }
    assertEquals(42, r1.await() + r2.await())
}

kotlinx-coroutines-test proporciona runTest, que ejecuta coroutines de test de forma determinística — delay se salta (virtual time), haciendo los tests rápidos. async/launch funcionan normalmente dentro del test.

Buenas prácticas
// 1. val por defecto, var solo si es necesario
val total = calcular()

// 2. Inmutable: listOf en vez de mutableListOf
val items = listOf(1, 2, 3)

// 3. Expresiones en vez de statements
val r = if (x > 0) x else -x

// 4. data class para modelos
data class Dto(val id: Int, val nombre: String)

// 5. Null-safety en vez de !!
val n = valor?.length ?: 0

Buenas prácticas: val por defecto, colecciones inmutables, expresiones en vez de statements, data class para modelos y null-safety idiomática (nunca !!). Sigue las Kotlin Coding Conventions oficiales para un estilo consistente.

Interop con Java
// Kotlin llama a Java directamente:
val lista = java.util.ArrayList<String>()
val fecha = java.time.LocalDate.now()

// Java llama a Kotlin:
// (compila a bytecode JVM normal)

// Anotaciones para interop:
@JvmStatic fun util() {}       // static real
@JvmOverloads fun f(a: Int, b: Int = 0) {}
@JvmField val x = 1            // campo público

@file:JvmName("Utils")         // nombre del archivo

Kotlin es 100% interoperable con Java — llama a bibliotecas Java directamente y viceversa. Las anotaciones @JvmStatic, @JvmOverloads, @JvmField y @JvmName controlan cómo el código Kotlin aparece para Java.

Multiplatform
// commonMain (compartido):
expect fun plataforma(): String

// androidMain:
actual fun plataforma() = "Android"

// iosMain:
actual fun plataforma() = "iOS"

// jvmMain:
actual fun plataforma() = "JVM"

// Uso en el código común:
fun saludar() = "Hola desde ${plataforma()}"

Kotlin Multiplatform comparte lógica de negocio entre plataformas (Android, iOS, JVM, JS, Native). expect declara una API en el código común; actual proporciona la implementación específica de cada plataforma. Ideal para networking, modelos y validación.

Gradle Kotlin DSL
// build.gradle.kts:
plugins {
    kotlin("jvm") version "1.9.0"
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    testImplementation(kotlin("test"))
}

tasks.test {
    useJUnitPlatform()
}

kotlin {
    jvmToolchain(17)
}

El Gradle Kotlin DSL (build.gradle.kts) usa Kotlin en vez de Groovy — type-safe, autocompletado en el IDE y refactorizable. plugins{} aplica plugins, dependencies{} gestiona dependencias. Es el estándar recomendado para proyectos nuevos.

Diseño null-safe
// Devuelve no-null siempre que sea posible:
fun nombre(): String = "Ana"   // nunca null

// Usa nullable solo cuando sea necesario:
fun encontrar(id: Int): User? =
    base.firstOrNull { it.id == id }

// Fuerza el tratamiento en el call site:
val u = encontrar(1)
    ?: throw NotFoundException("user 1")

// Tipos no-null + elvis = código seguro

Diseño null-safe: devuelve tipos no-null por defecto; usa nullable (?) solo cuando la ausencia es un estado válido. Fuerza el tratamiento en el call site con ?: o let. Cuantos menos nullables en la API, más seguro el código.