Por Adrián Ferrera González y Miguel Ángel Pérez García
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.
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 es útil cuando la inicialización inmediata no es posible.
Puedes verificar 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) {
}
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`() {
// ...
}
Se pueden escribir Strings sin procesar (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
"""
Existe un tipo de objeto denominado data-class
para almacenar datos, que 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
)
val booking = BookingCustomData("ABC123", "Spain", "Pending", "General") println(booking) // Output: BookingCustomData(reference=ABC123, origin=Spain, status=Pending, cargoType=General)
No existen los métodos estáticos, en su lugar existen los denominados companion objects
, que funcionan como objetos singleton asociados a la clase, limitados a contener dichos miembros estáticos:
class Irrelevant {
companion object {
const val USERNAME1 = "USERNAME1"
fun addTenTo(val number: Int): Int {
return number + 10
}
}
}
/*...*/
val aUserName = Irrelevant.USERNAME1 // aUserName = "USERNAME1"
val newNumber = Irrelevant.addTenTo(10) // newNumber = 20
Por defecto, las clases están declaradas como final y no se pueden sobrescribir los métodos en clases derivadas. Es por ello, que para permitir la herencia, es a través de la palabra clave open
. Se puede limitar la sobrescritura de métodos por clases nietas, aplicando en las clases hijas la palabra final
:
open class Parent() {
open fun foo (): String {
return "Hello from Parent"
}
}
class Child: Parent() {
final override fun foo (): String {
return "Hello from Child"
}
}
class GrandChild: Child() {
override fun foo (): String { // Error: 'foo' in 'Child' is final and cannot be overridden
return "Hello from GrandChild"
}
}
Tanto las interfaces como los companion objects pueden tener propiedades de extensión. Estas funciones extendidas pueden añadirse desde fuera de la clase original, sin necesidad de modificar su código:
// Interface
val <T> List<T>.lastIndex: Int
get() = size - 1
// Companion
fun Irrelevant.Companion.print() {
println("Irrelevant")
}
En un objeto basado en templates
, Kotlin permite restringir cómo se pueden usar los tipos genéricos en clases o interfaces, utilizando las anotaciones in
y out
:
out
se utiliza para tipos covariantes. De esta forma indicamos que se pueden devolver pero no recibir como argumento de entrada.in
, en cambio, se utiliza para contravariantes. Esto significa que se pueden recibir como argumento, pero no retornarlo.interface BaseUseCase<in I, out O> {
fun execute(input: I): O
}
De la misma forma, puedes sobrecargar operadores utilizando la palabra reservada operator
. Así, se puede por ejemplo, sobrecargar el operador invoke
para así definir una llamada directa a la instancia y que sea el método por defecto, y por tanto, la instancia de la clase actúe como una función:
interface BaseUseCase<in I, out O> {
operator fun invoke(input: I): O
}
class MyUseCase: BaseUseCase {
override fun invoke(input: String): String {
return "Say: $input"
}
}
fun main() {
val useCase = MyUseCase()
println(useCase("Hello World"))
}
Todos los métodos pueden usar el operador let
, also
y run
, los cuales forman parte de las denominadas scope functions, para encadenar operaciones (programación funcional) y evitar estructuras de control explícitas. Un ejemplo muy bueno es cuando existen propiedades opcionales.
En el siguiente caso, el valor devuelto por findByCode
solo se pasará a delete (como el parámetro it
), si este no es nulo:
customerRepository
.findByCode(customers[0].code)
?.let {
customerRepository.delete(it)
}
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') { }
En Kotlin existe el concepto de Pattern Matching
. Es un sustituto del clásico switch
, el cual es especialmente útil ya que:
break
explícito para detener la ejecución, ya que sólo se ejecuta la rama correspondiente:when (x) {
1 -> print("value is 1")
2 -> print("value is 2")
else -> print("value is not 1 or 2")
}
val extractResult = when (id) {
in 1..2 -> foo()
3, 4, 5, 6, 30 -> bar()
in 23..24 -> otherStuff()
else -> differentStuff()
}
Las sealed classes o clases selladas, son una extensión del concepto de clases jerárquicas, que, a diferencia de las clases convencionales, permiten restringir las posibles subclases a un conjunto definido en tiempo de compilación. Esto es útil por ejemplo, en aquellas situaciones donde tienes un conjunto definido de funcionalidades que puedan ser representadas.
Combinando esto con el pattern matching explicado en el punto anterior, tenemos una herramienta poderosa que nos avisará en el momento en el que no hayamos implementado, por ejemplo, una lógica de negocio:
sealed class Subscription {
data class Monthly(): Subscription()
data class Yearly(): Subscription()
data class Quarterly(): Subscription()
}
fun processSubscription(subscription: Subscription) {
when (subscription) {
is Subscription.Monthly -> processMonthlySubscription()
is Subscription.Yearly -> processYearlySubscription()
} // Error: Missing Quarterly case
}
```kotlin
## TODO para marcar operaciones no implementadas
Existe la palabra reservada `TODO`, que en sí, es una función que devuelve `Nothing` y lanza una excepción, permitiendo así marcar lugares en el código que aún no están listos, y facilitando compilar y ejecutar la aplicación sin problemas:
```kotlin
fun TODO(): Nothing = throw NotImplementedError()
Tenemos el denominado contract
, una característica experimental y avanzada del lenguaje, los cuales aportan información adicional al compilador sobre el flujo del programa y el manejo de tipos. Con esto nos referimos a aquellas partes que la persona que desarrolla el código conoce, pero el compilador es incapaz de inferir.
Por ejemplo: cuando hacemos una comprobación de que algo no es nulo, por contrato el compilador sabe que puede hacer un smart-cast
al tipo Not-nullable
.
public inline fun CharSequence?.osNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}
}
fun example(text: CharSequence?) {
if (!text.isNullOrEmpty()) {
// Ahora el compilador sabe que `text` no es nulo
println(text.length) // Sin error: smart cast a CharSequence no nullable
}
}
Se pueden definir type aliases typealias
para proporcionar nombres alternativos o simplificados, para tipos complejos, haciéndolos más legibles y/o adaptables en el código:
class Irrelevant {
class Foo
}
typealias SomethingRandom = Irrelevant.Foo
val fooInstance: SomethingRandom = Irrelevant.Foo()
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. Tenemos los siguientes delegados:
map
, podemos utilizar un mapa en el constructor de una clase para definir sus propiedades. De esta forma, cada valor del map se asociará a una propiedad definida en la clase, basándose en el nombre de la clave y el nombre propiedad:class User (val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
lazy
nos permite inicializar la propiedad sólo cuando ésta sea accedida por primera vez. Ideal para mejorar el rendimiento en aquellas situaciones donde la inicialización de un objeto es costosa :fun foo() {
val irrelevant: Irrelevant by lazy { Irrelevant() }
}
observable
, nos permite ejecutar un código cada vez que el valor de la propiedad cambie. Útil para monitorear cambios en propiedades y ejecutar alguna lógica, como actualizaciones en UI o validaciones:class User {
var name: String by observable("<no name>") {
prop, old, new ->
println("$old -> $new")
}
}
fun main() {
val user = User()
user.name = "first"
user.name = "second"
}
// <no name> -> first
// first -> second
Con lo que hemos visto hasta aquí, podemos decir que Kotlin ofrece múltiples herramientas para escribir código eficiente y expresivo.
¿Conoces alguna otra característica que deberíamos destacar?
¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.
Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?
Pero espera 🖐 que tenemos un conflicto interno. A nosotros las newsletter nos parecen 💩👎👹 Por eso hemos creado la LEAN LISTA, la primera lista zen, disfrutona y que suena a rock y reggaeton del sector de la programación. Todos hemos recibido newsletters por encima de nuestras posibilidades 😅 por eso este es el compromiso de la Lean Lista