Kotlin es un lenguaje de programación moderno, eficiente y diseñado para ser sencillo y seguro. Desde su introducción, ha ganado popularidad debido a su interoperabilidad con Java y su capacidad para escribir código conciso y expresivo. A continuación, exploraremos algunas de las características clave de Kotlin que lo hacen destacar y cómo estas pueden mejorar la experiencia de desarrollo.
Variables: val y var
En Kotlin existen dos tipos principales de variables: val y var. La diferencia fundamental es que val define variables inmutables, es decir, una vez asignado un valor no se puede cambiar, mientras que var permite la reasignación de valores. Además, Kotlin introduce la palabra clave lateinit, que indica que una variable var se inicializará más tarde, lo que resulta útil cuando la inicialización inmediata no es posible.
Puedes comprobar si una variable lateinit ha sido inicializada utilizando el siguiente código, siempre que la variable no sea de un tipo primitivo:
lateinit var foo: String
if (::foo.isInitialized) {
}
Template strings
Los nombres de los métodos pueden utilizar template strings, lo que aporta mucho valor a la hora de definir los tests:
@Test
fun `should do something`() {
// ...
}
Raw strings
Se pueden escribir raw strings utilizando comillas triples ("""), facilitando trabajar con cadenas de texto que incluyan saltos de línea sin utilizar caracteres de escape.
val text = """
Hello World
using
multiline
"""
Data classes
Existe un tipo de objeto denominado data class para almacenar datos. Implementa los métodos equals, hashCode y toString, lo que facilita la manipulación y comparación de objetos sin tener que escribir mucho código adicional.
data class BookingCustomData(
val reference: String,
val origin: String,
val status: String,
val killSlots: String,
val noOfPackages: String,
val cargoType: String,
val weight: String,
val contractRef: String,
)
Companion objects en lugar de métodos estáticos
No existen los métodos estáticos; en su lugar existen los companion object, que funcionan como objetos singleton asociados a la clase y limitados a contener dichos miembros estáticos.
class Irrelevant {
companion object {
const val USERNAME1 = "USERNAME1"
fun addTenTo(number: Int): Int {
return number + 10
}
}
}
val aUserName = Irrelevant.USERNAME1
val newNumber = Irrelevant.addTenTo(10)
Herencia
Por defecto, las clases están declaradas como final y no se pueden sobrescribir sus métodos en clases derivadas. Para permitir la herencia se utiliza la palabra clave open. También se puede limitar la sobrescritura de métodos por clases nietas aplicando final en las clases hijas.
open class Parent {
open fun foo(): String {
return "Hello from Parent"
}
}
class Child : Parent() {
final override fun foo(): String {
return "Hello from Child"
}
}
Extensiones en interfaces y objetos
Tanto las interfaces como los companion object pueden tener propiedades y funciones de extensión. Estas extensiones pueden añadirse desde fuera de la clase original sin necesidad de modificar su código.
val <T> List<T>.lastIndex: Int
get() = size - 1
fun Irrelevant.Companion.print() {
println("Irrelevant")
}
Generics
En objetos basados en templates, Kotlin permite restringir cómo se pueden usar los tipos genéricos en clases o interfaces utilizando las anotaciones in y out.
outse utiliza para tipos covariantes: se pueden devolver, pero no recibir como argumento de entrada.inse utiliza para tipos contravariantes: se pueden recibir como argumento, pero no retornarlos.
interface BaseUseCase<in I, out O> {
fun execute(input: I): O
}
Sobrecarga de operadores
Puedes sobrecargar operadores utilizando la palabra reservada operator. Así, por ejemplo, se puede sobrecargar el operador invoke para definir una llamada directa a la instancia y que sea el método por defecto.
interface BaseUseCase<in I, out O> {
operator fun invoke(input: I): O
}
Scope functions
Todos los métodos pueden usar let, also y run, que forman parte de las denominadas scope functions, para encadenar operaciones y evitar estructuras de control explícitas. Un caso muy práctico es cuando existen propiedades opcionales.
customerRepository
.findByCode(customers[0].code)
?.let {
customerRepository.delete(it)
}
Uso de rangos
El concepto de range permite definir rangos de valores de manera simple, tanto numéricos como de caracteres:
for (i in 1..3) { }
for (i in 'a'..'z') { }
Pattern matching
En Kotlin existe el concepto de pattern matching mediante when, como sustituto del clásico switch.
when (x) {
1 -> print("value is 1")
2 -> print("value is 2")
else -> print("value is not 1 or 2")
}
Además, se pueden aplicar rangos dentro de la propia estructura:
val extractResult = when (id) {
in 1..2 -> foo()
3, 4, 5, 6, 30 -> bar()
in 23..24 -> otherStuff()
else -> differentStuff()
}
Sealed classes
Las sealed classes o clases selladas son una extensión del concepto de clases jerárquicas. Permiten restringir las posibles subclases a un conjunto definido en tiempo de compilación, algo muy útil cuando el dominio tiene un conjunto conocido de variantes.
sealed class Subscription {
data class Monthly(): Subscription()
data class Yearly(): Subscription()
data class Quarterly(): Subscription()
}
Combinadas con when, nos ayudan a detectar casos de negocio no implementados.
TODO para marcar operaciones no implementadas
Existe la palabra reservada TODO, que en realidad es una función que devuelve Nothing y lanza una excepción, permitiendo marcar lugares del código que aún no están listos.
fun TODO(): Nothing = throw NotImplementedError()
Contratos y smart cast
Kotlin tiene la característica experimental de contract, que aporta información adicional al compilador sobre el flujo del programa y el manejo de tipos. Por ejemplo, cuando comprobamos que algo no es nulo, el compilador puede aplicar un smart cast a un tipo no anulable.
Type aliases
Se pueden definir typealias para proporcionar nombres alternativos o simplificados a tipos complejos, haciéndolos más legibles y adaptables.
class Irrelevant {
class Foo
}
typealias SomethingRandom = Irrelevant.Foo
Delegación de propiedades
Existe un sistema de delegación de propiedades que permite delegar la lógica de acceso y modificación de las propiedades a otras clases o funciones. Algunos delegados comunes son:
map, para asociar claves de un mapa con propiedades de una clase.lazy, para inicializar la propiedad solo cuando se accede por primera vez.observable, para ejecutar código cada vez que cambia el valor de la propiedad.
class User {
var name: String by observable("<no name>") { _, old, new ->
println("$old -> $new")
}
}
Con todo lo visto hasta aquí, Kotlin ofrece múltiples herramientas para escribir código eficiente y expresivo.
¿Conoces alguna otra característica que deberíamos destacar?