wandres.dev
EL LENGUAJE · quince años y un compilador nuevo

De dónde viene Kotlin

El problema concreto que JetBrains tenía con Java hacia 2010, los objetivos de diseño que salieron de ahí, y por qué la interoperabilidad total con la JVM fue la decisión que lo condicionó todo.

⏱ 13 min

Kotlin no nació de una tesis sobre teoría de tipos ni de la ambición de reinventar la programación. Nació de una empresa que vendía herramientas de desarrollo, que tenía millones de líneas de Java propias, y que hizo una cuenta incómoda: el lenguaje que usaba le estaba costando dinero cada día. Esa génesis industrial, y no académica, explica casi todas las decisiones que verás en los quince años siguientes.

🎯 Al terminar esta lección sabrás
  • Reconstruir el problema real de JetBrains con Java hacia 2010 y por qué las alternativas existentes no servían.
  • Enumerar los objetivos de diseño declarados y el orden de prioridad entre ellos.
  • Explicar por qué la interoperabilidad total fue la restricción fundacional y no una característica más.
  • Identificar el precio de diseño que Kotlin pagó por esa interoperabilidad.

El problema no era Java: era el coste de salir de Java

En 2010 JetBrains mantenía IntelliJ IDEA, una base de código de varios millones de líneas de Java acumuladas durante una década. El diagnóstico interno era doble. Primero, Java evolucionaba con una lentitud estructural: las lambdas no llegarían hasta Java 8, en 2014, y la nulabilidad seguía sin estar en el sistema de tipos. Segundo, y más grave, JetBrains vendía productividad, así que cada hora que sus propios ingenieros perdían escribiendo ceremonia era una hora que su producto no ganaba.

La salida obvia era migrar a otro lenguaje de la JVM. La descartaron después de evaluarla en serio:

🟣

Scala

Expresivo y potente, pero con un compilador que sobre una base del tamaño de IntelliJ implicaba tiempos de compilación inaceptables. Para una empresa cuyo ciclo interno es editar, compilar y probar cientos de veces al día, eso era un veto.

🟣

Groovy

Cómodo de escribir, pero de tipado dinámico. Sin tipos estáticos no hay refactorización fiable, y la refactorización fiable era literalmente el producto que JetBrains vendía.

🟣

Clojure

Un Lisp excelente, pero exigía reescribir el modelo mental de todo el equipo y no encajaba con una base orientada a objetos ya existente.

🟣

Quedarse en Java

La opción por defecto, y la que estaban pagando. Sin nulabilidad en el tipo, sin funciones de extensión y sin inferencia real, cada mejora dependía del calendario de otra compañía.

De ahí sale el proyecto, anunciado públicamente en julio de 2011 y liberado bajo licencia Apache 2.0 en febrero de 2012. El nombre viene de la isla de Kotlin, cerca de San Petersburgo, donde estaba el equipo: un guiño deliberado a Java, que también es una isla.

Conviene subrayar un rasgo del contexto que rara vez se menciona y que lo condiciona todo: JetBrains iba a ser su propio primer usuario a gran escala. Eso elimina de raíz el sesgo habitual del diseño de lenguajes, donde quien decide no sufre las consecuencias. Aquí el equipo que elegía la sintaxis sabía que dentro de dos años tendría que migrar con ella una base de código enorme y en producción continua. El resultado es un lenguaje con muy pocas apuestas arriesgadas y con una obsesión desproporcionada por la ruta de migración.

Los objetivos de diseño, en orden

Los objetivos que JetBrains declaró no son una lista de deseos, sino una jerarquía con desempates explícitos. Ese orden es lo que hace predecible el lenguaje.

  1. Tipado estático y compilación rápida. El requisito era compilar al menos tan rápido como javac sobre la misma base. Cualquier característica que degradase la velocidad de compilación entraba con sospecha.
  2. Interoperabilidad completa con Java. Bidireccional, sin adaptadores, sin capas de traducción y sin obligar a migrar nada de golpe.
  3. Más seguridad y menos ruido que Java. Nulabilidad en el sistema de tipos, inmutabilidad por defecto en las declaraciones, inferencia local.
  4. Diseñado para herramientas. Una gramática sin ambigüedades, insensible a la indentación y analizable de forma incremental incluso sobre código roto a medio escribir.

El cuarto punto es el más subestimado y el más revelador. Kotlin lo diseñó gente que escribe analizadores para editores, no gente que escribe artículos. La sintaxis evita construcciones que obliguen al compilador a mirar muy lejos para decidir qué es un identificador: por eso los tipos van detrás de los dos puntos, por eso las declaraciones empiezan con una palabra clave como val, fun o class, y por eso no hay operadores definidos por el usuario. Todo eso hace que el IDE pueda dar autocompletado correcto a mitad de una línea incompleta.

flowchart TD
A[JetBrains 2010: millones de lineas de Java] --> B[Java evoluciona demasiado lento]
B --> C[Probar Scala]
C --> D[Compilacion demasiado lenta a esa escala]
B --> E[Probar Groovy]
E --> F[Tipado dinamico rompe la refactorizacion]
B --> G[Crear un lenguaje propio]
G --> H[Restriccion dura: interoperar al cien por cien]
H --> I[Kotlin, anunciado en 2011]
style H fill:#a6e3a1,color:#11111b
style I fill:#89b4fa,color:#11111b

La interoperabilidad como restricción fundacional

Aquí está la decisión que lo explica todo. JetBrains no quería un lenguaje mejor: quería un lenguaje al que pudiera migrar IntelliJ archivo a archivo, sin congelar el desarrollo del producto durante años. Eso convierte la interoperabilidad en una restricción de arranque, no en un adorno posterior.

La consecuencia técnica es que Kotlin no introduce un universo de tipos paralelo. kotlin.String es java.lang.String en tiempo de ejecución. Un List<T> de Kotlin es un java.util.List en el bytecode, sin envoltorio ni copia. Llamar a una biblioteca de Java desde Kotlin no cuesta nada porque no hay nada que convertir.

import java.nio.file.Files
import java.nio.file.Path

fun leerLineas(ruta: String): List<String> =
    Files.readAllLines(Path.of(ruta))   // devuelve java.util.List, se usa tal cual

El caso difícil es la nulabilidad. Java no la codifica en el bytecode, así que Kotlin tenía dos opciones malas y eligió una tercera. Podía tratar todo lo que viene de Java como anulable, lo que habría llenado el código de comprobaciones redundantes y habría hecho la migración insoportable. Podía tratarlo como no anulable, lo que habría sido mentir. En su lugar inventó los tipos plataforma, que se escriben con una admiración al final en los mensajes del compilador y que significan «no lo sé, decide tú»: se permiten ambos usos y el error, si llega, llega en el punto de uso. Si la biblioteca Java lleva anotaciones como @Nullable o @NotNull, el compilador las respeta y el tipo deja de ser plataforma.

En la dirección contraria hay un catálogo de anotaciones para que el código Kotlin se vea idiomático desde Java. Su existencia es la prueba de que la interoperabilidad se diseñó en las dos direcciones, no solo en la cómoda:

class Config private constructor(val ruta: String, val modo: Int) {

    @Throws(java.io.IOException::class)          // Java recupera la firma que espera
    fun guardar() { /* ... */ }

    companion object {
        @JvmStatic                               // Config.crear() desde Java
        @JvmOverloads                            // genera las sobrecargas sin defaults
        fun crear(ruta: String, modo: Int = 0) = Config(ruta, modo)
    }
}

Cada una de esas anotaciones existe porque Kotlin decidió algo distinto de Java y necesitaba una vía para que el otro lado no lo notara. Sin @JvmStatic, Java tendría que escribir el acceso a través del campo Companion. Sin @JvmOverloads, los parámetros con valor por defecto de Kotlin se ven desde Java como un único método con todos los argumentos obligatorios, porque el bytecode no tiene el concepto de argumento omitido.

El precio que pagó

Comprometerse con la plataforma tiene coste, y conviene verlo sin romanticismo. Kotlin heredó el borrado de tipos genéricos, por lo que no puede inspeccionar el argumento de tipo en tiempo de ejecución sin el truco de reified en funciones inline. Heredó la distinción entre primitivos y objetos, que solo queda oculta: Int compila a int cuando puede y a java.lang.Integer cuando debe boxear. Heredó el modelo de herencia simple con interfaces, y las funciones de extensión se resuelven de forma estática precisamente porque son métodos estáticos en el bytecode.

Y hubo rupturas deliberadas que la interoperabilidad tuvo que absorber. Kotlin eliminó las excepciones comprobadas, eliminó los miembros estáticos en favor de los objetos acompañantes y añadió los argumentos con valor por defecto. Cada una de esas decisiones dejó una cicatriz visible en forma de anotación: son el registro público de un desacuerdo entre el diseño que querían y la plataforma que no podían romper.

Hay además una asimetría permanente que conviene aceptar pronto. La interoperabilidad es total, pero no es simétrica en garantías. Cuando llamas a Java desde Kotlin pierdes la garantía de nulabilidad en la frontera; cuando llamas a Kotlin desde Java pierdes las garantías del lenguaje entero, porque Java puede pasar null a un parámetro declarado como no anulable. Kotlin lo mitiga insertando comprobaciones en los puntos de entrada públicos, de modo que el fallo aparece inmediatamente y con un mensaje claro, en lugar de propagarse hasta un punto lejano. Es una decisión típicamente pragmática: no puede impedir el error, así que se limita a hacerlo barato de diagnosticar.

// visto desde Java, este parámetro se puede violar; Kotlin comprueba al entrar
fun registrar(nombre: String) {
    // el compilador genera aquí una comprobación de no nulidad
    println(nombre.uppercase())
}
La restricción es el diseño

Estudia Kotlin como un ejercicio de diseño bajo restricción dura y entenderás el lenguaje entero. La pregunta que JetBrains se hizo no fue «cuál es el mejor lenguaje posible», sino «cuál es el mejor lenguaje al que podemos migrar sin parar de facturar». Esa segunda pregunta es infinitamente más difícil, y su respuesta produce artefactos que un diseñador puro consideraría feos: tipos plataforma que renuncian a la garantía justo en la frontera, anotaciones cuyo único propósito es contentar a otro lenguaje, genéricos borrados que obligan a inventar reified. Ninguna de esas cosas se justifica desde la teoría; todas se justifican desde la migración incremental. Quince años después, con Kotlin 2.4 y el compilador K2, la validación es empírica: los lenguajes JVM que rompieron con Java para ser más limpios ocupan nichos, y el que se negó a romper se convirtió en el lenguaje oficial de Android. La lección transferible no es sobre Kotlin, es sobre tu propio trabajo. Un diseño que exige que todo el mundo salte a la vez casi nunca se adopta; un diseño que permite convivir con lo que ya existe casi siempre gana, aunque llegue con cicatrices.

⚔️ Arqueología del diseño
  1. Escribe una función Kotlin que reciba una List<String> de una API de Java y razona por escrito qué tipo exacto está viendo el compilador antes de que tú lo anotes.
  2. Busca en una biblioteca Java que uses tres métodos que devuelvan null legalmente y anota cómo los declararías si reescribieras esa API en Kotlin.
  3. Compila una clase Kotlin con un objeto acompañante y decompila el bytecode para ver dónde aparece el campo Companion. Repite añadiendo @JvmStatic y compara.
  4. Argumenta en un párrafo por qué los tipos plataforma son preferibles a asumir que todo lo de Java es anulable, y qué garantía se pierde a cambio.
  5. Localiza el anuncio original de Kotlin de julio de 2011 y contrasta los objetivos declarados entonces con el lenguaje que existe hoy: señala uno que se cumplió y uno que se reformuló.