Kotlin
A concise multiplatform language developed by JetBrains
Evaluación de Kotlin en proyectos reales
Artículo de Urs Peter, ingeniero de software sénior y formador de Kotlin certificado por JetBrains. Para los lectores que busquen una forma más estructurada de desarrollar sus habilidades en Kotlin, Urs también dirige el Kotlin Upskill Program en Xebia Academy.
Este es el segundo artículo de La guía definitiva para adoptar Kotlin en un entorno dominado por Java, una serie que sigue cómo crece la adopción de Kotlin entre equipos reales, desde la curiosidad de un único desarrollador hasta la transformación de toda la empresa.
Lea la primera parte: Introducción a Kotlin para desarrolladores Java
La fase de evaluación: más allá de la experimentación con Kotlin
Una vez que se sienta cómodo haciendo pruebas en Kotlin, es hora de una evaluación más exhaustiva. Puede recurrir a dos enfoques principales:
- Crear un nuevo microservicio o aplicación en Kotlin
- Ampliar o convertir una aplicación Java existente
1. Crear un nuevo microservicio o aplicación en Kotlin
Empezando de cero con una nueva aplicación o microservicio disfrutará de toda la experiencia Kotlin sin las limitaciones del código heredado. Este enfoque suele ser el más práctico para el aprendizaje, y muestra con mayor claridad los puntos fuertes de Kotlin.
Consejo pro: pida ayuda a un experto durante esta fase. Aunque los desarrolladores confían en sus capacidades por naturaleza, evitar los primeros errores en forma de Kotlin al estilo Java y la falta de bibliotecas impulsadas por Kotlin puede ahorrar meses de deuda técnica.

Así es como puede evitar los errores más comunes al utilizar Kotlin desde un entorno Java:
Error: elegir un marco de trabajo diferente del que utiliza en Java.
Consejo: cíñase a su marco actual.
Lo más probable es que haya utilizado Spring Boot con Java, así que utilícelo también con Kotlin. La compatibilidad de Spring Boot con Kotlin es excepcional, por lo que no le beneficia de ningún modo utilizar otra cosa. Es más, se vería obligado a aprender no solo un nuevo lenguaje, sino también un nuevo marco de trabajo, lo que únicamente añade complejidad sin aportar ninguna ventaja.
Importante: Spring interfiere con el principio de «herencia por diseño» de Kotlin, que exige marcar explícitamente las clases abiertas para poder extenderlas.
Para evitar añadir la palabra clave abierta a todas las clases relacionadas con Spring (como @Configuration, etc.), utilice el siguiente complemento de compilación: https://kotlinlang.org/docs/all-open-plugin.html#spring-support. Si crea un proyecto Spring con la conocida herramienta en línea Spring initializr, este complemento de compilación ya estará configurado para usted.
Error: escribir Kotlin al estilo Java, confiando en las API comunes de Java en lugar de en la biblioteca estándar de Kotlin:
Esta lista puede ser muy larga, así que centrémonos en los errores más comunes:
Error 1: utilizar Java Stream en lugar de Kotlin Collections.
Consejo: utilice siempre Kotlin Collections.
Las Kotlin Collections son totalmente interoperables con las Java Collections, pero están equipadas con funciones de orden superior sencillas y cargadas de funcionalidades que dejan Java Stream obsoleto.
A continuación se muestra un ejemplo cuyo objetivo es elegir los 3 productos más vendidos por ingresos (precio * número vendido) agrupados por categoría de producto:
Java
record Product(String name, String category, double price, int sold){}
List products = List.of(
new Product("Lollipop", "sweets", 1.2, 321),
new Product("Broccoli", "vegetable", 1.8, 5);
Map<String, List> top3RevenueByCategory =
products.stream()
.collect(Collectors.groupingBy(
Product::category,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream()
.sorted(Comparator.comparingDouble(
(Product p) -> p.price() * p.sold())
.reversed())
.limit(3)
.toList()
)
)
);
Kotlin
val top3RevenueByCategory: Map<String, List> =
products.groupBy { it.category }
.mapValues { (_, list) ->
list.sortedByDescending { it.price * it.sold }.take(3)
}
La interoperabilidad entre Kotlin y Java le permite trabajar con clases y registros Java como si fueran nativos de Kotlin, aunque también podría utilizar una clase (de datos) Kotlin en su lugar.
Error 2: seguir utilizando los Optional de Java.
Consejo: adopte los tipos anulables.
Una de las principales razones por las que los desarrolladores Java se pasan a Kotlin es por la compatibilidad con la nulabilidad integrada en Kotlin, que dice adiós a las NullPointerExceptions. Por lo tanto, intente utilizar solo tipos «Nullable», y no use más «Optionals». ¿Todavía tiene «Optionals» en sus interfaces? Así es como puede deshacerse fácilmente de ellos convirtiéndolos en tipos «Nullable»:
Kotlin
//Let’s assume this repository is hard to change, because it’s a library you depend on
class OrderRepository {
//it returns Optional, but we want nullable types
fun getOrderBy(id: Long): Optional = …
}
//Simply add an extension method and apply the orElse(null) trick
fun OrderRepository.getOrderByOrNull(id: Long): Order? =
getOrderBy(id).orElse(null)
//Now enjoy the safety and ease of use of nullable types:
//Past:
val g = repository.getOrderBy(12).flatMap { product ->
product.goody.map { it.name }
}.orElse("No goody found")
//Future:
val g = repository.getOrderByOrNull(12)?.goody?.name ?: "No goody found"
Error 3: seguir utilizando envoltorios estáticos.
Consejo: adopte los métodos de extensión.
Los métodos de extensión le ofrecen muchas ventajas:
- Hacen que su código sea mucho más fluido y legible que los envoltorios.
- Se pueden encontrar con la finalización de código, lo que no ocurre con los envoltorios.
- Dado que las extensiones deben importarse, le permiten utilizar selectivamente la funcionalidad ampliada en una sección específica de su aplicación.
Java
//Very common approach in Java to add additional helper methods
public class DateUtils {
public static final DateTimeFormatter DEFAULT_DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public String formatted(LocalDateTime dateTime,
DateTimeFormatter formatter) {
return dateTime.format(formatter);
}
public String formatted(LocalDateTime dateTime) {
return formatted(dateTime, DEFAULT_DATE_TIME_FORMATTER);
}
}
//Usage
formatted(LocalDateTime.now());
Kotlin
val DEFAULT_DATE_TIME_FORMATTER: DateTimeFormatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
//Use an extension method, including a default argument, which omits the need for an overloaded method.
fun LocalDateTime.asString(
formatter: DateTimeFormatter = DEFAULT_DATE_TIME_FORMATTER): String =
this.format(formatter)
//Usage
LocalDateTime.now().formatted()
Tenga en cuenta que Kotlin ofrece métodos y variables de nivel superior. Esto implica que podemos simplemente declarar, por ejemplo, el DEFAULT_DATE_TIME_FORMATTER de nivel superior sin necesidad de vincularlo a un objeto como ocurre en Java.
Error 4: depender (torpemente) de las API de Java.
Consejo: utilice la correspondiente de Kotlin.
La biblioteca estándar de Kotlin utiliza métodos de extensión para hacer que las bibliotecas Java sean mucho más fáciles de usar, aunque la implementación subyacente siga siendo Java. Casi todas las principales bibliotecas y marcos de trabajo de terceros, como Spring, han hecho lo mismo.
Ejemplo de biblioteca estándar:
Java
String text;
try (
var reader = new BufferedReader(
new InputStreamReader(new FileInputStream("out.txt"),
StandardCharsets.UTF_8))) {
text = reader
.lines()
.collect(Collectors.joining(System.lineSeparator()));
}
System.out.println("Downloaded text: " + text + "n");
Kotlin
//Kotlin has enhanced the Java standard library with many powerful extension methods, like on java.io.*, which makes input stream processing a snap due to its fluent nature, fully supported by code completion
val text = FileInputStream("path").use {
it.bufferedReader().readText()
}
println("Downloaded text: $textn");
Ejemplo en Spring:
Java
final var books = RestClient.create()
.get()
.uri("http://.../api/books")
.retrieve()
.body( new ParameterizedTypeReference<List>(){}); // ⇦ inconvenient ParameterizedTypeReference
Kotlin
import org.springframework.web.client.body
val books = RestClient.create()
.get()
.uri("http://.../api/books")
.retrieve()
.body<List>() //⇦ Kotlin offers an extension that only requires the type without the need for a ParameterizedTypeReference
Error 5: utilizar un archivo distinto para cada clase pública.
Consejo: combine clases públicas relacionadas en un único archivo.
Esto le permite hacerse una idea de cómo está estructurado un (sub)dominio sin tener que navegar por decenas de archivos.
Java

Kotlin
//For domain classes consider data classes - see why below
data class User(val email: String,
//Use nullable types for safety and expressiveness
val avatarUrl: URL? = null,
var isEmailVerified: Boolean)
data class Account(val user:User,
val address: Address,
val mfaEnabled:Boolean,
val createdAt: Instant)
data class Address(val street: String,
val city: String,
val postalCode: String)
Error 6: depender del paradigma de programación mutable.
Consejo: adopte la inmutabilidad, el valor predeterminado en Kotlin
La tendencia en muchos lenguajes de programación (incluido Java) es clara: la inmutabilidad está ganando a la mutabilidad.
La razón es sencilla: la inmutabilidad evita efectos secundarios no deseados, lo que hace que el código sea más seguro, más predecible y más fácil de razonar. También simplifica la concurrencia, ya que los datos inmutables se pueden compartir libremente entre hilos sin riesgo de condiciones de carrera.
Por eso la mayoría de los lenguajes modernos —Kotlin entre ellos— enfatizan la inmutabilidad predeterminada o la fomentan en gran medida. En Kotlin, la inmutabilidad es la opción predeterminada, aunque la mutabilidad sigue siendo una opción cuando es realmente necesaria.
He aquí una guía rápida de la potencia de la inmutabilidad de Kotlin:
1. Use val en lugar de var
Priorice val sobre var. IntelliJ IDEA le notificará si ha utilizado un var para el que se podría utilizar un val.
2. Utilice clases de datos (inmutables) con copy(...)
Para las clases relacionadas con dominios, utilice clases data con val. Las clases data de Kotlin se comparan a menudo con los records de Java. Aunque existe cierto solapamiento, las clases data ofrecen la genial funcionalidad copy(...), cuya ausencia hace que la transformación de record (a menudo necesaria en la lógica empresarial) sea tan tediosa:
Java
//only immutable state
public record Person(String name, int age) {
//Lack of default parameters requires overloaded constructor
public Person(String name) {
this(name, 0);
}
//+ due to lack of String interpolation
public String sayHi() {
return "Hello, my name is " + name + " and I am " + age + " years old.";
}
}
//Usage
final var jack = new Person("Jack", 42);
jack: Person[name=Jack, age=5]
//The issue is here: transforming a record requires manually copying the identical state to the new instance ☹️
final var fred = new Person("Fred", jack.name);
Kotlin
//also supports mutable state (var)
data class Person(val name: String,
val age: Int = 0) {
//string interpolation
fun sayHi() = "Hi, my name is $name and I am $age years old."
}
val jack = Person("Jack", 42)
jack: Person(name=Jack, age=42)
//Kotlin offers the copy method, which, due to the ‘named argument’ feature, allows you to only adjust the state you want to change 😃
val fred = jack.copy(name = "Fred")
fred: Person(name=Fred, age=42)
Así pues, utilice clases de datos para clases relacionadas con dominios siempre que sea posible. Su naturaleza inmutable garantiza una experiencia segura, concisa y sin complicaciones al trabajar con el núcleo de su aplicación.
Consejo: priorice las colecciones inmutables sobre las mutables
Las colecciones inmutables tienen ventajas claras en cuanto a la seguridad de los subprocesos, pueden pasarse de una a otra de forma segura y son más fáciles de razonar. Aunque Java ofrece algunas funcionalidades de inmutabilidad para las colecciones, su uso es peligroso, porque provoca fácilmente excepciones durante la ejecución:
Java
List.of(1,2,3).add(4); ❌unsafe 😬! .add(...) compiles, but throws UnsupportedOperationException
Kotlin
//The default collections in Kotlin are immutable (read-only) listOf(1,2,3).add(4); //✅safe: does not compile val l0 = listOf(1,2,3) val l1 = l0 + 4 //✅safe: it will return a new list containing the added element l1 shouldBe listOf(1,2,3,4) //✅
Lo mismo se aplica al uso de Collections.unmodifiableList(...), que no solo no es seguro, sino que además requiere una asignación adicional:
Java
class PersonRepo {
private final List cache = new ArrayList();
// Java – must clone or wrap every call
public List getItems() {
return Collections.unmodifiableList(cache); //⚠️extra alloc
}
}
//Usage
personRepo.getItems().add(joe) ❌unsafe 😬! .add(...) can be called but throws UnsupportedOperationException
Kotlin
class PersonRepo {
//The need to type ‘mutable’ for mutable collections is intentional: Kotlin wants you to use immutable ones by default. But sometimes you need them:
private val cache: MutableList = mutableListOf()
fun items(): List = cache //✅safe: though the underlying collection is mutable, by returning it as its superclass List, it only exposes the read-only interface
}
//Usage
personRepo.items().add(joe) //✅safe:😬! Does not compile
Cuando se trata de concurrencia, deben preferirse las estructuras de datos inmutables, incluidas las colecciones. En Java, se requiere un mayor esfuerzo con las colecciones especiales que ofrecen una API diferente o limitada, como CopyOnWriteArrayList. En Kotlin, por otro lado, la lista de solo lectura List es aplicable a casi todos los casos de uso.
Si necesita colecciones mutables y a prueba de hilos, Kotlin ofrece colecciones persistentes (persistentListOf(...), persistentMapOf(...)), que comparten la misma potente interfaz.
Java
ConcurrentHashMap persons = new ConcurrentHashMap();
persons.put("Alice", 23);
persons.put("Bob", 21);
//not fluent and data copying going on
Map incPersons = new HashMap(persons.size());
persons.forEach((k, v) -> incPersons.put(k, v + 1));
//wordy and data copying going on
persons
.entrySet()
.stream()
.forEach(entry ->
entry.setValue(entry.getValue() + 1));
Kotlin
persistentMapOf("Alice" to 23, "Bob" to 21)
.mapValues { (key, value) -> value + 1 } //✅same rich API like any other Kotlin Map type and not data copying going on
Error 7: seguir utilizando constructores (o peor aún: intentar utilizar Lombok).
Consejo: utilice argumentos con nombre.
Los constructores son muy comunes en Java. Aunque son prácticos, añaden código adicional, no son seguros y aumentan la complejidad. En Kotlin, no sirven de nada, ya que una simple funcionalidad del lenguaje los hace obsoletos: los argumentos con nombre.
Java
public record Person(String name, int age) {
// Builder for Person
public static class Builder {
private String name;
private int age;
public Builder() {}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Person build() {
return new Person(name, age);
}
}
}
//Usage
new JPerson.Builder().name("Jack").age(36).build(); //compiles and succeeds at runtime
new JPerson.Builder().age(36).build(); //❌unsafe 😬: compiles but fails at runtime.
Kotlin
data class Person(val name: String, val age: Int = 0) //Usage - no builder, only named arguments. Person(name = "Jack") //✅safe: if it compiles, it always succeeds at runtime Person(name = "Jack", age = 36) //✅
2. Ampliar/convertir una aplicación Java existente
Si no tiene ninguna opción de nueva creación para probar Kotlin, añada nuevas funcionalidades de Kotlin o módulos enteros de Kotlin a una base de código Java existente . Gracias a la interoperabilidad fluida de Kotlin con Java, puede escribir código Kotlin que parece Java para los llamadores desde Java. Este enfoque permite lo siguiente:
- Migración gradual sin grandes reescrituras
- Pruebas reales de Kotlin en su contexto específico
- Crear confianza en el equipo con el código Kotlin de producción
En lugar de empezar por donde sea, considere estas diferentes opciones:
Desde fuera hacia dentro:
Comience en la sección «hoja» de su aplicación, por ejemplo, el controlador, el trabajo por lotes, etc., y luego diríjase hacia el dominio central. Esto le proporcionará las siguientes ventajas:
- Aislamiento en tiempo de compilación: las clases hoja rara vez tienen algo que dependa de ellas, por lo que puede pasarlas a Kotlin y seguir construyendo el resto del sistema sin cambios.
- Menos ediciones en cascada. Una IU/controlador convertido puede llamar al código de dominio Java existente sin apenas cambios gracias a la interoperabilidad fluida.
- Solicitudes de incorporación de cambios más reducidas, revisiones más fáciles. Puede migrar archivo por archivo o funcionalidad por funcionalidad.
De dentro hacia fuera:
Empezar por el núcleo y pasar después a las capas exteriores suele ser más arriesgado, ya que compromete las ventajas del enfoque de fuera a dentro mencionado anteriormente. Sin embargo, es una opción viable en los siguientes casos:
- Núcleo muy pequeño o autónomo. Si su capa de dominio son solo unos pocos POJO y servicios, darle la vuelta antes puede ser barato y desbloquear inmediatamente construcciones idiomáticas (clase de datos, clases de valores, jerarquías selladas).
- Rearquitectura de cualquier modo. Si planea refactorizar invariantes o introducir patrones DDD (objetos de valor, agregados) mientras migra, a veces es más limpio rediseñar primero el dominio en Kotlin.
- Contratos de seguridad nula estrictos. Poner Kotlin en el centro convierte el dominio en una «fortaleza a prueba de nulos»; las capas externas de Java pueden seguir enviando valores nulos, pero los límites se hacen explícitos y más fáciles de vigilar.
Módulo por módulo
- Si su arquitectura está organizada por funcionalidad y no por capas, y los módulos tienen un tamaño manejable, convertirlos uno a uno es una buena estrategia.
Funcionalidades del lenguaje para convertir Java a Kotlin
Kotlin ofrece una serie de funcionalidades, principalmente anotaciones, que permiten que su código Kotlin se comporte como Java nativo. Esto resulta especialmente valioso en entornos híbridos en los que Kotlin y Java coexisten dentro de la misma base de código.
Kotlin
class Person @JvmOverloads constructor(val name: String,
var age: Int = 0) {
companion object {
@JvmStatic
@Throws(InvalidNameException::class)
fun newBorn(name: String): Person = if (name.isEmpty())
throw InvalidNameException("name not set")
else Person(name, 0)
@JvmField
val LOG = LoggerFactory.getLogger(KPerson.javaClass)
}
}
Java
//thanks to @JvmOverloads an additional constructor is created, propagating Kotlin’s default arguments to Java
var john = new Person("John");
//Kotlin automatically generates getters (val) and setters (var) for Java
john.setAge(23);
var name = ken.getName();
//@JvmStatic and @JvmField all accessing (companion) object fields and methods as statics in Java
//Without @JvmStatic it would be: Person.Companion.newBorn(...)
var ken = Person.newBorn("Ken");
//Without @JvmField it would be: Person.Companion.LOG
Person.LOG.info("Hello World, Ken ;-)");
//@Throws(...) will put the checked Exception in the method signature
try {
Person ken = Person.newBorn("Ken");
} catch (InvalidNameException e) {
//…
}
Kotlin
@file:JvmName("Persons")
package org.abc
@JvmName("prettyPrint")
fun Person.pretty() =
Person.LOG.info("$name is $age old")
Java
//@JvmName for files and methods makes accessing static fields look like Java: without it would be: PersonKt.pretty(...) Persons.prettyPrint(ken)
Conversor de Java a Kotlin de IntelliJ IDEA
IntelliJ IDEA ofrece un conversor de Java a Kotlin, por lo que, en teoría, la herramienta puede hacerlo por usted. Sin embargo, el código resultante dista mucho de ser perfecto, así que utilícelo solo como punto de partida. A partir de ahí, conviértalo en una representación más propia de Kotlin. En la última sección de esta serie de artículos del blog se tratará más sobre este tema: Factores de éxito para la adopción de Kotlin a gran escala.
Si toma Java como punto de partida, lo más probable es que escriba Kotlin al estilo Java, lo que le ofrecerá algunas ventajas, pero no desatará todo el potencial de Kotlin. Por lo tanto, mi opción preferida es escribir una nueva aplicación.
Siguiente de la serie
Esta entrega de nuestra serie de artículos de blog La guía definitiva para adoptar Kotlin en un entorno dominado por Java demostró cómo los experimentos con Kotlin pueden evolucionar hasta convertirse en código de producción. Nuestro próximo artículo se centra en el lado humano de la adopción: convencer a sus compañeros. Se explica cómo presentar argumentos claros y basados en el código, guiar a los nuevos desarrolladores y crear una comunidad de Kotlin pequeña pero duradera dentro de su equipo.
Artículo original en inglés de:
