Este codelab es parte del curso Capacitación de Kotlin para programadores. Aprovecharás al máximo este curso si trabajas con los codelabs en secuencia. Según tus conocimientos, es posible que puedas cambiar algunas secciones. Este curso está dirigido a los programadores que conocen un lenguaje orientado a objetos y desean aprender Kotlin.
Introducción
En este codelab, conocerás clases, funciones y métodos genéricos, así como la forma en que funcionan en Kotlin.
En lugar de compilar una sola app de muestra, las lecciones de este curso están diseñadas para aumentar tu conocimiento, pero son semiindependientes entre sí a fin de que puedas leer las secciones con las que estás familiarizado. Para vincularlos, muchos de los ejemplos usan un tema de acuario. Si deseas ver la historia completa del acuario, consulta el curso de Udacity Capacitación de Kotlin para programadores.
Conocimientos que ya deberías tener
- La sintaxis de las funciones, las clases y los métodos de Kotlin
- Cómo crear una clase nueva en IntelliJ IDEA y cómo ejecutar un programa
Qué aprenderás
- Cómo trabajar con funciones, métodos y clases genéricas
Actividades
- Crea una clase genérica y agrega restricciones
- Crear tipos
in
yout
- Crea funciones, métodos y funciones genéricas de extensión
Introducción a los genéricos
Kotlin, al igual que muchos lenguajes de programación, tiene tipos genéricos. Un tipo genérico te permite hacer que una clase sea genérica y, por lo tanto, una clase mucho más flexible.
Imagina que estás implementando una clase MyList
que contiene una lista de elementos. Sin los elementos genéricos, debes implementar una versión nueva de MyList
para cada tipo: una para Double
, otra para String
y otra para Fish
. Con ella, puede hacer que la lista sea genérica para que contenga cualquier tipo de objeto. Es como hacer que el tipo sea un comodín que se adapte a muchos tipos.
Para definir un tipo genérico, coloca T entre corchetes angulares <T>
después del nombre de la clase. (podría usar otra letra o un nombre más largo, pero la convención para un tipo genérico es T).
class MyList<T> {
fun get(pos: Int): T {
TODO("implement")
}
fun addItem(item: T) {}
}
Puedes hacer referencia a T
como si fuera un tipo normal. El tipo de datos que se muestra para get()
es T
y el parámetro para addItem()
es de tipo T
. Por supuesto, las listas genéricas son muy útiles, por lo que la clase List
está integrada en Kotlin.
Paso 1: Crea una jerarquía de tipos
En este paso, crearás algunas clases para usar en el paso siguiente. En una codelab anterior, se trataron las subclases, pero aquí te ofrecemos una breve revisión.
- Para mantener el ejemplo ordenado, crea un paquete nuevo en src y llámalo
generics
. - En el paquete generics, crea un archivo
Aquarium.kt
nuevo. De esta manera, puedes redefinir las cosas con los mismos nombres sin conflictos, por lo que el resto del código de este codelab se incluye en este archivo. - Haz una jerarquía de tipos de tipos de suministro de agua. Comienza por hacer que
WaterSupply
sea una claseopen
, por lo que puede ser una subclase. - Agrega un parámetro booleano
var
,needsProcessing
. Esto crea automáticamente una propiedad mutable, junto con un método get y un método set. - Crea una subclase
TapWater
que extiendaWaterSupply
, y pasatrue
paraneedsProcessing
, ya que el agua del grifo contiene aditivos que son malos para los peces. - En
TapWater
, define una función llamadaaddChemicalCleaners()
que establezcaneedsProcessing
enfalse
después de limpiar el agua. La propiedadneedsProcessing
se puede establecer desdeTapWater
, ya que espublic
de forma predeterminada y accesible para las subclases. Este es el código completo.
package generics
open class WaterSupply(var needsProcessing: Boolean)
class TapWater : WaterSupply(true) {
fun addChemicalCleaners() {
needsProcessing = false
}
}
- Crea dos subclases más de
WaterSupply
, llamadasFishStoreWater
yLakeWater
.FishStoreWater
no necesita procesarse, peroLakeWater
debe filtrarse con el métodofilter()
. Después del filtrado, no es necesario volver a procesarlo, por lo que enfilter()
debes configurarneedsProcessing = false
.
class FishStoreWater : WaterSupply(false)
class LakeWater : WaterSupply(true) {
fun filter() {
needsProcessing = false
}
}
Si necesitas más información, consulta la lección anterior sobre herencia en Kotlin.
Paso 2: Crea una clase genérica
En este paso, modificarás la clase Aquarium
para admitir diferentes tipos de suministros de agua.
- En Aquarium.kt, define una clase
Aquarium
, con<T>
entre corchetes después del nombre de la clase. - Agrega una propiedad inmutable
waterSupply
de tipoT
aAquarium
.
class Aquarium<T>(val waterSupply: T)
- Escribe una función llamada
genericsExample()
. Esto no forma parte de una clase, por lo que puede ir al nivel superior del archivo, como la funciónmain()
o las definiciones de clase. En la función, crea unAquarium
y pásale unaWaterSupply
. Dado que el parámetrowaterSupply
es genérico, debes especificar el tipo entre corchetes angulares<>
.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
}
- En
genericsExample()
, tu código puede acceder alwaterSupply
de tu acuario. Como es de tipoTapWater
, puedes llamar aaddChemicalCleaners()
sin conversiones de tipo.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
- Cuando creas el objeto
Aquarium
, puedes quitar los corchetes angulares y lo que se encuentra entre estos porque Kotlin tiene inferencia de tipo. Por lo tanto, no hay motivo para decirTapWater
dos veces cuando creas la instancia. El argumento se puede deducir mediante el argumento deAquarium
; de todos modos, creará unAquarium
de tipoTapWater
.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
- Para ver qué está sucediendo, imprime
needsProcessing
antes y después de llamar aaddChemicalCleaners()
. A continuación, se muestra la función completada.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
aquarium.waterSupply.addChemicalCleaners()
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
}
- Agrega una función
main()
para llamar agenericsExample()
y, luego, ejecuta tu programa y observa el resultado.
fun main() {
genericsExample()
}
⇒ water needs processing: true water needs processing: false
Paso 3: Sé más específico
"Genérico" significa que puedes pasar casi todo lo que sucede, y a veces eso es un problema. En este paso, harás que la clase Aquarium
sea más específica sobre lo que puedes poner en ella.
- En
genericsExample()
, crea unAquarium
y pasa una string parawaterSupply
y, luego, imprime la propiedadwaterSupply
del acuario.
fun genericsExample() {
val aquarium2 = Aquarium("string")
println(aquarium2.waterSupply)
}
- Ejecuta el programa para observar el resultado.
⇒ string
El resultado es la string que pasaste, ya que Aquarium
no coloca ninguna limitación en T.
Cualquier tipo, incluido String
, se puede pasar.
- En
genericsExample()
, crea otroAquarium
y pasanull
parawaterSupply
. SiwaterSupply
es nulo, imprime"waterSupply is null"
.
fun genericsExample() {
val aquarium3 = Aquarium(null)
if (aquarium3.waterSupply == null) {
println("waterSupply is null")
}
}
- Ejecuta el programa y observa el resultado.
⇒ waterSupply is null
¿Por qué puedes pasar null
cuando creas un Aquarium
? Esto es posible porque, de forma predeterminada, T
representa el tipo anulable Any?
, el tipo en la parte superior de la jerarquía de tipos. El siguiente es equivalente a lo que escribiste antes.
class Aquarium<T: Any?>(val waterSupply: T)
- Para no permitir el paso de
null
, quita el elemento?
después deAny
y haz queT
de tipoAny
sea explícito.
class Aquarium<T: Any>(val waterSupply: T)
En este contexto, Any
se denomina restricción genérica. Significa que se puede pasar cualquier tipo para T
, siempre que no sea null
.
- Lo que realmente quieres es asegurarte de que solo se pueda pasar un
WaterSupply
(o una de sus subclases) paraT
. ReemplazaAny
porWaterSupply
para definir una restricción genérica más específica.
class Aquarium<T: WaterSupply>(val waterSupply: T)
Paso 4: Agrega más verificaciones
En este paso, aprenderás sobre la función check()
para asegurarte de que tu código se comporte como se espera. La función check()
es una función de biblioteca estándar en Kotlin. Funciona como una aserción y arrojará una IllegalStateException
si su argumento se evalúa como false
.
- Agrega un método
addWater()
a la claseAquarium
a fin de agregar agua, con uncheck()
que garantiza que no se necesite procesar el agua primero.
class Aquarium<T: WaterSupply>(val waterSupply: T) {
fun addWater() {
check(!waterSupply.needsProcessing) { "water supply needs processing first" }
println("adding water from $waterSupply")
}
}
En este caso, si needsProcessing
es verdadero, check()
arrojará una excepción.
- En
genericsExample()
, agrega código para crearAquarium
conLakeWater
y, luego, agrégale agua.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.addWater()
}
- Ejecuta el programa. Se mostrará una excepción, ya que primero debes filtrar el agua.
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)
- Agrega una llamada para filtrar el agua antes de agregarla a
Aquarium
. Cuando ejecutas el programa, no se producen excepciones.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.waterSupply.filter()
aquarium4.addWater()
}
⇒ adding water from generics.LakeWater@880ec60
Lo anterior cubre los conceptos básicos de los elementos genéricos. Las siguientes tareas cubren más, pero el concepto importante es cómo declarar y usar una clase genérica con una restricción genérica.
En esta tarea, aprenderá sobre los tipos de entrada y salida con genéricos. Un tipo in
es un tipo que solo se puede pasar a una clase, no se muestra. Un tipo out
es un tipo que solo se puede mostrar desde una clase.
Si observas la clase Aquarium
, verás que el tipo genérico solo se muestra cuando se obtiene la propiedad waterSupply
. No hay ningún método que tome un valor de tipo T
como parámetro (excepto por definirlo en el constructor). Kotlin te permite definir tipos out
para este caso con exactitud, y también puede inferir información adicional sobre los lugares en los que los tipos son seguros. De manera similar, puedes definir tipos in
para tipos genéricos que solo se pasan a métodos, no se muestran. Esto permite que Kotlin realice verificaciones adicionales para comprobar la seguridad del código.
Los tipos in
y out
son directivas para el sistema de tipos de Kotlin. La explicación de todo el sistema de tipos está fuera del alcance de este bootcamp (está bastante relacionado). Sin embargo, el compilador marcará los tipos que no estén marcados correctamente in
y out
, por lo que debes conocerlos.
Paso 1: Define un tipo de salida
- En la clase
Aquarium
, cambiaT: WaterSupply
para que sea un tipoout
.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
...
}
- En el mismo archivo, fuera de la clase, declara una función
addItemTo()
que espere unaAquarium
deWaterSupply
.
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")
- Llama a
addItemTo()
desdegenericsExample()
y ejecuta tu programa.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
addItemTo(aquarium)
}
⇒ item added
Kotlin puede asegurarse de que addItemTo()
no haga nada de tipo no seguro con el WaterSupply
genérico, ya que se declara como un tipo out
.
- Si quitas la palabra clave
out
, el compilador mostrará un error cuando se llame aaddItemTo()
, ya que Kotlin no puede garantizar que no hagas nada inseguro con el tipo.
Paso 2: Define un tipo en
El tipo in
es similar al tipo out
, pero para los tipos genéricos que solo se pasan a funciones, no se muestran. Si intentas mostrar un tipo in
, se mostrará un error de compilador. En este ejemplo, definirás un tipo in
como parte de una interfaz.
- En Aquarium.kt, define una interfaz
Cleaner
que tome un elementoT
genérico que esté restringido aWaterSupply
. Como solo se usa como argumento declean()
, puedes convertirlo en un parámetroin
.
interface Cleaner<in T: WaterSupply> {
fun clean(waterSupply: T)
}
- Si deseas usar la interfaz
Cleaner
, agrega productos químicos para crear una claseTapWaterCleaner
que implementeCleaner
a fin de limpiarTapWater
.
class TapWaterCleaner : Cleaner<TapWater> {
override fun clean(waterSupply: TapWater) = waterSupply.addChemicalCleaners()
}
- En la clase
Aquarium
, actualizaaddWater()
para tomar unCleaner
de tipoT
y limpia el agua antes de agregarlo.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
fun addWater(cleaner: Cleaner<T>) {
if (waterSupply.needsProcessing) {
cleaner.clean(waterSupply)
}
println("water added")
}
}
- Actualiza el código de ejemplo de
genericsExample()
para crear unTapWaterCleaner
, unAquarium
conTapWater
y, luego, agrega agua con el limpiador. Usará la herramienta de limpieza según sea necesario.
fun genericsExample() {
val cleaner = TapWaterCleaner()
val aquarium = Aquarium(TapWater())
aquarium.addWater(cleaner)
}
Kotlin usará la información de tipo in
y out
para asegurarse de que tu código use los genéricos de forma segura. Out
y in
son fáciles de recordar: los tipos out
se pueden pasar hacia afuera como valores de retorno, los tipos in
se pueden pasar hacia adentro como argumentos.
Si deseas profundizar en el tipo de problemas que tienen los tipos y cómo se resuelven los tipos, la documentación los trata en profundidad.
En esta tarea, aprenderá sobre las funciones genéricas y cuándo usarlas. Por lo general, una función genérica es recomendable cuando la función toma un argumento de una clase que tiene un tipo genérico.
Paso 1: Crea una función genérica
- En generics/Aquarium.kt, crea una función
isWaterClean()
que tome un elementoAquarium
. Debes especificar el tipo genérico del parámetro; una opción es usarWaterSupply
.
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
println("aquarium water is clean: ${aquarium.waterSupply.needsProcessing}")
}
Sin embargo, esto significa que Aquarium
debe tener un parámetro de tipo out
para que se lo llame. A veces, out
o in
son demasiado restrictivos porque debes usar un tipo tanto para la entrada como para la salida. Puedes quitar el requisito out
si haces que la función sea genérica.
- Para que la función sea genérica, coloca corchetes angulares después de la palabra clave
fun
con un tipo genéricoT
y cualquier restricción, en este caso,WaterSupply
. CambiaAquarium
para que esté restringido porT
en lugar de porWaterSupply
.
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}
T
es un parámetro de tipo para isWaterClean()
que se usa a fin de especificar el tipo genérico del acuario. Este patrón es muy común, y te recomendamos tomarte un momento para solucionarlo.
- Llama a la función
isWaterClean()
especificando el tipo entre corchetes angulares justo después del nombre de la función y antes de los paréntesis.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean<TapWater>(aquarium)
}
- Debido a la inferencia de tipo del argumento
aquarium
, no es necesario, por lo que debes quitarlo. Ejecuta el programa y observa el resultado.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean(aquarium)
}
⇒ aquarium water is clean: false
Paso 2: Crea un método genérico con un tipo reificado
También puedes usar funciones genéricas para los métodos, incluso en clases que tengan su propio tipo genérico. En este paso, agregarás un método genérico a Aquarium
, que verifica si tiene un tipo de WaterSupply
.
- En la clase
Aquarium
, declara un método quehasWaterSupplyOfType()
tome un parámetro genéricoR
(T
ya está usado) restringido aWaterSupply
y muestratrue
siwaterSupply
es del tipoR
. Esto es como la función que declaraste antes, pero dentro de la claseAquarium
.
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
- Observa que el
R
final está subrayado en rojo. Coloca el cursor sobre él para ver cuál es el error. - Para realizar una verificación de
is
, debes indicarle a Kotlin que el tipo es reificado o real, y que se puede usar en la función. Para ello, escribeinline
delante de la palabra clavefun
yreified
delante del tipo genéricoR
.
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
Una vez que se especifica un tipo, se puede usar como un tipo normal, ya que se trata de un tipo real después del intercalado. Esto significa que puedes realizar verificaciones de is
con el tipo.
Si no usas reified
aquí, el tipo no será lo suficientemente real como para que Kotlin permita las comprobaciones de is
. Eso se debe a que los tipos no reificados solo están disponibles durante el tiempo de compilación y tu programa no puede usarlos. Esto se analiza con más detalle en la siguiente sección.
- Pasa
TapWater
como el tipo. Al igual que con las funciones genéricas, puedes llamar a métodos genéricos usando corchetes angulares con el tipo que aparece después del nombre de la función. Ejecuta el programa y observa el resultado.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>()) // true
}
⇒ true
Paso 3: Crea funciones de extensión
También puedes usar tipos reificados para funciones normales y de extensión.
- Fuera de la clase
Aquarium
, define una función de extensión enWaterSupply
llamadaisOfType()
que verifique si elWaterSupply
pasado es de un tipo específico, por ejemplo,TapWater
.
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T
- Llama a la función de extensión como si fuera un método.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.waterSupply.isOfType<TapWater>())
}
⇒ true
Con estas funciones de extensión, no importa qué tipo de Aquarium
sea (Aquarium
, TowerTank
o alguna otra subclase), siempre que sea un Aquarium
. La sintaxis de star-projection es una forma conveniente de especificar una variedad de coincidencias. Y, cuando uses una proyección en estrella, Kotlin se asegurará de no hacer nada inseguro.
- Para usar una proyección en estrella, coloca
<*>
después deAquarium
. MuevehasWaterSupplyOfType()
para que sea una función de extensión, porque realmente no forma parte de la API principal deAquarium
.
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R
- Cambia la llamada a
hasWaterSupplyOfType()
y ejecuta tu programa.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>())
}
⇒ true
En el ejemplo anterior, tuviste que marcar el tipo genérico como reified
y hacer la función inline
, porque Kotlin necesita conocerlos en el tiempo de ejecución, no solo el tiempo de compilación.
Kotlin solo usa todos los tipos genéricos en el tiempo de compilación. De esta manera, el compilador se asegurará de que lo hagas de forma segura. Durante el tiempo de ejecución, se borran todos los tipos genéricos. Por lo tanto, el mensaje de error anterior sobre verificar un tipo borrado
Resulta que el compilador puede crear código correcto sin conservar los tipos genéricos hasta el tiempo de ejecución. Sin embargo, a veces realizas acciones, como is
, para comprobar tipos genéricos que el compilador no admite. Por eso Kotlin agregó tipos reificados o reales.
Puedes leer más sobre los tipos reificados y el borrado de tipos en la documentación de Kotlin.
Esta lección se enfocó en elementos genéricos, que son importantes para que el código sea más flexible y fácil de reutilizar.
- Crea clases genéricas para que el código sea más flexible.
- Agregue restricciones genéricas para limitar los tipos que se usan con elementos genéricos.
- Usa los tipos
in
yout
con genéricos para proporcionar una mejor verificación de tipos a fin de restringir los tipos que se pasan a las clases o se devuelven de las clases. - Crea funciones y métodos genéricos para trabajar con tipos genéricos. Por ejemplo:
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) { ... }
- Usa funciones de extensión genéricas para agregar una funcionalidad no principal a una clase.
- Los tipos reificados a veces son necesarios debido al borrado de tipos. Los tipos reificados, a diferencia de los genéricos, persisten en el entorno de ejecución.
- Usa la función
check()
para verificar que tu código se ejecute como se espera. Por ejemplo:check(!waterSupply.needsProcessing) { "water supply needs processing first" }
Documentación de Kotlin
Si necesitas más información sobre algún tema de este curso o si no puedes avanzar, https://kotlinlang.org es tu mejor punto de partida.
- Genéricos
- Restricciones genéricas
- Proyecciones en estrella
- Tipos
In
yout
- Parámetros reificados
- Eliminación de tipos
- Función
check()
Instructivos de Kotlin
El sitio web https://try.kotlinlang.org incluye instructivos enriquecidos llamados Kotlin Koans, un intérprete basado en la Web, y un conjunto completo de documentación de referencia con ejemplos.
Curso de Udacity
Para ver el curso de Udacity sobre este tema, consulta el Capacitación de Kotlin para programadores.
IntelliJ IDEA
Puedes encontrar la documentación de IntelliJ IDEA en el sitio web de JetBrains.
En esta sección, se enumeran las posibles tareas para los alumnos que trabajan con este codelab como parte de un curso que dicta un instructor. Depende del instructor hacer lo siguiente:
- Si es necesario, asigna la tarea.
- Informa a los alumnos cómo enviar los deberes.
- Califica las tareas.
Los instructores pueden usar estas sugerencias lo poco o lo que quieran, y deben asignar cualquier otra tarea que consideren apropiada.
Si estás trabajando en este codelab por tu cuenta, usa estas tareas para poner a prueba tus conocimientos.
Responde estas preguntas
Pregunta 1
¿Cuál de las siguientes opciones es una convención para asignar un nombre genérico?
▢ <Gen>
▢ <Generic>
▢ <T>
▢ <X>
Pregunta 2
Una restricción sobre los tipos permitidos para un tipo genérico se denomina:
▢ una restricción genérica
▢ una restricción genérica
▢ desambiguación
▢ un límite de tipos genérico
Pregunta 3
Unificado significa lo siguiente:
▢ Se calculó el impacto real de ejecución de un objeto.
▢ Se estableció un índice de entrada restringido en la clase.
▢ El parámetro de tipo genérico se convirtió en un tipo real.
▢ Se activó un indicador de error remoto.
Continúa con la siguiente lección:
Para obtener una descripción general del curso, incluidos vínculos a otros codelabs, consulta "Capacitación de Kotlin para programadores: Bienvenido al curso."