Kotlin vs Java: ventajas de Kotlin sobre Java y cuál elegir
Kotlin vs Java es la comparación entre dos lenguajes que comparten casa: los dos se compilan para la máquina virtual de Java (JVM), usan las mismas librerías y pueden convivir en el mismo proyecto. Las ventajas de Kotlin sobre Java están en qué errores te deja cometer cada uno y en cuánto tienes que escribir para decir lo mismo.
Las listas de diferencias siguen la página oficial Comparison to Java de la documentación de Kotlin (actualizada el 14 de octubre de 2026). Los ejemplos se han ejecutado con Kotlin 2.4.21 y Java 25 (OpenJDK 25.0.5, la versión LTS actual).
Kotlin vs Java en una tabla
Esta tabla es un resumen nuestro a partir de la documentación de los dos lenguajes; cada fila se explica más abajo.
| Java 25 | Kotlin 2.4 | |
|---|---|---|
| Valores nulos | Cualquier referencia puede ser null; el fallo llega al ejecutar |
El tipo dice si puede ser nulo (String?); el compilador obliga a comprobarlo |
| Clases de datos | record (desde Java 16), inmutables |
data class, con copy() para crear variantes |
| Parámetros opcionales | Sobrecarga de métodos | Parámetros por defecto y con nombre |
| Añadir métodos a una clase ajena | Clases de utilidad con métodos estáticos | Funciones de extensión |
| Colecciones | La misma interfaz List para las modificables y las que no |
List (solo lectura) y MutableList |
| Excepciones comprobadas | Obligatorias | No existen |
| Código asíncrono | Hilos y, desde Java 21, hilos virtuales | Corrutinas |
| Estáticos | static |
companion object, funciones de nivel superior |
| Ternario | a ? b : c |
if como expresión |
| Android | Soportado | Preferente («Kotlin-first») y obligatorio para Jetpack Compose |
Ventajas de Kotlin sobre Java, con ejemplos
1. Los nulos los controla el sistema de tipos
Es la diferencia que muestra la portada. Esta función devuelve un apodo o, si no lo hay, null. En Java, el compilador no avisa de que apodo puede ser nulo:
public class Nulos {
static String apodo(String nombre) {
return nombre.equals("Robert") ? "Bob" : null;
}
public static void main(String[] args) {
String apodo = apodo("Ana");
System.out.println(apodo.length());
}
}
Compila sin problemas y falla al ejecutarlo:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null
at Nulos.main(Nulos.java:8)
En Kotlin, el tipo String? (con interrogación) significa «un texto o nulo», y String a secas, «un texto, nunca nulo». El mismo código no llega a compilar:
fun apodo(nombre: String): String? = if (nombre == "Robert") "Bob" else null
fun main() {
val apodo = apodo("Ana")
println(apodo.length)
}
nulos/Nulos.kt:5:18: error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?'.
println(apodo.length)
^
El compilador te obliga a decidir qué pasa con el nulo. ?. devuelve null en lugar de fallar, y ?: (el operador Elvis) da un valor alternativo:
fun apodo(nombre: String): String? = if (nombre == "Robert") "Bob" else null
fun main() {
val apodo = apodo("Ana")
println(apodo?.length)
println(apodo?.length ?: 0)
}
null
0
También vale un if (apodo != null): dentro de ese bloque, Kotlin ya trata apodo como String sin que lo conviertas tú. Es lo que la documentación llama smart cast. Java tiene remedios parciales: anotaciones como @Nullable y herramientas de análisis que avisan de posibles nulos, pero no forman parte del lenguaje y el compilador no las exige.
2. Clases de datos con menos código
Para una clase que solo guarda datos, Java 25 tiene los records y Kotlin las data classes. Los dos generan el constructor, equals, hashCode y toString:
record Usuario(String nombre, int edad) {}
void main() {
var ana = new Usuario("Ana", 34);
var mayor = new Usuario(ana.nombre(), ana.edad() + 1);
IO.println(ana);
IO.println(mayor);
IO.println(ana.equals(new Usuario("Ana", 34)));
}
Usuario[nombre=Ana, edad=34]
Usuario[nombre=Ana, edad=35]
true
data class Usuario(val nombre: String, val edad: Int)
fun main() {
val ana = Usuario("Ana", 34)
val mayor = ana.copy(edad = ana.edad + 1)
println(ana)
println(mayor)
println(ana == Usuario("Ana", 34))
}
Usuario(nombre=Ana, edad=34)
Usuario(nombre=Ana, edad=35)
true
Aquí la distancia es pequeña. Lo que añade Kotlin es copy(): crear una variante cambiando solo un campo, sin repetir los demás. En Java hay que llamar al constructor con todos. Y == en Kotlin compara el contenido, como equals en Java; tienes el detalle en igualdad referencial y estructural en Kotlin.
El ejemplo de Java usa los ficheros compactos de Java 25 (JEP 512): un void main() sin clase alrededor y IO.println. Con versiones anteriores necesitas la clase y System.out.println.
3. Parámetros por defecto y con nombre
En Java, para que un parámetro sea opcional se escribe una versión del método por cada combinación (sobrecarga):
String saludar(String nombre) {
return saludar(nombre, "Hola", false);
}
String saludar(String nombre, boolean exclamacion) {
return saludar(nombre, "Hola", exclamacion);
}
String saludar(String nombre, String saludo, boolean exclamacion) {
String cierre = exclamacion ? "!" : ".";
return saludo + ", " + nombre + cierre;
}
void main() {
IO.println(saludar("Ana"));
IO.println(saludar("Luis", true));
IO.println(saludar("Marta", "Buenas", false));
}
En Kotlin basta una función con valores por defecto, y al llamarla puedes nombrar los argumentos, en el orden que quieras:
fun saludar(nombre: String, saludo: String = "Hola", exclamacion: Boolean = false): String {
val cierre = if (exclamacion) "!" else "."
return "$saludo, $nombre$cierre"
}
fun main() {
println(saludar("Ana"))
println(saludar("Luis", exclamacion = true))
println(saludar(saludo = "Buenas", nombre = "Marta"))
}
Los dos imprimen lo mismo:
Hola, Ana.
Hola, Luis!
Buenas, Marta.
Fíjate también en "$saludo, $nombre$cierre": son las plantillas de texto de Kotlin, que meten variables dentro de un string sin concatenar con +.
4. Funciones de extensión
Una función de extensión añade un método a una clase que no es tuya, como String, sin heredar de ella ni modificarla:
fun String.iniciales(): String =
split(" ").filter { it.isNotBlank() }.joinToString("") { it.first().uppercase() }
fun main() {
println("ada lovelace".iniciales())
println("grace brewster hopper".iniciales())
}
En Java, lo habitual es una clase de utilidad con un método estático:
import java.util.Arrays;
import java.util.stream.Collectors;
class Textos {
static String iniciales(String texto) {
return Arrays.stream(texto.split(" "))
.filter(palabra -> !palabra.isBlank())
.map(palabra -> palabra.substring(0, 1).toUpperCase())
.collect(Collectors.joining());
}
}
void main() {
IO.println(Textos.iniciales("ada lovelace"));
IO.println(Textos.iniciales("grace brewster hopper"));
}
AL
GBH
El resultado es el mismo; cambia cómo se lee la llamada: "ada lovelace".iniciales() frente a Textos.iniciales("ada lovelace"). Si los streams de Java te resultan nuevos, mira Java Stream: guía práctica.
5. Colecciones de solo lectura que el compilador vigila
En Java, List.of() crea una lista que no se puede modificar, pero su tipo es el mismo List que tiene add(). El compilador no protesta y el error llega al ejecutar:
import java.util.List;
void main() {
List<Integer> numeros = List.of(1, 2, 3);
numeros.add(4);
IO.println(numeros);
}
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:159)
at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:164)
at Lista.main(Lista.java:5)
Kotlin separa las dos interfaces: List es de solo lectura y no tiene add(); MutableList sí. El error aparece al compilar:
fun main() {
val numeros = listOf(1, 2, 3)
numeros.add(4)
println(numeros)
}
coleccion/Lista.kt:3:13: error: unresolved reference 'add' on receiver of type 'List<Int>'.
numeros.add(4)
^^^
Si quieres una lista con un elemento más, creas otra; si necesitas modificarla, la declaras mutable:
fun main() {
val numeros = listOf(1, 2, 3)
val conCuatro = numeros + 4
val cesta = mutableListOf("pan")
cesta.add("leche")
println(numeros)
println(conCuatro)
println(cesta)
}
[1, 2, 3]
[1, 2, 3, 4]
[pan, leche]
6. Sin excepciones comprobadas
En Java, si un método puede lanzar una excepción comprobada (por ejemplo, IOException al leer un fichero), quien lo llama tiene que capturarla con try/catch o declararla con throws. Kotlin no tiene excepciones comprobadas: puedes capturarlas igual, pero el compilador no te obliga. Es una ventaja para escribir menos ceremonia y un riesgo si olvidas tratar un error que importa; la decisión pasa a ser tuya.
7. Corrutinas para el código asíncrono
Las corrutinas de Kotlin permiten escribir código asíncrono (una llamada de red, una lectura de base de datos) con el aspecto de código secuencial, sin encadenar callbacks. Necesitan la librería kotlinx.coroutines, por eso no hay ejemplo aquí. Java ha respondido en otra dirección con los hilos virtuales de Java 21 (JEP 444), que abaratan tener muchos hilos a la vez.
Qué tiene Java que Kotlin no
Según la documentación de Kotlin, estas son cosas de Java que Kotlin no copia. La segunda columna es la alternativa que propone Kotlin:
| En Java | En Kotlin |
|---|---|
| Excepciones comprobadas | No existen |
Tipos primitivos que no son clases (int, double) |
Todo se usa como objeto (Int, Double); al compilar, Kotlin usa por debajo los primitivos de la JVM cuando puede |
Miembros static |
companion object, funciones de nivel superior, de extensión o @JvmStatic |
Comodines de tipos (? extends T) |
Varianza en la declaración y proyecciones de tipos |
Operador ternario a ? b : c |
if como expresión: val max = if (a > b) a else b |
| Records | data class |
| Visibilidad de paquete (package-private) | internal, visible en todo el módulo (modificadores de acceso en Kotlin) |
La varianza decide si, por ejemplo, una lista de String puede usarse donde se espera una lista de Any. Java lo indica con comodines cada vez que usa el tipo; Kotlin suele indicarlo una vez, al declarar la clase (out T, in T), y eso es la varianza en la declaración. Las proyecciones de tipos son el caso puntual: out o in en un uso concreto.
La documentación añade un matiz: Kotlin no tiene pattern matching como Java, pero sus smart casts cubren casos parecidos. Este ejemplo hace lo mismo en los dos lenguajes, con una jerarquía cerrada (sealed):
sealed interface Forma permits Circulo, Cuadrado {}
record Circulo(double radio) implements Forma {}
record Cuadrado(double lado) implements Forma {}
double area(Forma forma) {
return switch (forma) {
case Circulo c -> Math.PI * c.radio() * c.radio();
case Cuadrado q -> q.lado() * q.lado();
};
}
void main() {
IO.println(area(new Cuadrado(3)));
IO.println(area(new Circulo(1)));
}
sealed interface Forma
data class Circulo(val radio: Double) : Forma
data class Cuadrado(val lado: Double) : Forma
fun area(forma: Forma): Double = when (forma) {
is Circulo -> Math.PI * forma.radio * forma.radio
is Cuadrado -> forma.lado * forma.lado
}
fun main() {
println(area(Cuadrado(3.0)))
println(area(Circulo(1.0)))
}
9.0
3.141592653589793
Los dos dan el mismo resultado y, en los dos, el compilador comprueba que no falte ningún caso. Aquí Java ha alcanzado a Kotlin. Más sobre when en control de flujo en Kotlin.
Qué tiene Kotlin que Java no
La lista oficial de lo que Kotlin tiene y Java no, agrupada por nosotros:
- Seguridad y tipos: seguridad ante nulos, inferencia de tipos en variables y propiedades (Java solo la tiene en variables locales, con
var, desde Java 10), varianza en la declaración y proyecciones de tipos. - Menos código: propiedades, constructores primarios, data classes, parámetros por defecto y con nombre, plantillas de texto y funciones de nivel superior (fuera de cualquier clase).
- Ampliar sin heredar: funciones de extensión y delegación de primera clase.
- Expresividad: rangos (
1..10), sobrecarga de operadores, funcionesinfix, y lambdas con funcionesinline(el compilador copia el cuerpo de la función en cada llamada) para crear estructuras de control propias sin coste extra. - Objetos: singletons con
objecty companion objects. - Asincronía: corrutinas.
- Multiplataforma: declaraciones
expect(se declara en el código común) yactual(cada plataforma aporta su implementación), para compartir código entre JVM, Android, iOS y web. - Librerías: el modo de API explícita (explicit API mode), que obliga a escribir la visibilidad de cada declaración y el tipo de las funciones y propiedades públicas.
La documentación tiene además una lista de problemas de Java que Kotlin corrige («Some Java issues addressed in Kotlin»). Además de los nulos y las excepciones comprobadas, ya vistos, incluye estos (explicación nuestra):
| En Java | En Kotlin |
|---|---|
Tipos genéricos «en bruto» (raw types): List sin decir de qué |
No existen: siempre se indica el tipo |
Los arrays son covariantes: un String[] se acepta como Object[], y guardar en él otro tipo falla con el programa en marcha |
Los arrays son invariantes: un Array<String> no es un Array<Any> |
| Para pasar una función se usan interfaces de un solo método (conversiones SAM) | Tiene tipos función propios, como (Int) -> String; también existen las interfaces funcionales (fun interface) |
Comodines (? extends T) al usar genéricos |
Varianza en el uso sin comodines (Array<out Any>) |
| Las colecciones mutables y las de solo lectura comparten interfaz | Interfaces separadas: List (solo lectura) y MutableList |
Enum.values() crea un array nuevo en cada llamada |
La propiedad entries, que devuelve siempre la misma lista |
Java también ha avanzado
Muchas de las comparaciones de 2018 ya no valen tal cual. Desde entonces, Java ha incorporado:
| Versión | Novedad | Equivalente en Kotlin |
|---|---|---|
| Java 10 | var en variables locales (JEP 286) |
val y var |
| Java 16 | Records (JEP 395) | data class |
| Java 21 | Pattern matching en switch (JEP 441) |
when con smart casts |
| Java 25 | Ficheros compactos y void main() sin clase (JEP 512) |
fun main() de nivel superior |
La distancia es menor, pero la seguridad ante nulos, la separación de colecciones de solo lectura, los parámetros por defecto y las funciones de extensión siguen sin equivalente en Java. Sobre si Java tiene futuro con Kotlin al lado, hay un artículo aparte: ¿Ha quedado Java obsoleto con Kotlin?.
Kotlin y Java en el mismo proyecto
La interoperabilidad no es un detalle: es lo que permite introducir Kotlin sin reescribir nada. Esta clase de Java:
public class Iva {
public static final double GENERAL = 0.21;
public static double conIva(double base) {
return base * (1 + GENERAL);
}
}
se usa desde Kotlin como si fuera propia, igual que cualquier clase de la librería estándar de Java (java.time.LocalDate):
import java.time.LocalDate
fun main() {
val base = 100.0
println("Total: ${Iva.conIva(base)}")
println("Tipo general: ${Iva.GENERAL}")
println(LocalDate.of(2026, 11, 9).dayOfWeek)
}
javac -d out Iva.java
kotlinc -cp out Factura.kt -d out
kotlin -cp out FacturaKt
Total: 121.0
Tipo general: 0.21
MONDAY
kotlinc compila Kotlin a ficheros .class como los de Java, y FacturaKt es el nombre de la clase que genera para las funciones de nivel superior de Factura.kt. En la otra dirección, Java también puede llamar a código Kotlin.
Por qué usar Kotlin en Android: lo que dice Google
| Año | Qué anunció Google |
|---|---|
| 2017 | Soporte oficial de Kotlin en Android, anunciado el 17 de mayo |
| 2019 | Android será «cada vez más Kotlin-first» (Google I/O 2019; traducción nuestra de «increasingly Kotlin-first») |
«Kotlin-first» significa, según la página oficial de Android, que las herramientas, las librerías de Jetpack, los ejemplos, la documentación y la formación nuevas se diseñan pensando primero en Kotlin, sin dejar de dar soporte a Java. Google recomienda empezar cualquier app nueva con Kotlin.
La misma página resume qué ofrece cada lenguaje en Android (tabla resumida por nosotros):
| Java | Kotlin | |
|---|---|---|
| SDK, Android Studio, Lint, documentación, AndroidX | Sí | Sí |
| APIs específicas de Kotlin (KTX, corrutinas) | No aplica | Sí |
| Formación y ejemplos | Según disponibilidad («best effort») | Sí |
| Proyectos multiplataforma | No | Sí |
| Jetpack Compose | No | Sí |
Las razones que da Google para preferir Kotlin coinciden con lo visto arriba: es más conciso, más seguro frente a los nulos, interoperable con Java y tiene corrutinas para la concurrencia. Google afirma también que las apps Android con código Kotlin tienen un 20 % menos de probabilidades de fallar. Es un dato de la propia Google y la página no publica cómo se ha medido, así que conviene tomarlo como orientativo. Además, describe una asociación, no una causa: no demuestra que esas apps fallen menos por usar Kotlin.
Además, Kotlin es de código abierto (licencia Apache 2.0, con el código en GitHub), y Google es cofundadora de la Kotlin Foundation junto a JetBrains, la empresa que crea Kotlin e IntelliJ IDEA, la base de Android Studio.
Si quieres empezar, tienes un Hola Mundo con Android Studio y Kotlin.
Kotlin fuera de Android: la charla «Fullstack Kotlin»
Kotlin no se limita a Android. La versión de 2018 de esta comparación enlazaba la charla «Fullstack Kotlin: aplicaciones web modernas cliente y servidor», de César Alberca y Javier Sánchez, en el canal Beyond Tech Notes de YouTube.
La versión original de este artículo ya la enlazaba en octubre de 2018, así que las herramientas han cambiado. Hoy, para la web, la documentación de Kotlin propone Compose Multiplatform con Kotlin/Wasm como destino principal, y Kotlin/JS para navegadores antiguos o para integrarse con librerías de JavaScript como React. Si la charla te anima a probar, empieza por esa documentación y no por las librerías que aparecen en el vídeo.
¿Kotlin o Java? Cuál elegir
- App Android nueva: Kotlin. Es la recomendación de Google, y Jetpack Compose, la forma actual de crear interfaces, solo funciona con Kotlin.
- App Android existente en Java: no hace falta reescribirla. Puedes escribir en Kotlin lo nuevo y convivir con lo antiguo.
- Backend: los dos son válidos. Kotlin puede usar las mismas librerías y frameworks de Java y aporta lo visto arriba; conviene tener en cuenta qué lenguaje conoce ya el equipo.
- Aprender a programar: cualquiera de los dos. Desde Java 25, un primer programa en Java es casi tan corto como en Kotlin.
- Ya sabes Java y quieres aprender Kotlin: los Kotlin Koans son ejercicios oficiales pensados para ti. Después, variables y tipos en Kotlin y qué es Kotlin completan la base.
Cambiar de lenguaje no arregla un mal diseño: la seguridad ante nulos evita una clase de errores, pero los tests y un diseño simple siguen haciendo falta en los dos. Si te interesa ese enfoque, empieza por qué es TDD.
Cómo se han ejecutado los ejemplos
Todos los ejemplos de este artículo se han ejecutado el 9 de noviembre de 2026 en un contenedor eclipse-temurin:25 (OpenJDK 25.0.5 LTS), con el compilador oficial de Kotlin 2.4.21 descargado de las releases de GitHub de JetBrains (kotlinc-jvm 2.4.21). Los de Java se lanzan directamente desde el fichero fuente, y los de Kotlin se compilan y se ejecutan:
java Nulos.java
kotlinc Nulos.kt -d out
kotlin -cp out NulosKt
Fuentes
Consultadas el 9 de noviembre de 2026:
- Kotlin, Comparison to Java (actualizada el 14 de octubre de 2026): lo que tiene cada lenguaje y el otro no, y los problemas de Java que Kotlin corrige.
- Kotlin, Enum classes (
entries) y API guidelines: simplicity (modo de API explícita). - Kotlin, Kotlin/JS overview (actualizada el 23 de septiembre de 2026): opciones actuales para la web.
- Android Developers, Android’s Kotlin-first approach: qué significa «Kotlin-first», la tabla de soporte, el dato del 20 % y la Kotlin Foundation.
- Android Developers Blog, Android Announces Support for Kotlin (17 de mayo de 2017).
- Kotlin, repositorio en GitHub (licencia Apache 2.0) y release 2.4.21.
- OpenJDK: JEP 286 (
var, Java 10), JEP 395 (records, Java 16), JEP 441 (pattern matching enswitch, Java 21), JEP 444 (hilos virtuales, Java 21) y JEP 512 (ficheros compactos, Java 25). - YouTube, vídeo SuykoyC0T74: título y canal. No se ha revisado el contenido del vídeo.