
Las extensión functions y las extensión properties permiten ampliar el comportamiento de cualquier tipo sin heredarlo ni modificar su código fuente. Constituyen uno de los mecanismos que hacen a Kotlin especialmente expresivo al interactuar con bibliotecas existentes, incluidas las escritas en Java con las que el lenguaje es plenamente interoperable.
La idea central es sencilla. Si una clase no expone un método útil para tu caso, puedes declararlo fuera y llamarlo como si perteneciera al tipo. El compilador resuelve la llamada en tiempo de compilación y genera una invocación estática con el receptor como primer argumento.
Funciones de extensión
Una función de extensión se declara indicando el tipo receptor seguido de un punto y el nombre de la función. Dentro del cuerpo, la palabra clave this hace referencia a la instancia sobre la que se invoca la función.
fun String.ocultarMedio(): String {
if (length < 4) return this
val visibles = 2
val ocultos = "*".repeat(length - visibles * 2)
return take(visibles) + ocultos + takeLast(visibles)
}
fun main() {
println("alan@certidevs.com".ocultarMedio())
println("1234567890".ocultarMedio())
}
La función ocultarMedio se comporta como si perteneciese a String, aunque no forma parte de la clase original. Dentro del cuerpo, length, take y takeLast se aplican al receptor implícito.
Las funciones de extensión no modifican la clase original ni su bytecode. Se traducen a funciones estáticas que toman el receptor como primer argumento.
Este comportamiento implica que no existe polimorfismo dinámico sobre el receptor. Si defines la extensión sobre un tipo base y la invocas sobre una variable declarada con ese tipo, se usara la extensión del tipo declarado aunque el objeto en tiempo de ejecución sea de una subclase.
open class Figura
class Circulo : Figura()
fun Figura.descripcion(): String = "figura generica"
fun Circulo.descripcion(): String = "circulo concreto"
fun main() {
val figura: Figura = Circulo()
println(figura.descripcion())
}
La salida imprime "figura genérica" porque la resolución se realiza sobre el tipo estático de la variable. Este detalle es clave cuando se disena una API: la herencia sigue rigiendo en los miembros declarados dentro de la clase, mientras que las extensiones operan como funciones con azucar sintáctico.
Propiedades de extensión
Además de funciones, Kotlin admite propiedades de extensión. Como las clases externas no pueden almacenar estado nuevo, estas propiedades deben definirse con un getter explícito y, opcionalmente, un setter.
val String.esCorreoValido: Boolean
get() = matches(Regex("^[\\w.+-]+@[\\w-]+\\.[\\w.-]+$"))
val List<Int>.mediana: Double
get() {
val ordenada = sorted()
val medio = size / 2
return if (size % 2 == 0) {
(ordenada[medio - 1] + ordenada[medio]) / 2.0
} else {
ordenada[medio].toDouble()
}
}
fun main() {
println("hola@kotlin.es".esCorreoValido)
println(listOf(3, 1, 4, 1, 5, 9, 2, 6).mediana)
}
Las propiedades de extensión suelen usarse para exponer información derivada sin necesidad de crear funciones obtenerAlgo(). Se leen igual que las propiedades de la clase original, lo que produce un código mas fluido.
Extensiones sobre tipos nullable
Una caracteristica útil es poder declarar extensiones sobre un tipo con marca de nulabilidad. Dentro del cuerpo se puede comprobar this == null y evitar excepciones incluso cuando el receptor no esta presente.
fun String?.oVacia(): String = this ?: ""
fun String?.esBlancoOLong(): Boolean = this == null || isBlank()
fun main() {
val nombre: String? = null
println(nombre.oVacia().length)
println(nombre.esBlancoOLong())
}
Esta técnica es habitual en bibliotecas de utilidades. El operador ?. sigue disponible en el receptor, pero la extensión se puede invocar incluso sobre valores nulos sin romper la cadena.
Como se resuelven en el compilador
Cuando el compilador encuentra objeto.funcion() busca primero un miembro real con esa firma; si no lo halla, busca una extensión visible en el scope; si tampoco existe, falla la compilación. Este orden importa porque cualquier nueva extensión queda relegada cuando ya hay un miembro con el mismo nombre.
Este orden de busqueda tiene una consecuencia importante. Si mas tarde se anade un miembro a la clase con la misma firma que tu extensión, la llamada pasara a resolverse al miembro, no a la extensión. En un proyecto en evolución conviene ser cauteloso con los nombres para no ocultar futuros métodos oficiales.
Extensiones genéricas
Las funciones de extensión soportan parámetros de tipo, lo que permite crear utilidades que funcionan para cualquier colección o clase genérica.
fun <T> List<T>.segundo(): T {
require(size >= 2) { "La lista necesita al menos dos elementos" }
return this[1]
}
fun <K, V> Map<K, V>.invertir(): Map<V, K> = entries.associate { (k, v) -> v to k }
fun main() {
val numeros = listOf(10, 20, 30, 40)
println(numeros.segundo())
val paises = mapOf("ES" to "Espana", "FR" to "Francia")
println(paises.invertir())
}
Combinar generics con extensiones permite escribir APIs que se leen como código nativo del tipo. En el día a día es el patrón que emplea la biblioteca estandar en kotlin.collections o kotlin.text.
Scope y organización del código
Las extensiones viven en el paquete donde se declaren. Para usarlas hay que importarlas de forma explícita igual que cualquier otro símbolo. Esta necesidad de import evita colisiones y hace visible la procedencia del método.
package com.certidevs.utils
fun String.ponerEnMayusculas(): String = uppercase()
package com.certidevs.app
import com.certidevs.utils.ponerEnMayusculas
fun main() {
println("hola".ponerEnMayusculas())
}
Agrupa las extensiones en ficheros por tema ("StringExtensions.kt", "CollectionExtensions.kt"). Mantener pocas extensiones públicas por paquete facilita su descubrimiento.
Dentro de una clase se pueden definir extensiones con doble receptor. El receptor explícito sigue siendo el tipo declarado, pero el receptor implícito es la propia clase contenedora. Esta técnica es útil en DSLs y en clases de utilidad que envuelven lógica compleja.
class FormateadorFactura(private val simbolo: String) {
fun Double.conMoneda(): String = "%.2f %s".format(this, simbolo)
fun imprimirTotal(total: Double) {
println("Total: ${total.conMoneda()}")
}
}
fun main() {
FormateadorFactura("EUR").imprimirTotal(1250.5)
}
La extensión conMoneda solo esta disponible dentro del cuerpo de FormateadorFactura y puede acceder a su estado. Fuera de la clase, Double no tiene esa función. Este patrón encapsula el azucar sintáctico en el contexto donde tiene sentido.
Extensiones combinadas con scope functions
Las extensiones componen muy bien con las scope functions (let, run, with, apply, also). La combinación permite encadenar transformaciones, introducir contexto temporal y ejecutar efectos colaterales sin perder la fluidez del receptor.
La extensión aporta el nombre de dominio y la scope function organiza el bloque de trabajo. Este patrón aparece en conversiones DTO a dominio, en construcción de peticiones HTTP con Ktor y en la configuración de clientes en builders.
Buenas practicas al escribir extensiones
Crear extensiones a diestro y siniestro puede volver difícil de navegar un proyecto. Estos criterios ayudan a mantener un equilibrio sano.
- 1. Prefiere extensiones para utilidades sobre tipos ajenos: tipos de la biblioteca estandar, clases de Java o DTOs generados. Para tu propio dominio suele ser mejor anadir miembros reales.
- 2. No dupliques nombres con la API original: si el método ya existe, la extensión quedara oculta.
- 3. Mantenlas puras cuando sea posible: una extensión que hace IO o modifica globales complica el razonamiento. Si realmente la necesitas, nombrala con un verbo claro.
- 4. Considera la visibilidad
internal: para utilidades de proyecto que no quieres exponer como API pública. - 5. Evita extensiones sobre
Any: pueden capturar cualquier expresión y producir código difícil de leer.
Ejemplo práctico aplicado a negocio
Un escenario frecuente es transformar DTOs que llegan del backend en modelos de dominio. Las extensiones convierten esta tarea en un oneliner limpio.
data class UsuarioDto(val id: String, val nombre: String, val email: String?)
data class Usuario(val id: String, val nombre: String, val email: String)
fun UsuarioDto.aDominio(): Usuario? {
val correo = email?.takeIf { it.isNotBlank() } ?: return null
return Usuario(id = id, nombre = nombre.trim(), email = correo)
}
fun List<UsuarioDto>.soloValidos(): List<Usuario> = mapNotNull { it.aDominio() }
fun main() {
val dtos = listOf(
UsuarioDto("1", "Ana ", "ana@kotlin.es"),
UsuarioDto("2", "Luis", null),
UsuarioDto("3", " Maria ", "maria@kotlin.es")
)
println(dtos.soloValidos())
}
La extensión aDominio encapsula la lógica de validación y conversión. soloValidos opera sobre la lista entera y aprovecha mapNotNull para descartar los no convertibles. El código cliente se lee como prosa técnica.
En IntelliJ IDEA el autocompletado sugiere las extensiones visibles en el momento de escribir, lo que las convierte en elementos descubribles sin necesidad de consultar documentación externa. Esta caracteristica es la que permite que la biblioteca kotlin.collections se lea como una API de primera clase aunque este construida casi por completo a base de extensiones.
Extensiones con generics y restricciones
Cuando una extensión se escribe sobre un tipo genérico, se pueden aplicar restricciones con where o con los upper bounds habituales. Esto permite escribir utilidades seguras que solo tienen sentido para subtipos concretos.
fun <T : Comparable<T>> List<T>.entre(min: T, max: T): List<T> =
filter { it in min..max }
fun <T> List<T>.agruparPar(): List<Pair<T, T>> {
require(size % 2 == 0) { "La lista debe tener un numero par de elementos" }
return (indices step 2).map { i -> this[i] to this[i + 1] }
}
fun main() {
val numeros = listOf(1, 5, 10, 15, 20, 25)
println(numeros.entre(5, 15))
val nombres = listOf("Ana", "Luis", "Maria", "Javier")
println(nombres.agruparPar())
}
Al exigir T : Comparable<T>, la función entre solo esta disponible para tipos que pueden compararse. Para tipos que no cumplen la restricción, el compilador rechaza la llamada con un mensaje claro.
Extensiones con @JvmName para interoperabilidad con Java
Kotlin es interoperable con Java en ambos sentidos. Desde Java, una extensión aparece como un método estático en la clase que la contiene. Para darle un nombre concreto se usa @JvmName, útil cuando la firma genérica genera nombres duplicados en la JVM.
@file:JvmName("StringUtils")
package com.certidevs.utils
fun String.ocultarMedio(): String {
if (length < 4) return this
val visibles = 2
val ocultos = "*".repeat(length - visibles * 2)
return take(visibles) + ocultos + takeLast(visibles)
}
Desde Java, la llamada sería StringUtils.ocultarMedio("texto"). Sin la anotación, el nombre del fichero original sería el prefijo. Esta combinación permite que el mismo código se consuma de forma idiomatica en ambos lenguajes.
Kotlin es interoperable con Java, pero el objetivo de este curso es dominar Kotlin moderno. La anotación
@JvmNamees una herramienta puntual que aparece solo cuando hay consumo desde Java.
Extensiones sobre funciones y lambdas
Se pueden declarar extensiones sobre tipos función, lo que habilita utilidades como aplicar un timeout, registrar el tiempo de ejecución o memoizar resultados.
fun <T, R> ((T) -> R).registrar(nombre: String): (T) -> R = { entrada ->
val inicio = System.nanoTime()
val resultado = this(entrada)
val tiempoMs = (System.nanoTime() - inicio) / 1_000_000
println("[$nombre] ${tiempoMs} ms -> $resultado")
resultado
}
fun main() {
val duplicar: (Int) -> Int = { it * 2 }
val duplicarConLog = duplicar.registrar("duplicar")
println(duplicarConLog(21))
}
Este patrón es útil para aspectos transversales como trazabilidad, reintentos o metricas, sin tener que recurrir a AOP ni a decoradores externos. El resultado sigue siendo una función con la misma firma, por lo que encaja en cualquier lugar donde se esperara la original.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Kotlin es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Kotlin
Explora más contenido relacionado con Kotlin y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Dominar las funciones y propiedades de extensión para ampliar tipos existentes sin herencia, comprender su resolución estática y aplicarlas en diseno de APIs idiomaticas.