Este codelab es parte del curso de capacitación de Kotlin para programadores. Aprovecharás al máximo este curso si trabajas con los codelabs de forma secuencial. Según tus conocimientos, es posible que puedas cambiar algunas secciones. Este curso está dirigido a programadores que conocen un lenguaje orientado a objetos y quieren aprender Kotlin.
Introducción
En este codelab, crearás un programa en Kotlin y aprenderás sobre clases y objetos en Kotlin. Gran parte de este contenido te resultará familiar si conoces otro lenguaje orientado a objetos, pero Kotlin tiene algunas diferencias importantes para reducir la cantidad de código que necesitas escribir. También aprenderás sobre las clases abstractas y la delegación de interfaces.
En lugar de crear una sola app de ejemplo, las lecciones de este curso están diseñadas para ampliar tus conocimientos, pero son semiindependientes entre sí para que puedas revisar las secciones con las que ya estás familiarizado. Para unirlos, muchos de los ejemplos usan un tema de acuario. Si quieres ver la historia completa del acuario, consulta el curso de Udacity Kotlin Bootcamp for Programmers.
Conocimientos que ya deberías tener
- Los conceptos básicos de Kotlin, incluidos los tipos, los operadores y los bucles
- Sintaxis de funciones de Kotlin
- Conceptos básicos de la programación orientada a objetos
- Los conceptos básicos de un IDE, como IntelliJ IDEA o Android Studio
Qué aprenderás
- Cómo crear clases y acceder a propiedades en Kotlin
- Cómo crear y usar constructores de clases en Kotlin
- Cómo crear una subclase y cómo funciona la herencia
- Acerca de las clases abstractas, las interfaces y la delegación de interfaces
- Cómo crear y usar clases de datos
- Cómo usar singletons, enumeraciones y clases selladas
Actividades
- Crea una clase con propiedades
- Cómo crear un constructor para una clase
- Crea una subclase
- Examina ejemplos de clases abstractas e interfaces
- Crea una clase de datos simple
- Obtén información sobre los singleton, los enums y las clases selladas
Ya deberías conocer los siguientes términos de programación:
- Las clases son planos para los objetos. Por ejemplo, una clase
Aquarium
es el plano para crear un objeto de acuario. - Los objetos son instancias de clases; un objeto de acuario es un
Aquarium
real. - Las propiedades son características de las clases, como la longitud, el ancho y la altura de un
Aquarium
. - Los métodos, también llamados funciones miembro, son la funcionalidad de la clase. Los métodos son lo que puedes "hacer" con el objeto. Por ejemplo, puedes
fillWithWater()
un objetoAquarium
. - Una interfaz es una especificación que puede implementar una clase. Por ejemplo, la limpieza es común a objetos que no son acuarios, y la limpieza generalmente se realiza de manera similar para diferentes objetos. Por lo tanto, podrías tener una interfaz llamada
Clean
que defina un métodoclean()
. La claseAquarium
podría implementar la interfazClean
para limpiar el acuario con una esponja suave. - Los paquetes son una forma de agrupar código relacionado para mantenerlo organizado o crear una biblioteca de código. Una vez que se crea un paquete, puedes importar su contenido a otro archivo y reutilizar el código y las clases que contiene.
En esta tarea, crearás un paquete y una clase nuevos con algunas propiedades y un método.
Paso 1: Crea un paquete
Los paquetes pueden ayudarte a mantener tu código organizado.
- En el panel Project, en el proyecto Hello Kotlin, haz clic con el botón derecho en la carpeta src.
- Selecciona New > Package y llámalo
example.myapp
.
Paso 2: Crea una clase con propiedades
Las clases se definen con la palabra clave class
, y los nombres de las clases, por convención, comienzan con una letra mayúscula.
- Haz clic con el botón derecho en el paquete example.myapp.
- Selecciona New > Kotlin File / Class.
- En Kind, selecciona Class y asigna el nombre
Aquarium
a la clase. IntelliJ IDEA incluye el nombre del paquete en el archivo y crea una claseAquarium
vacía para ti. - Dentro de la clase
Aquarium
, define y, luego, inicializa las propiedadesvar
para el ancho, la altura y la longitud (en centímetros). Inicializa las propiedades con valores predeterminados.
package example.myapp
class Aquarium {
var width: Int = 20
var height: Int = 40
var length: Int = 100
}
De forma interna, Kotlin crea automáticamente métodos get y set para las propiedades que definiste en la clase Aquarium
, por lo que puedes acceder a las propiedades directamente, por ejemplo, myAquarium.length
.
Paso 3: Crea una función main()
Crea un archivo nuevo llamado main.kt
para contener la función main()
.
- En el panel Project de la izquierda, haz clic con el botón derecho en el paquete example.myapp.
- Selecciona New > Kotlin File / Class.
- En el menú desplegable Kind, mantén la selección como File y asigna el nombre
main.kt
al archivo. IntelliJ IDEA incluye el nombre del paquete, pero no incluye una definición de clase para un archivo. - Define una función
buildAquarium()
y, dentro de ella, crea una instancia deAquarium
. Para crear una instancia, haz referencia a la clase como si fuera una función,Aquarium()
. Esto llama al constructor de la clase y crea una instancia de la claseAquarium
, de manera similar a usarnew
en otros lenguajes. - Define una función
main()
y llama abuildAquarium()
.
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium()
}
fun main() {
buildAquarium()
}
Paso 4: Agrega un método
- En la clase
Aquarium
, agrega un método para imprimir las propiedades de dimensión del acuario.
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
}
- En
main.kt
, enbuildAquarium()
, llama al métodoprintSize()
enmyAquarium
.
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
}
- A fin de ejecutar el programa, haz clic en el triángulo verde junto a la función
main()
. Observa el resultado.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm
- En
buildAquarium()
, agrega código para establecer la altura en 60 y, luego, imprime las propiedades de dimensión modificadas.
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
myAquarium.height = 60
myAquarium.printSize()
}
- Ejecuta tu programa y observa el resultado.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 100 cm Height: 60 cm
En esta tarea, crearás un constructor para la clase y seguirás trabajando con las propiedades.
Paso 1: Crea un constructor
En este paso, agregarás un constructor a la clase Aquarium
que creaste en la primera tarea. En el ejemplo anterior, cada instancia de Aquarium
se crea con las mismas dimensiones. Puedes cambiar las dimensiones una vez que se crea el objeto configurando las propiedades, pero sería más sencillo crearlo con el tamaño correcto desde el principio.
En algunos lenguajes de programación, el constructor se define creando un método dentro de la clase que tiene el mismo nombre que la clase. En Kotlin, defines el constructor directamente en la declaración de la clase, especificando los parámetros entre paréntesis como si la clase fuera un método. Al igual que con las funciones en Kotlin, esos parámetros pueden incluir valores predeterminados.
- En la clase
Aquarium
que creaste antes, cambia la definición de la clase para incluir tres parámetros del constructor con valores predeterminados paralength
,width
yheight
, y asígnalos a las propiedades correspondientes.
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
// Dimensions in cm
var length: Int = length
var width: Int = width
var height: Int = height
...
}
- La forma más compacta de Kotlin es definir las propiedades directamente con el constructor, usando
var
oval
, y Kotlin también crea los métodos getter y setter automáticamente. Luego, puedes quitar las definiciones de propiedades del cuerpo de la clase.
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
- Cuando creas un objeto
Aquarium
con ese constructor, puedes no especificar ningún argumento y obtener los valores predeterminados, o bien especificar solo algunos de ellos, o bien especificar todos y crear unAquarium
con un tamaño completamente personalizado. En la funciónbuildAquarium()
, prueba diferentes formas de crear un objetoAquarium
con parámetros con nombre.
fun buildAquarium() {
val aquarium1 = Aquarium()
aquarium1.printSize()
// default height and length
val aquarium2 = Aquarium(width = 25)
aquarium2.printSize()
// default width
val aquarium3 = Aquarium(height = 35, length = 110)
aquarium3.printSize()
// everything custom
val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
aquarium4.printSize()
}
- Ejecuta el programa y observa el resultado.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 25 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 110 cm Height: 35 cm Width: 25 cm Length: 110 cm Height: 35 cm
Observa que no tuviste que sobrecargar el constructor ni escribir una versión diferente para cada uno de estos casos (además de algunos más para las otras combinaciones). Kotlin crea lo que se necesita a partir de los valores predeterminados y los parámetros con nombre.
Paso 2: Agrega bloques de inicialización
Los constructores de ejemplo anteriores solo declaran propiedades y les asignan el valor de una expresión. Si tu constructor necesita más código de inicialización, se puede colocar en uno o más bloques init
. En este paso, agregarás algunos bloques init
a la clase Aquarium
.
- En la clase
Aquarium
, agrega un bloqueinit
para imprimir que el objeto se está inicializando y un segundo bloque para imprimir el volumen en litros.
class Aquarium (var length: Int = 100, var width: Int = 20, var height: Int = 40) {
init {
println("aquarium initializing")
}
init {
// 1 liter = 1000 cm^3
println("Volume: ${width * length * height / 1000} l")
}
}
- Ejecuta el programa y observa el resultado.
aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 40 cm
aquarium initializing
Volume: 100 l
Width: 25 cm Length: 100 cm Height: 40 cm
aquarium initializing
Volume: 77 l
Width: 20 cm Length: 110 cm Height: 35 cm
aquarium initializing
Volume: 96 l
Width: 25 cm Length: 110 cm Height: 35 cm
Ten en cuenta que los bloques init
se ejecutan en el orden en que aparecen en la definición de la clase y todos se ejecutan cuando se llama al constructor.
Paso 3: Obtén información sobre los constructores secundarios
En este paso, aprenderás sobre los constructores secundarios y agregarás uno a tu clase. Además de un constructor principal, que puede tener uno o más bloques init
, una clase de Kotlin también puede tener uno o más constructores secundarios para permitir la sobrecarga de constructores, es decir, constructores con diferentes argumentos.
- En la clase
Aquarium
, agrega un constructor secundario que tome una cantidad de peces como argumento, usando la palabra claveconstructor
. Crea una propiedad de tanqueval
para el volumen calculado del acuario en litros según la cantidad de peces. Supón que necesitas 2 litros (2,000 cm³) de agua por pez, más un poco de espacio adicional para que no se derrame el agua.
constructor(numberOfFish: Int) : this() {
// 2,000 cm^3 per fish + extra room so water doesn't spill
val tank = numberOfFish * 2000 * 1.1
}
- Dentro del constructor secundario, mantén la misma longitud y el mismo ancho (que se establecieron en el constructor principal) y calcula la altura necesaria para que el tanque tenga el volumen indicado.
// calculate the height needed
height = (tank / (length * width)).toInt()
- En la función
buildAquarium()
, agrega una llamada para crear unAquarium
con tu nuevo constructor secundario. Imprime el tamaño y el volumen.
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
- Ejecuta tu programa y observa el resultado.
⇒ aquarium initializing Volume: 80 l Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
Observa que el volumen se imprime dos veces: una vez por el bloque init
en el constructor principal antes de que se ejecute el constructor secundario y otra vez por el código en buildAquarium()
.
También podrías haber incluido la palabra clave constructor
en el constructor principal, pero no es necesario en la mayoría de los casos.
Paso 4: Agrega un nuevo getter de propiedad
En este paso, agregarás un getter de propiedad explícito. Kotlin define automáticamente los métodos get y set cuando defines propiedades, pero, a veces, el valor de una propiedad debe ajustarse o calcularse. Por ejemplo, arriba, imprimiste el volumen del Aquarium
. Puedes hacer que el volumen esté disponible como una propiedad definiendo una variable y un getter para ella. Como volume
debe calcularse, el getter debe devolver el valor calculado, lo que puedes hacer con una función de una sola línea.
- En la clase
Aquarium
, define una propiedadInt
llamadavolume
y un métodoget()
que calcule el volumen en la siguiente línea.
val volume: Int
get() = width * height * length / 1000 // 1000 cm^3 = 1 l
- Quita el bloque
init
que imprime el volumen. - Quita el código de
buildAquarium()
que imprime el volumen. - En el método
printSize()
, agrega una línea para imprimir el volumen.
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm "
)
// 1 l = 1000 cm^3
println("Volume: $volume l")
}
- Ejecuta tu programa y observa el resultado.
⇒ aquarium initializing Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
Las dimensiones y el volumen son los mismos que antes, pero el volumen solo se imprime una vez después de que el constructor principal y el secundario inicializan por completo el objeto.
Paso 5: Agrega un setter de propiedad
En este paso, crearás un nuevo establecedor de propiedades para el volumen.
- En la clase
Aquarium
, cambiavolume
a unvar
para que se pueda configurar más de una vez. - Agrega un setter para la propiedad
volume
agregando un métodoset()
debajo del getter, que vuelve a calcular la altura según la cantidad de agua proporcionada. Por convención, el nombre del parámetro setter esvalue
, pero puedes cambiarlo si lo prefieres.
var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
- En
buildAquarium()
, agrega código para establecer el volumen del acuario en 70 litros. Imprime el tamaño nuevo.
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
aquarium6.volume = 70
aquarium6.printSize()
}
- Vuelve a ejecutar el programa y observa la altura y el volumen modificados.
⇒ aquarium initialized
Width: 20 cm Length: 100 cm Height: 31 cm
Volume: 62 l
Width: 20 cm Length: 100 cm Height: 35 cm
Volume: 70 l
Hasta ahora, no hubo modificadores de visibilidad, como public
o private
, en el código. Esto se debe a que, de forma predeterminada, todo en Kotlin es público, lo que significa que se puede acceder a todo en cualquier lugar, incluidas las clases, los métodos, las propiedades y las variables miembro.
En Kotlin, las clases, los objetos, las interfaces, los constructores, las funciones, las propiedades y sus métodos setter pueden tener modificadores de visibilidad:
public
significa que es visible fuera de la clase. Todo es público de forma predeterminada, incluidas las variables y los métodos de la clase.internal
significa que solo será visible dentro de ese módulo. Un módulo es un conjunto de archivos de Kotlin que se compilan juntos, por ejemplo, una biblioteca o una aplicación.private
significa que solo será visible en esa clase (o archivo fuente si trabajas con funciones).protected
es igual queprivate
, pero también será visible para cualquier subclase.
Consulta Modificadores de visibilidad en la documentación de Kotlin para obtener más información.
Variables miembro
Las propiedades dentro de una clase, o variables miembro, son public
de forma predeterminada. Si los defines con var
, son mutables, es decir, legibles y escribibles. Si los defines con val
, serán de solo lectura después de la inicialización.
Si quieres una propiedad que tu código pueda leer o escribir, pero que el código externo solo pueda leer, puedes dejar la propiedad y su captador como públicos, y declarar el establecedor como privado, como se muestra a continuación.
var volume: Int
get() = width * height * length / 1000
private set(value) {
height = (value * 1000) / (width * length)
}
En esta tarea, aprenderás cómo funcionan las subclases y la herencia en Kotlin. Son similares a los que viste en otros idiomas, pero hay algunas diferencias.
En Kotlin, de forma predeterminada, no se pueden crear subclases de las clases. Del mismo modo, las subclases no pueden anular las propiedades ni las variables miembro (aunque se puede acceder a ellas).
Debes marcar una clase como open
para permitir que se subclasifique. Del mismo modo, debes marcar las propiedades y las variables miembro como open
para anularlas en la subclase. Se requiere la palabra clave open
para evitar que se filtren accidentalmente detalles de implementación como parte de la interfaz de la clase.
Paso 1: Abre la clase de Aquarium
En este paso, harás que la clase Aquarium
sea open
para que puedas anularla en el siguiente paso.
- Marca la clase
Aquarium
y todas sus propiedades con la palabra claveopen
.
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
open var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
- Agrega una propiedad
shape
abierta con el valor"rectangle"
.
open val shape = "rectangle"
- Agrega una propiedad
water
abierta con un getter que devuelva el 90% del volumen delAquarium
.
open var water: Double = 0.0
get() = volume * 0.9
- Agrega código al método
printSize()
para imprimir la forma y la cantidad de agua como porcentaje del volumen.
fun printSize() {
println(shape)
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
// 1 l = 1000 cm^3
println("Volume: $volume l Water: $water l (${water/volume*100.0}% full)")
}
- En
buildAquarium()
, cambia el código para crear unAquarium
conwidth = 25
,length = 25
yheight = 40
.
fun buildAquarium() {
val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
aquarium6.printSize()
}
- Ejecuta el programa y observa el nuevo resultado.
⇒ aquarium initializing rectangle Width: 25 cm Length: 25 cm Height: 40 cm Volume: 25 l Water: 22.5 l (90.0% full)
Paso 2: Crea una subclase
- Crea una subclase de
Aquarium
llamadaTowerTank
, que implementa un tanque cilíndrico redondeado en lugar de un tanque rectangular. Puedes agregarTowerTank
debajo deAquarium
, ya que puedes agregar otra clase en el mismo archivo que la claseAquarium
. - En
TowerTank
, anula la propiedadheight
, que se define en el constructor. Para anular una propiedad, usa la palabra claveoverride
en la subclase.
- Haz que el constructor de
TowerTank
tome undiameter
. Usadiameter
paralength
ywidth
cuando llames al constructor en la superclaseAquarium
.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
- Anula la propiedad de volumen para calcular un cilindro. La fórmula para un cilindro es pi por el radio al cuadrado por la altura. Debes importar la constante
PI
dejava.lang.Math
.
override var volume: Int
// ellipse area = π * r1 * r2
get() = (width/2 * length/2 * height / 1000 * PI).toInt()
set(value) {
height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
}
- En
TowerTank
, anula la propiedadwater
para que sea el 80% del volumen.
override var water = volume * 0.8
- Anula el valor
shape
para que sea"cylinder"
.
override val shape = "cylinder"
- Tu clase
TowerTank
final debería verse como el siguiente código:
Aquarium.kt
:
package example.myapp
import java.lang.Math.PI
... // existing Aquarium class
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
override var volume: Int
// ellipse area = π * r1 * r2
get() = (width/2 * length/2 * height / 1000 * PI).toInt()
set(value) {
height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
}
override var water = volume * 0.8
override val shape = "cylinder"
}
- En
buildAquarium()
, crea unTowerTank
con un diámetro de 25 cm y una altura de 45 cm. Imprime el tamaño.
main.kt:
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium(width = 25, length = 25, height = 40)
myAquarium.printSize()
val myTower = TowerTank(diameter = 25, height = 40)
myTower.printSize()
}
- Ejecuta tu programa y observa el resultado.
⇒ aquarium initializing rectangle Width: 25 cm Length: 25 cm Height: 40 cm Volume: 25 l Water: 22.5 l (90.0% full) aquarium initializing cylinder Width: 25 cm Length: 25 cm Height: 40 cm Volume: 18 l Water: 14.4 l (80.0% full)
A veces, deseas definir un comportamiento o propiedades comunes para que se compartan entre algunas clases relacionadas. Kotlin ofrece dos formas de hacerlo: interfaces y clases abstractas. En esta tarea, crearás una clase abstracta AquariumFish
para las propiedades que son comunes a todos los peces. Creas una interfaz llamada FishAction
para definir el comportamiento común a todos los peces.
- Ni una clase abstracta ni una interfaz se pueden instanciar por sí solas, lo que significa que no puedes crear objetos de esos tipos directamente.
- Las clases abstractas tienen constructores.
- Las interfaces no pueden tener lógica de constructor ni almacenar ningún estado.
Paso 1: Crea una clase abstracta
- En example.myapp, crea un archivo nuevo,
AquariumFish.kt
. - Crea una clase, también llamada
AquariumFish
, y márcala conabstract
. - Agrega una propiedad
String
,color
, y márcala conabstract
.
package example.myapp
abstract class AquariumFish {
abstract val color: String
}
- Crea dos subclases de
AquariumFish
,Shark
yPlecostomus
. - Como
color
es abstracta, las subclases deben implementarla. Haz queShark
sea gris yPlecostomus
, dorado.
class Shark: AquariumFish() {
override val color = "gray"
}
class Plecostomus: AquariumFish() {
override val color = "gold"
}
- En main.kt, crea una función
makeFish()
para probar tus clases. Crea instancias de unShark
y unPlecostomus
, y, luego, imprime el color de cada uno. - Borra el código de prueba anterior en
main()
y agrega una llamada amakeFish()
. El código debería ser similar al siguiente.
main.kt
:
package example.myapp
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
println("Plecostomus: ${pleco.color}")
}
fun main () {
makeFish()
}
- Ejecuta tu programa y observa el resultado.
⇒ Shark: gray Plecostomus: gold
En el siguiente diagrama, se representan las clases Shark
y Plecostomus
, que son subclases de la clase abstracta AquariumFish
.
Paso 2: Crea una interfaz
- En AquariumFish.kt, crea una interfaz llamada
FishAction
con un métodoeat()
.
interface FishAction {
fun eat()
}
- Agrega
FishAction
a cada una de las subclases y, luego, implementaeat()
para que imprima lo que hace el pez.
class Shark: AquariumFish(), FishAction {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
class Plecostomus: AquariumFish(), FishAction {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
- En la función
makeFish()
, haz que cada pez que creaste coma algo llamando aeat()
.
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
shark.eat()
println("Plecostomus: ${pleco.color}")
pleco.eat()
}
- Ejecuta tu programa y observa el resultado.
⇒ Shark: gray hunt and eat fish Plecostomus: gold eat algae
En el siguiente diagrama, se representan las clases Shark
y Plecostomus
, que se componen de la interfaz FishAction
y la implementan.
Cuándo usar clases abstractas en lugar de interfaces
Los ejemplos anteriores son simples, pero cuando tienes muchas clases interrelacionadas, las clases abstractas y las interfaces pueden ayudarte a mantener tu diseño más limpio, organizado y fácil de mantener.
Como se mencionó anteriormente, las clases abstractas pueden tener constructores, y las interfaces no, pero, de lo contrario, son muy similares. Entonces, ¿cuándo deberías usar cada una?
Cuando usas interfaces para componer una clase, la funcionalidad de la clase se extiende por medio de las instancias de clase que contiene. La composición tiende a hacer que el código sea más fácil de reutilizar y razonar que la herencia de una clase abstracta. Además, puedes usar varias interfaces en una clase, pero solo puedes crear subclases a partir de una clase abstracta.
La composición suele generar una mejor encapsulación, un menor acoplamiento (interdependencia), interfaces más limpias y código más utilizable. Por estos motivos, usar la composición con interfaces es el diseño preferido. Por otro lado, la herencia de una clase abstracta tiende a ser una opción natural para algunos problemas. Por lo tanto, debes preferir la composición, pero cuando la herencia tiene sentido, Kotlin también te permite usarla.
- Usa una interfaz si tienes muchos métodos y una o dos implementaciones predeterminadas, por ejemplo, como en
AquariumAction
a continuación.
interface AquariumAction {
fun eat()
fun jump()
fun clean()
fun catchFish()
fun swim() {
println("swim")
}
}
- Usa una clase abstracta siempre que no puedas completar una clase. Por ejemplo, si volvemos a la clase
AquariumFish
, puedes hacer que todos losAquariumFish
implementenFishAction
y proporcionar una implementación predeterminada paraeat
mientras dejascolor
abstracto, ya que no hay un color predeterminado para los peces.
interface FishAction {
fun eat()
}
abstract class AquariumFish: FishAction {
abstract val color: String
override fun eat() = println("yum")
}
En la tarea anterior, se presentaron las clases abstractas, las interfaces y la idea de composición. La delegación de interfaces es una técnica avanzada en la que los métodos de una interfaz se implementan con un objeto auxiliar (o delegado) que luego usa una clase. Esta técnica puede ser útil cuando usas una interfaz en una serie de clases no relacionadas: agregas la funcionalidad de interfaz necesaria a una clase de ayuda separada, y cada una de las clases usa una instancia de la clase de ayuda para implementar la funcionalidad.
En esta tarea, usarás la delegación de interfaces para agregar funcionalidad a una clase.
Paso 1: Crea una interfaz nueva
- En AquariumFish.kt, quita la clase
AquariumFish
. En lugar de heredar de la claseAquariumFish
,Plecostomus
yShark
implementarán interfaces para la acción del pez y su color. - Crea una interfaz nueva,
FishColor
, que defina el color como una cadena.
interface FishColor {
val color: String
}
- Cambia
Plecostomus
para implementar dos interfaces,FishAction
yFishColor
. Debes anularcolor
deFishColor
yeat()
deFishAction
.
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
- Cambia tu clase
Shark
para que también implemente las dos interfaces,FishAction
yFishColor
, en lugar de heredar deAquariumFish
.
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
- El código finalizado debería verse de la siguiente manera:
package example.myapp
interface FishAction {
fun eat()
}
interface FishColor {
val color: String
}
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
Paso 2: Crea una clase singleton
A continuación, implementa la configuración de la parte de delegación creando una clase de ayuda que implemente FishColor
. Creas una clase básica llamada GoldColor
que implementa FishColor
. Lo único que hace es indicar que su color es dorado.
No tiene sentido crear varias instancias de GoldColor
, ya que todas harían exactamente lo mismo. Por lo tanto, Kotlin te permite declarar una clase en la que solo puedes crear una instancia de ella usando la palabra clave object
en lugar de class
. Kotlin creará esa instancia, y el nombre de la clase hará referencia a ella. Luego, todos los demás objetos pueden usar esta única instancia. No hay forma de crear otras instancias de esta clase. Si conoces el patrón singleton, así es como se implementan los singletons en Kotlin.
- En AquariumFish.kt, crea un objeto para
GoldColor
. Anula el color.
object GoldColor : FishColor {
override val color = "gold"
}
Paso 3: Agrega la delegación de la interfaz para FishColor
Ahora puedes usar la delegación de interfaces.
- En AquariumFish.kt, quita la anulación de
color
dePlecostomus
. - Cambia la clase
Plecostomus
para obtener su color deGoldColor
. Para ello, agregaby GoldColor
a la declaración de la clase y crea la delegación. Esto significa que, en lugar de implementarFishColor
, debes usar la implementación que proporcionaGoldColor
. Por lo tanto, cada vez que se accede acolor
, se delega aGoldColor
.
class Plecostomus: FishAction, FishColor by GoldColor {
override fun eat() {
println("eat algae")
}
}
Con la clase tal como está, todos los Plecos serán dorados, pero estos peces en realidad vienen en muchos colores. Para solucionar este problema, agrega un parámetro de constructor para el color con GoldColor
como color predeterminado para Plecostomus
.
- Cambia la clase
Plecostomus
para que tome unfishColor
pasado con su constructor y establece su valor predeterminado enGoldColor
. Cambia la delegación deby GoldColor
aby fishColor
.
class Plecostomus(fishColor: FishColor = GoldColor): FishAction,
FishColor by fishColor {
override fun eat() {
println("eat algae")
}
}
Paso 4: Agrega la delegación de la interfaz para FishAction
Del mismo modo, puedes usar la delegación de la interfaz para FishAction
.
- En AquariumFish.kt, crea una clase
PrintingFishAction
que implementeFishAction
, que tome unString
,food
y, luego, imprima lo que come el pez.
class PrintingFishAction(val food: String) : FishAction {
override fun eat() {
println(food)
}
}
- En la clase
Plecostomus
, quita la función de anulacióneat()
, ya que la reemplazarás por una delegación. - En la declaración de
Plecostomus
, delegaFishAction
aPrintingFishAction
y pasa"eat algae"
. - Con toda esa delegación, no hay código en el cuerpo de la clase
Plecostomus
, por lo que se debe quitar{}
, ya que todas las anulaciones se controlan con la delegación de la interfaz.
class Plecostomus (fishColor: FishColor = GoldColor):
FishAction by PrintingFishAction("eat algae"),
FishColor by fishColor
En el siguiente diagrama, se representan las clases Shark
y Plecostomus
, ambas compuestas por las interfaces PrintingFishAction
y FishColor
, pero delegando la implementación en ellas.
La delegación de interfaces es potente y, en general, debes considerar cómo usarla siempre que uses una clase abstracta en otro lenguaje. Te permite usar la composición para conectar comportamientos, en lugar de requerir muchas subclases, cada una especializada de una manera diferente.
Una clase de datos es similar a un struct
en otros lenguajes (existe principalmente para contener algunos datos), pero un objeto de clase de datos sigue siendo un objeto. Los objetos de clase de datos de Kotlin tienen algunos beneficios adicionales, como utilidades para imprimir y copiar. En esta tarea, crearás una clase de datos simple y aprenderás sobre la compatibilidad que Kotlin proporciona para las clases de datos.
Paso 1: Crea una clase de datos
- Agrega un paquete
decor
nuevo en el paquete example.myapp para contener el código nuevo. Haz clic con el botón derecho en example.myapp en el panel Project y selecciona File > New > Package. - En el paquete, crea una nueva clase llamada
Decoration
.
package example.myapp.decor
class Decoration {
}
- Para convertir
Decoration
en una clase de datos, antepón la palabra clavedata
a la declaración de la clase. - Agrega una propiedad
String
llamadarocks
para proporcionar algunos datos a la clase.
data class Decoration(val rocks: String) {
}
- En el archivo, fuera de la clase, agrega una función
makeDecorations()
para crear e imprimir una instancia de unDecoration
con"granite"
.
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
}
- Agrega una función
main()
para llamar amakeDecorations()
y ejecuta tu programa. Observa el resultado útil que se crea porque se trata de una clase de datos.
⇒ Decoration(rocks=granite)
- En
makeDecorations()
, crea dos objetosDecoration
más que sean "pizarra" y, luego, imprímelos.
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
val decoration2 = Decoration("slate")
println(decoration2)
val decoration3 = Decoration("slate")
println(decoration3)
}
- En
makeDecorations()
, agrega una sentencia de impresión que imprima el resultado de comparardecoration1
condecoration2
y otra que comparedecoration3
condecoration2
. Usa el método equals() que proporcionan las clases de datos.
println (decoration1.equals(decoration2))
println (decoration3.equals(decoration2))
- Ejecuta tu código.
⇒ Decoration(rocks=granite) Decoration(rocks=slate) Decoration(rocks=slate) false true
Paso 2: Usa la desestructuración
Para acceder a las propiedades de un objeto de datos y asignarlas a variables, puedes asignarlas una por vez, de la siguiente manera.
val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver
En cambio, puedes crear variables, una para cada propiedad, y asignar el objeto de datos al grupo de variables. Kotlin coloca el valor de la propiedad en cada variable.
val (rock, wood, diver) = decoration
Esto se llama desestructuración y es una abreviatura útil. La cantidad de variables debe coincidir con la cantidad de propiedades, y las variables se asignan en el orden en que se declaran en la clase. Aquí tienes un ejemplo completo que puedes probar en Decoration.kt.
// Here is a data class with 3 properties.
data class Decoration2(val rocks: String, val wood: String, val diver: String){
}
fun makeDecorations() {
val d5 = Decoration2("crystal", "wood", "diver")
println(d5)
// Assign all properties to variables.
val (rock, wood, diver) = d5
println(rock)
println(wood)
println(diver)
}
⇒ Decoration2(rocks=crystal, wood=wood, diver=diver) crystal wood diver
Si no necesitas una o más de las propiedades, puedes omitirlas usando _
en lugar de un nombre de variable, como se muestra en el siguiente código.
val (rock, _, diver) = d5
En esta tarea, aprenderás sobre algunas de las clases de propósito especial en Kotlin, incluidas las siguientes:
- Clases singleton
- Enumeraciones
- Clases selladas
Paso 1: Recuerda las clases singleton
Recuerda el ejemplo anterior con la clase GoldColor
.
object GoldColor : FishColor {
override val color = "gold"
}
Como cada instancia de GoldColor
hace lo mismo, se declara como un object
en lugar de como un class
para que sea singleton. Solo puede haber una instancia de él.
Paso 2: Crea una enumeración
Kotlin también admite enumeraciones, que te permiten enumerar algo y hacer referencia a ello por su nombre, de forma similar a otros lenguajes. Declara un enum agregando el prefijo de la declaración con la palabra clave enum
. Una declaración de enumeración básica solo necesita una lista de nombres, pero también puedes definir uno o más campos asociados con cada nombre.
- En Decoration.kt, prueba un ejemplo de una enumeración.
enum class Color(val rgb: Int) {
RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}
Los enums son un poco como los singleton: solo puede haber uno, y solo uno de cada valor en la enumeración. Por ejemplo, solo puede haber un Color.RED
, un Color.GREEN
y un Color.BLUE
. En este ejemplo, los valores RGB se asignan a la propiedad rgb
para representar los componentes de color. También puedes obtener el valor ordinal de un enum con la propiedad ordinal
y su nombre con la propiedad name
.
- Probemos con otro ejemplo de enumeración.
enum class Direction(val degrees: Int) {
NORTH(0), SOUTH(180), EAST(90), WEST(270)
}
fun main() {
println(Direction.EAST.name)
println(Direction.EAST.ordinal)
println(Direction.EAST.degrees)
}
⇒ EAST 2 90
Paso 3: Crea una clase sellada
Una clase sellada es una clase que puede tener subclases, pero solo dentro del archivo en el que se declara. Si intentas crear una subclase en un archivo diferente, recibirás un error.
Como las clases y las subclases están en el mismo archivo, Kotlin conocerá todas las subclases de forma estática. Es decir, en el momento de la compilación, el compilador ve todas las clases y subclases, y sabe que son todas, por lo que puede realizar verificaciones adicionales por ti.
- En AquariumFish.kt, prueba un ejemplo de una clase sellada, manteniendo el tema acuático.
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()
fun matchSeal(seal: Seal): String {
return when(seal) {
is Walrus -> "walrus"
is SeaLion -> "sea lion"
}
}
La clase Seal
no se puede subclasear en otro archivo. Si deseas agregar más tipos de Seal
, debes agregarlos en el mismo archivo. Esto hace que las clases selladas sean una forma segura de representar una cantidad fija de tipos. Por ejemplo, las clases selladas son ideales para devolver un éxito o un error desde una API de red.
En esta lección, se abordaron muchos temas. Si bien gran parte de este lenguaje te resultará familiar por otros lenguajes de programación orientada a objetos, Kotlin agrega algunas funciones para mantener el código conciso y legible.
Clases y constructores
- Define una clase en Kotlin con
class
. - Kotlin crea automáticamente métodos get y set para las propiedades.
- Define el constructor principal directamente en la definición de la clase. Por ejemplo:
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
- Si un constructor principal necesita código adicional, escríbelo en uno o más bloques
init
. - Una clase puede definir uno o más constructores secundarios con
constructor
, pero el estilo de Kotlin es usar una función de fábrica en su lugar.
Modificadores de visibilidad y subclases
- Todas las clases y funciones en Kotlin son
public
de forma predeterminada, pero puedes usar modificadores para cambiar la visibilidad ainternal
,private
oprotected
. - Para crear una subclase, la clase superior debe estar marcada como
open
. - Para anular métodos y propiedades en una subclase, los métodos y las propiedades deben estar marcados como
open
en la clase principal. - Una clase sellada solo puede tener subclases en el mismo archivo en el que se define. Para crear una clase sellada, agrega el prefijo
sealed
a la declaración.
Clases de datos, singletons y enumeraciones
- Crea una clase de datos agregando el prefijo
data
a la declaración. - La desestructuración es una abreviatura para asignar las propiedades de un objeto
data
a variables separadas. - Crea una clase singleton con
object
en lugar declass
. - Define un enum con
enum class
.
Clases abstractas, interfaces y delegación
- Las clases abstractas y las interfaces son dos formas de compartir un comportamiento común entre las clases.
- Una clase abstracta define propiedades y comportamiento, pero deja la implementación a las subclases.
- Una interfaz define el comportamiento y puede proporcionar implementaciones predeterminadas para parte o todo el comportamiento.
- Cuando usas interfaces para componer una clase, la funcionalidad de la clase se extiende por medio de las instancias de clase que contiene.
- La delegación de interfaces usa la composición, pero también delega la implementación a las clases de interfaz.
- La composición es una forma eficaz de agregar funcionalidad a una clase a través de la delegación de interfaces. En general, se prefiere la composición, pero la herencia de una clase abstracta se adapta mejor a algunos problemas.
Documentación de Kotlin
Si deseas obtener más información sobre algún tema de este curso o si te quedas atascado, https://kotlinlang.org es el mejor punto de partida.
- Clases y herencia
- Constructores
- Funciones de fábrica
- Propiedades y campos
- Modificadores de visibilidad
- Clases abstractas
- Interfaces
- Delegation
- Clases de datos
- Igualdad
- Cómo realizar la desestructuración
- Declaraciones de objetos
- Clases enum
- Clases selladas
- Cómo controlar errores opcionales con clases selladas de Kotlin
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 Capacitación de Kotlin para programadores.
IntelliJ IDEA
Encontrarás la documentación de IntelliJ IDEA en el sitio web de JetBrains.
En esta sección, se enumeran las posibles actividades para el hogar para los alumnos que trabajan en este codelab como parte de un curso dirigido por un instructor. Depende del instructor hacer lo siguiente:
- Si es necesario, asigna una tarea.
- Comunicarles a los alumnos cómo enviar las actividades para el hogar.
- Califica las actividades para el hogar.
Los instructores pueden usar estas sugerencias en la medida que quieran y deben asignar cualquier otra actividad para el hogar que consideren apropiada.
Si estás trabajando en este codelab por tu cuenta, usa estas actividades para el hogar para probar tus conocimientos.
Responde estas preguntas:
Pregunta 1
Las clases tienen un método especial que sirve como plano para crear objetos de esa clase. ¿Cómo se llama el método?
▢ Un compilador
▢ Un instanciador
▢ Un constructor
▢ Un plano
Pregunta 2
¿Cuál de las siguientes afirmaciones sobre las interfaces y las clases abstractas NO es correcta?
▢ Las clases abstractas pueden tener constructores.
▢ Las interfaces no pueden tener constructores.
▢ Las interfaces y las clases abstractas se pueden crear instancias directamente.
▢ Las propiedades abstractas deben ser implementadas por las subclases de la clase abstracta.
Pregunta 3
¿Cuál de las siguientes opciones NO es un modificador de visibilidad de Kotlin para propiedades, métodos, etcétera?
▢ internal
▢ nosubclass
▢ protected
▢ private
Pregunta 4
Considera esta clase de datos:data class Fish(val name: String, val species:String, val colors:String)
¿Cuál de los siguientes códigos NO es válido para crear y desestructurar un objeto Fish
?
▢ val (name1, species1, colors1) = Fish("Pat", "Plecostomus", "gold")
▢ val (name2, _, colors2) = Fish("Bitey", "shark", "gray")
▢ val (name3, species3, _) = Fish("Amy", "angelfish", "blue and black stripes")
▢ val (name4, species4, colors4) = Fish("Harry", "halibut")
Pregunta 5
Supongamos que tienes un zoológico con muchos animales que necesitan cuidados. ¿Cuál de las siguientes opciones NO formaría parte de la implementación del cuidado?
▢ Un interface
para los diferentes tipos de alimentos que comen los animales
▢ Una clase abstract Caretaker
desde la que puedes crear diferentes tipos de cuidadores.
▢ Un interface
por darle agua limpia a un animal
▢ Una clase data
para una entrada en un programa de alimentación.
Continúa con la siguiente lección:
Para obtener una descripción general del curso, incluidos los vínculos a otros codelabs, consulta "Capacitación de Kotlin para programadores: Bienvenido al curso".