Curso de Kotlin desde cero, en español

Kotlin es el lenguaje oficial para hacer apps de Android, pero no se queda ahí: también se usa en el servidor, con Ktor o Spring Boot, y para compartir código entre Android, iOS, escritorio y web con Kotlin Multiplatform. Es un lenguaje con tipos, conciso y de código abierto, que corre en la misma máquina virtual que Java y convive con todo el código Java que ya existe. Este curso empieza desde cero, aunque si ya programas, sobre todo en Java, irás más rápido. Todo el código está comprobado con Kotlin 2.4.21, la versión estable actual, sobre Java 25.

La ruta: ocho lecciones

Están pensadas para leerse en orden. Las dos primeras están en esta misma página y las demás tienen la suya. Cada lección tiene un recuadro «El error que te ahorra» con un fallo que el compilador detiene antes de que el programa llegue a ejecutarse, al menos un esquema y retos al final con la solución escondida. Si vienes de Java, busca los recuadros «Si vienes de Java»: comparan lo que ya conoces con su equivalente en Kotlin. Al terminar hay un test de 24 preguntas con un formato distinto: adivinar si el código compila.

Lección 1. Primeros pasos

Qué es Kotlin y de dónde sale

Kotlin lo creó JetBrains, la empresa de IntelliJ IDEA, que lo presentó en julio de 2011. Buscaba un lenguaje para la máquina virtual de Java (la JVM) más conciso y más seguro que Java, pero capaz de usar cualquier biblioteca de Java sin adaptadores. La versión 1.0 salió en febrero de 2016, con licencia Apache 2.0.

El gran salto llegó en la conferencia Google I/O de 2017, cuando el equipo de Android anunció que Kotlin pasaba a ser un lenguaje con soporte oficial. Dos años después, en Google I/O 2019, Google fue más lejos: el desarrollo de Android sería «Kotlin-first», con las bibliotecas nuevas pensadas primero para Kotlin, y la recomendación de escribir en Kotlin todo proyecto nuevo. En mayo de 2024 salió Kotlin 2.0, con un compilador reescrito (llamado K2) que compila hasta el doble de rápido. Hoy Kotlin está en la versión 2.4.

  • DóndePara qué se usaHerramientas típicas
  • Androidapps nativas: es el lenguaje que recomienda GoogleAndroid Studio, Jetpack Compose
  • ServidorAPIs y servicios web, igual que con JavaKtor, Spring Boot
  • Multiplataformacompartir la lógica entre Android, iOS, escritorio y webKotlin Multiplatform, Compose Multiplatform
  • Scripts y herramientasprogramas de línea de órdenes y automatización de buildskotlinc, Gradle (sus archivos .kts son Kotlin)

El archivo hola.kt pasa por el compilador kotlinc, que revisa los tipos y los null y produce HolaKt.class, bytecode de la JVM; la JVM lo ejecuta en cualquier sistema. Con -include-runtime -d hola.jar todo queda en un solo jarDel archivo .kt al programa en marchahola.kttu códigokotlincrevisa tipos y nullHolaKt.classbytecode de la JVMJVM (java)lo ejecutaLos errores de tipos y de null salenaquí, antes de ejecutar nada.El mismo bytecode que genera Java:corre en Windows, Linux y macOS.-include-runtime -d hola.jar

Kotlin se compila a bytecode de la JVM: el compilador revisa el código y la máquina virtual de Java lo ejecuta. Esquema: decodigo.com.

El esquema explica dos cosas que verás en todo el curso. La primera: los errores de tipos, y también los de null, los encuentra el compilador, antes de ejecutar nada. La segunda: el resultado es bytecode de la JVM, el mismo formato que genera Java. Por eso un programa de Kotlin puede usar cualquier biblioteca de Java, y al revés.

Instalar Kotlin

Necesitas un JDK (Java 17 o posterior está bien; aquí usamos Java 25) y el compilador de Kotlin. Hay tres caminos:

  • IntelliJ IDEA (la edición gratuita sirve) o Android Studio: traen Kotlin integrado. Creas un proyecto Kotlin y das clic en el triángulo verde junto a fun main. Es lo más cómodo para trabajar a diario.
  • El compilador de línea de órdenes: se descarga el zip kotlin-compiler-2.4.21.zip de la página de versiones de github.com/JetBrains/kotlin, se descomprime y se añade su carpeta bin al PATH. En macOS y Linux también está en Homebrew y SDKMAN.
  • Gradle, la herramienta de build que usan los proyectos reales: descarga el compilador solo. Lo vemos en la lección 8.

Para seguir este curso basta con el compilador. Comprueba que está bien instalado:

kotlinc -version
java -version
salidainfo: kotlinc-jvm 2.4.21 (JRE 25.0.5+8-LTS)
openjdk version "25.0.5" 2026-10-20 LTS

El primer programa

Crea un archivo hola.kt:

fun main() {
    println("Hola, mundo")
}

Todo programa empieza en una función llamada main. fun declara una función y println escribe una línea. No hacen falta punto y coma, ni una clase alrededor, ni el public static void de Java. Para compilarlo y ejecutarlo:

kotlinc hola.kt -include-runtime -d hola.jar
java -jar hola.jar
salidaHola, mundo

kotlinc es el compilador. -d hola.jar dice dónde dejar el resultado, y -include-runtime mete dentro del jar la biblioteca estándar de Kotlin, para que java -jar lo pueda ejecutar en cualquier computadora con Java. Por eso el jar no es pequeño, aunque el programa tenga tres líneas:

ls -lh hola.jar5.5M hola.jar

Compilar tarda unos segundos, porque el compilador también corre en la JVM y tiene que arrancar. En IntelliJ IDEA no lo notarás tanto: el IDE mantiene el compilador en marcha.

Si vienes de Java: dónde está la clase

La JVM solo entiende clases, así que kotlinc crea una por ti: las funciones de nivel superior del archivo hola.kt van a una clase llamada HolaKt. Si compilas sin -include-runtime a una carpeta (kotlinc hola.kt -d clases) y la miras con javap, la herramienta del JDK, aparece el main de siempre:

find clases -type fclases/HolaKt.class
clases/META-INF/main.kotlin_module
javap -cp clases HolaKtCompiled from "hola.kt"
public final class HolaKt {
  public static final void main();
  public static void main(java.lang.String[]);
}

Hay dos main: el tuyo, sin parámetros, y uno con String[] que genera Kotlin para que Java lo encuentre. Desde Java, esas funciones se llaman como métodos estáticos: HolaKt.main(). Si quieres repasar cómo es en Java, está el curso de Java.

Variables, texto y la consola

fun main() {
    val nombre = "Ana"
    val edad = 31
    println("Hola, $nombre. El año que viene tendrás ${edad + 1} años.")

    print("¿Cómo te llamas? ")
    val respuesta = readln()
    println("Mucho gusto, $respuesta.")
}
salidaHola, Ana. El año que viene tendrás 32 años.
¿Cómo te llamas? Mucho gusto, Luis.

Tres cosas nuevas. val declara una variable; el compilador deduce su tipo del valor: nombre es un String y edad un Int, y lo serán siempre. El $ dentro de las comillas es una plantilla de cadena (string template): $nombre mete el valor de la variable, y con llaves, ${edad + 1}, cualquier expresión. Y readln() lee una línea de lo que escribe el usuario (en la salida no aparece «Luis» porque aquí la entrada vino de un archivo y no del teclado).

Tu primer error

Kotlin revisa los tipos al compilar. Si llamas a una función con un dato del tipo equivocado, el programa ni siquiera llega a ejecutarse. Los mensajes de kotlinc siempre salen en inglés, porque el compilador no tiene traducciones; aquí los copiamos tal cual y los explicamos.

El error que te ahorra: pasar un dato del tipo equivocado

fun saludar(nombre: String) = "Hola, $nombre"

fun main() {
    println(saludar("Ana"))
    println(saludar(42))
}
kotlincsaludar_mal.kt:5:21: error: argument type mismatch: actual type is 'Int', but 'String' was expected.
    println(saludar(42))
                    ^^

El mensaje da el archivo, la línea y la columna (5:21), dice que es un error y explica el problema: el argumento es un Int y se esperaba un String. Debajo copia la línea y marca con ^^ el sitio exacto. En JavaScript o Python el programa arrancaría y fallaría (o haría algo raro) al llegar a esa línea, quizá meses después. En Kotlin no se ejecuta nada hasta que lo arreglas.

Lección 2. Variables, tipos y funciones

val y var

Kotlin tiene dos formas de declarar una variable. val (de value) no se puede volver a asignar; var (de variable) sí. La regla de la casa es usar val siempre que puedas y var solo cuando el valor de verdad tenga que cambiar, como un contador:

fun main() {
    val nombre = "Ana"
    var visitas = 0
    visitas = visitas + 1
    visitas += 1
    println("$nombre tiene $visitas visitas")
}
salidaAna tiene 2 visitas

Si intentas cambiar un val, el compilador lo detiene:

fun main() {
    val total = 5
    total = 6
}
kotlincval_reasignar.kt:3:5: error: 'val' cannot be reassigned.
    total = 6
    ^^^^^

Ojo con lo que significa: val fija la variable, no el objeto al que apunta. Si ese objeto se puede modificar, como una lista mutable, se modifica sin problema:

fun main() {
    val compras = mutableListOf("pan", "leche")
    compras.add("café")
    println(compras)
}
salida[pan, leche, café]

A la izquierda, val total = 5 no admite total = 6, mientras que var cuenta = 5 sí admite cuenta = 6. A la derecha, val compras apunta a una lista mutable: se le pueden añadir elementos con add, pero no se puede hacer que la variable apunte a otra listaval fija la variable, no el contenidoval total = 55total = 6no compilavar cuenta = 56cuenta = 6se puedeval compras = mutableListOf(…)compraspanlechecafécompras.add(…) sí se puede: cambiala lista, no la variable.compras = otraLista no compila.

Un val no se puede reasignar, pero el objeto al que apunta sí puede cambiar por dentro. Esquema: decodigo.com.

Los tipos básicos

Toda variable tiene un tipo fijo, lo escribas o lo deduzca el compilador. Cuando quieras escribirlo, va después del nombre y de dos puntos: val edad: Int = 31. Estos son los que más usarás:

  • TipoQué guardaEjemplo
  • Intenteros de 32 bits, hasta unos 2,100 millonesval edad = 31
  • Longenteros de 64 bits; sufijo Lval poblacion = 8_100_000_000L
  • Doubledecimales en coma flotante, rápidos pero aproximadosval pi = 3.14159
  • Booleantrue o falseval activo = true
  • Charun carácter, entre comillas simplesval inicial = 'A'
  • Stringtexto, entre comillas doblesval nombre = "Ana"

En Kotlin no hay «tipos primitivos» aparte, como el int y el Integer de Java: todo es un objeto con métodos (31.toString(), 'a'.uppercaseChar()), y el compilador usa por dentro los primitivos de la JVM cuando puede, así que no pagas nada por ello. Los guiones bajos en los números (8_100_000_000) solo sirven para leerlos mejor.

Ninguna conversión a escondidas

En Java, un int se convierte solo en long cuando hace falta. En Kotlin, no: cada conversión se escribe.

El error que te ahorra: mezclar Int y Long sin darte cuenta

fun main() {
    val piezas: Int = 1200
    val total: Long = piezas
    println(total)
}
kotlincint_a_long.kt:3:21: error: initializer type mismatch: expected 'Long', actual 'Int'.
    val total: Long = piezas
                    ^

El tipo del valor inicial (Int) no coincide con el declarado (Long). La solución es decirlo: val total: Long = piezas.toLong(). Parece una molestia, pero evita una familia entera de errores silenciosos al mezclar tamaños de números.

Todos los números tienen sus funciones de conversión: toInt(), toLong(), toDouble(), toString()…

fun main() {
    val piezas = 1200
    val total: Long = piezas.toLong() * 3_000_000
    val promedio = piezas.toDouble() / 7
    println(total)
    println(promedio)
    println(3.9.toInt())
    println('A' + 2)
}
salida3600000000
171.42857142857142
3
C

toInt() sobre un decimal corta los decimales, no redondea (para redondear está roundToInt(), de kotlin.math). Y a un Char se le puede sumar un número para avanzar en el alfabeto.

Los números tienen sorpresas

fun main() {
    println(7 / 2)
    println(7 / 2.0)
    println(7 % 3)
    println(0.1 + 0.2)

    val maximo = Int.MAX_VALUE
    println(maximo + 1)
}
salida3
3.5
1
0.30000000000000004
-2147483648

Línea a línea: dividir dos enteros da un entero, sin decimales (7 / 2 es 3); basta que uno sea decimal para que el resultado lo sea. % es el resto de la división. Double no puede representar exactamente 0.1, y se nota en la cuarta línea: para dinero se usa BigDecimal de Java, o se cuentan centavos en un Long. Y un Int que se pasa de su máximo da la vuelta sin avisar y se vuelve negativo; si tus números pueden ser tan grandes, usa Long.

De texto a número

Lo que llega del teclado, de un archivo o de la red es texto. toInt() lo convierte y lanza una excepción si no es un número; toIntOrNull() devuelve null en ese caso:

fun main() {
    println("42".toInt() + 1)
    println("cuarenta".toIntOrNull())
    println("cuarenta".toInt())
}
salida con error43
null
Exception in thread "main" java.lang.NumberFormatException: For input string: "cuarenta"
    at Texto_a_numeroKt.main(texto_a_numero.kt:4)

Los errores al ejecutar se llaman excepciones; el mensaje viene de Java y también sale en inglés. Debajo, la línea at dice dónde ocurrió: en main, en la línea 4 del archivo. Con datos que escribe una persona, usa toIntOrNull(): que alguien se equivoque al teclear no es una situación excepcional.

Un adelanto: lo que puede faltar

El null de antes no es un detalle: en Kotlin un String nunca puede ser null. Si un valor puede faltar, su tipo lleva un signo de interrogación: String?, Int?. Y el compilador te obliga a contemplar ese caso:

fun main() {
    val texto = readln()
    val numero: Int? = texto.toIntOrNull()
    println(numero ?: "eso no es un número")
    println(numero?.plus(1))
}
salidaeso no es un número
null

?: (el operador Elvis) da un valor por omisión si lo de la izquierda es null, y ?. llama a la función solo si no lo es; si lo es, el resultado es null sin fallar. Es lo que en Java sería un NullPointerException esperando a ocurrir. Toda la lección 4 va de esto.

Cadenas

fun main() {
    val ciudad = "Guadalajara"
    println(ciudad.length)
    println(ciudad.uppercase())
    println(ciudad.take(4))
    println("jara" in ciudad)
    println(ciudad[0])

    val ticket = """
        Tienda La Esquina
        Total: ${"$"}149.50
    """.trimIndent()
    println(ticket)
}
salida11
GUADALAJARA
Guad
true
G
Tienda La Esquina
Total: $149.50

Los String no se modifican: uppercase() devuelve una cadena nueva. in pregunta si un texto contiene otro y los corchetes dan un carácter por su posición, empezando en 0. Las comillas triples crean una cadena cruda (raw string) de varias líneas, donde las barras invertidas no son especiales; trimIndent() le quita la sangría común. Dentro de ella, un $ literal se escribe ${"$"}, porque si no Kotlin intentaría leer una variable.

Funciones

Una función se declara con fun, sus parámetros con su tipo y, después de los dos puntos, el tipo que devuelve. Si el cuerpo es una sola expresión, se puede escribir con = y el tipo de retorno se deduce:

fun area(base: Double, altura: Double): Double {
    return base * altura / 2
}

fun doble(n: Int) = n * 2

fun saludar(nombre: String, saludo: String = "Hola", final: String = "!") =
    "$saludo, $nombre$final"

fun main() {
    println(area(10.0, 4.0))
    println(doble(21))
    println(saludar("Ana"))
    println(saludar("Luis", "Buenos días"))
    println(saludar("Eva", final = "."))
}
salida20.0
42
Hola, Ana!
Buenos días, Luis!
Hola, Eva.

saludar tiene parámetros con valor por omisión: si no los pasas, se usan esos. Y en cualquier llamada puedes nombrar los argumentos (final = "."), lo que permite saltarte los de en medio y hace la llamada más legible. En Java esto requiere escribir varias sobrecargas del mismo método.

Una función que no devuelve nada útil devuelve Unit, el equivalente de void, y no hace falta escribirlo. Por eso esta función no compila: su cuerpo es un bloque, así que su tipo es Unit, y el return intenta devolver un número.

fun triple(n: Int) {
    return n * 3
}

fun main() {
    println(triple(4))
}
kotlincreturn_unit.kt:2:12: error: return type mismatch: expected 'Unit', actual 'Int'.
    return n * 3
           ^^^^^

Hay dos arreglos: declarar el tipo, fun triple(n: Int): Int { ... }, o usar el cuerpo de expresión, fun triple(n: Int) = n * 3.

Resumen de las lecciones 1 y 2

  • ParaEscribeRecuerda
  • Compilar y ejecutarkotlinc a.kt -include-runtime -d a.jar y java -jar a.jaren el IDE, el triángulo verde
  • Una variableval n = 5 o var n = 5val por omisión; el tipo no cambia nunca
  • Escribir el tipoval n: Long = 5va después del nombre
  • Convertirn.toLong(), x.toInt()no hay conversiones automáticas
  • Texto a númerotexto.toIntOrNull()toInt() lanza si falla
  • Meter valores en un texto"Hola, $nombre: ${n + 1}"llaves para expresiones
  • Algo que puede faltarString?, ?., ?:lección 4
  • Una función cortafun doble(n: Int) = n * 2parámetros por omisión y con nombre

Retos

Reto 1. Este programa compila, pero escribe 2 en vez de 2.5. Arréglalo cambiando lo mínimo. val total = 5; val partes = 2; println(total / partes)

Ver la solución
fun main() {
    val total = 5
    val partes = 2
    println(total.toDouble() / partes)
}
salida2.5

Basta con convertir uno de los dos a Double antes de dividir. Ojo: (total / partes).toDouble() no sirve, porque convierte el resultado de una división entera que ya perdió los decimales.

Reto 2. Pide al usuario su año de nacimiento con readln() y escribe cuántos años cumple en 2026. Si no escribe un número, que el programa lo diga sin fallar.

Ver la solución
fun main() {
    print("¿En qué año naciste? ")
    val anio = readln().toIntOrNull()
    if (anio != null) {
        println("En 2026 cumples ${2026 - anio} años.")
    } else {
        println("Eso no parece un año.")
    }
}
salida¿En qué año naciste? En 2026 cumples 31 años.

Dentro del if, después de comprobar que anio no es null, el compilador ya lo trata como un Int normal y deja restarlo. Eso se llama smart cast y lo verás a fondo en la lección 4.

Reto 3. Escribe una función precioConIva que reciba un precio y una tasa de IVA con valor por omisión de 0.16 y devuelva el precio con IVA. Llámala con un precio de 100.0 y luego con el mismo precio y una tasa de 0.08, nombrando ese argumento.

Ver la solución
fun precioConIva(precio: Double, tasa: Double = 0.16) = precio * (1 + tasa)

fun main() {
    println(precioConIva(100.0))
    println(precioConIva(100.0, tasa = 0.08))
}
salida115.99999999999999
108.0

Fíjate en el primer resultado: otra vez la coma flotante. Para mostrar un precio, redondea al imprimir, por ejemplo con "%.2f".format(x), o trabaja con BigDecimal; en los ejemplos de Kotlin verás las dos cosas.

Cómo está hecho este curso

Cada fragmento de código se compiló con Kotlin 2.4.21 (kotlinc) y se ejecutó en la JVM de Temurin 25.0.5, en un contenedor limpio y sin red. Los mensajes del compilador y las salidas están copiados de esa ejecución, así que verás exactamente lo mismo en tu computadora si usas esas versiones. Los mensajes de kotlinc salen en inglés: no tiene traducciones.

¿Buscas una receta concreta en vez de una lección? En Ejemplos de Kotlin hay tareas resueltas: leer y escribir archivos, JSON, CSV, fechas, peticiones HTTP, una base de datos SQLite, un servidor web con Ktor… Y si vienes de Java, el curso de Java te servirá para comparar: Kotlin nació para convivir con él.

Kotlin® es una marca registrada de la Kotlin Foundation; decodigo.com no tiene relación con ella.