Clases y objetos
Al terminar: Entender qué es una clase y un objeto, y escribir una clase con propiedades y métodos.
Al terminar sabrás qué es una clase y qué es un objeto, y habrás escrito la primera clase del torneo: Jugador, que reúne el nombre, el dorsal y las funciones del dorsal que practicaste en Nulabilidad. Verás cómo un programa pasa de tener datos sueltos a tener «cosas» con sus datos y sus funciones juntos.
Importa porque casi todo lo que viene después (la app de Android, la de iOS, el resto del curso) está construido con clases. Si entiendes bien el molde y las piezas que se hacen con él, el resto se lee con mucho menos esfuerzo.
Temas: clase y objeto, propiedades, métodos, constructor (con y sin val/var), el punto, val y var en una clase, y dos comprobaciones nuevas en los tests (assertNull y assertFailsWith).
Por qué existen las clases
Hasta ahora, cada dato viajaba solo. En el ejercicio de Nulabilidad, cada función recibía el nombre o el dorsal como parámetro: dorsalOGuion(9), longitudDelNombre("Ana"). Con dos datos ya es incómodo; con diez, un programa se llena de funciones a las que hay que pasarles siempre los mismos datos, y es fácil mezclarlos.
Una clase soluciona eso: junta en un solo sitio los datos de una «cosa» (un jugador, un libro, una entrada) y las funciones que trabajan con esos datos. A esa forma de organizar un programa, en objetos que guardan sus datos y saben hacer cosas con ellos, se le llama programación orientada a objetos. No tienes que aprender nada más sobre ella ahora: es lo que verás en esta lección y en la siguiente.
Cinco palabras que se usan sin parar a partir de aquí:
| Palabra | Qué es | En el ejemplo de abajo |
|---|---|---|
| clase | el molde: dice qué datos tiene una cosa y qué sabe hacer | Libro |
| objeto | una cosa concreta hecha con ese molde (también se le llama instancia: cada objeto creado desde una clase) | el libro «Rayuela» |
| propiedad | un dato del objeto | titulo, paginas |
| método | una función que vive dentro de la clase y usa los datos del objeto | esLargo() |
| constructor | la receta para crear un objeto: qué datos hay que darle | los paréntesis de Libro(…) |
Para crear un objeto se escribe el nombre de la clase con sus datos entre paréntesis, sin ninguna palabra delante. Y para pedirle algo a un objeto se usa el punto, como ya hiciste con "Ana".length: rayuela.titulo lee una propiedad y rayuela.esLargo() llama a un método.
// La clase es el molde: dice qué datos tiene un libro y qué sabe hacer
class Libro(val titulo: String, val paginas: Int) { // los paréntesis son el constructor
// var: una propiedad que puede cambiar; empieza en false
var prestado: Boolean = false
// un método: una función que vive dentro de la clase y usa sus datos
fun esLargo(): Boolean = paginas > 400
// otro método: cambia un dato del objeto
fun prestar() {
prestado = true
}
}
fun main() {
val rayuela = Libro("Rayuela", 600) // crear un objeto: el nombre de la clase y sus datos
val diario = Libro("Diario", 120) // otro objeto del mismo molde, con otros datos
println(rayuela.titulo) // el punto lee una propiedad del objeto: Rayuela
println(diario.paginas) // 120
println(rayuela.esLargo()) // el punto también llama a un método: true
println(diario.esLargo()) // false: cada objeto usa sus propios datos
rayuela.prestar() // cambia solo el objeto rayuela
println(rayuela.prestado) // true
println(diario.prestado) // false: el otro objeto sigue igual
}Rayuela
120
true
false
true
falseDos detalles de esa clase:
valovardentro de los paréntesis convierte el dato en una propiedad del objeto.vales de solo lectura (como una variableval: no se puede reasignar) yvarse puede cambiar. Si en el ejemplo escribesrayuela.titulo = "Otro", Kotlin rechaza la línea con'val' cannot be reassigned.- Lo que va dentro de las llaves de la clase son más propiedades (
prestado, con su valor inicial) y los métodos. Dentro de un método,paginasyprestadose escriben sin punto: son los del propio objeto.
¿Y si no pones val ni var en el constructor?
Un parámetro del constructor sin val ni var no se convierte en propiedad de la clase. Es solo un dato de paso: existe mientras se construye el objeto (dentro de las llaves de la clase, en el valor inicial de otra propiedad o en un bloque init, que se explica en la siguiente lección) y después se pierde. Desde fuera del objeto no se puede leer, y los métodos de la clase tampoco lo ven, salvo que lo hayas usado para guardar algo en una propiedad.
class Jugador(nombre: String) { // «nombre» no lleva val ni var: no es una propiedad
val letras = nombre.length // se usa aquí, al crear el objeto; el resultado sí se guarda
}
fun main() {
val j = Jugador("Ana")
println(j.letras) // la propiedad «letras» existe
// println(j.nombre) // esta línea no compila: «nombre» no es una propiedad
}3Si quitas el // de la última línea, Kotlin se queja con unresolved reference 'nombre' (referencia sin resolver: ese nombre no existe en el objeto). Con val, en cambio, sí existe:
class Jugador(val nombre: String) // val: Kotlin crea la propiedad «nombre» y guarda el dato
fun main() {
val j = Jugador("Ana")
println(j.nombre) // se lee desde fuera, porque es una propiedad
}AnaEn resumen: val o var le dicen a Kotlin «guarda esto como parte del objeto, para poder leerlo desde fuera» (con var, además, se puede cambiar). Sin ellos, el parámetro solo sirve para construir el objeto y luego se pierde, a menos que lo guardes tú en otra propiedad.
Si imprimes un objeto entero (println(rayuela)), Kotlin no sabe cómo mostrarlo y escribe algo como Libro@3d4eac69: el nombre de la clase y un número sin sentido. En la siguiente lección verás cómo decidir qué se imprime.
De funciones sueltas a una clase
Lo mismo que en el ejemplo anterior, pero con el dato que puede faltar (Int?, visto en Nulabilidad) y comparando con la forma de antes: la función suelta recibe el dato; el método lo toma del objeto. Al final aparece listOf(…), que crea una lista: varios valores guardados en orden, que un for recorre uno a uno. Con eso basta por ahora; las listas se ven a fondo en Colecciones.
// Antes: una función suelta que recibe el dato
fun letrasDelTituloSuelta(titulo: String): Int? = titulo.takeIf { it.isNotBlank() }?.length
// Después: el dato vive en el objeto; val dentro de los paréntesis crea una propiedad.
// Aquí las páginas pueden faltar: por eso son Int?
class Libro(val titulo: String, val paginas: Int?) {
// un método: una función dentro de la clase. Usa titulo sin recibirlo
fun letrasDelTitulo(): Int? = titulo.takeIf { it.isNotBlank() }?.length
}
// Las funciones que no son de un solo libro se quedan fuera y reciben el objeto
fun ficha(libro: Libro?): String {
if (libro == null) return "Sin libro"
return "${libro.titulo}, ${libro.paginas ?: "?"} págs." // libro.titulo: la propiedad del objeto
}
fun main() {
println(letrasDelTituloSuelta("Rayuela")) // 7: la versión suelta
println(Libro("Rayuela", 600).letrasDelTitulo()) // 7: el mismo cálculo, como método
println(ficha(Libro("Diario", null))) // Diario, ? págs.
println(ficha(null)) // Sin libro
val libros = listOf(Libro("Rayuela", 600), Libro("Diario", null)) // listOf: una lista de solo lectura
for (libro in libros) println(libro.titulo) // for recorre la lista: una vuelta por libro
}7
7
Diario, ? págs.
Sin libro
Rayuela
DiarioUn último detalle sobre Int? antes del ejercicio: un valor nulable se puede comparar con == sin comprobar antes que no es null. No falla: da true solo si el valor existe y es igual, y false si es null o es otro valor.
fun main() {
val edadA: Int? = 0
val edadB: Int? = null
val edadC: Int? = 7
println(edadA == 0) // true: tiene valor y es 0
println(edadB == 0) // false: es null, no se queja
println(edadC == 0) // false: tiene valor, pero es otro
}true
false
falseReto · Ejercicio: la ficha de la mascota
En el Kotlin Playground (te lo presentó Tu primer proyecto en Kotlin): una mascota se crea con un nombre, una especie y una edad que puede no saberse. Sabe presentarse («Luna (gato, 3 años)», o «edad desconocida» si no se sabe) y sabe decir si es cachorro (si tiene 0 años). Crea a Luna (gato, 3), Coco (perro, edad desconocida) y Nube (conejo, 0), presenta a las tres y escribe si Nube es cachorro y si Coco lo es. La consola debe mostrar:
Luna (gato, 3 años)
Coco (perro, edad desconocida)
Nube (conejo, 0 años)
true
falseVer solución
class Mascota(val nombre: String, val especie: String, val edad: Int?) {
// un método que devuelve el texto de la presentación
fun presentar(): String {
val años = if (edad == null) "edad desconocida" else "$edad años"
return "$nombre ($especie, $años)"
}
// otro método: true solo si la edad es 0 (con null, edad == 0 es false, como en el ejemplo de arriba)
fun esCachorro(): Boolean = edad == 0
}
fun main() {
val luna = Mascota("Luna", "gato", 3)
val coco = Mascota("Coco", "perro", null)
val nube = Mascota("Nube", "conejo", 0)
for (mascota in listOf(luna, coco, nube)) println(mascota.presentar())
println(nube.esCachorro()) // true
println(coco.esCachorro()) // false: no se sabe su edad
}Luna (gato, 3 años)
Coco (perro, edad desconocida)
Nube (conejo, 0 años)
true
falseLa clase guarda los tres datos como propiedades y los dos métodos los usan sin recibirlos como parámetros. Los tres objetos salen del mismo molde, y cada uno tiene sus propios datos.
Paso · Jugador: los datos y sus funciones, juntos
Objetivo: la clase Jugador en tu proyecto, con su nombre, su dorsal y, dentro, las funciones del dorsal que practicaste en el Playground. main imprime Ana (#9), Luis (-), Marta (#1) y Sin jugador.
Es el mismo paso del ejemplo de Libro, con los datos del proyecto: las funciones del ejercicio de Nulabilidad pasan a ser métodos, y el nombre y el dorsal viven dentro del objeto: Jugador("Ana", 9).dorsalOGuion().
-
Crea
Jugador.kt: clic derecho sobre el paquetecom.sebastiannarvaez.marcador.torneo(el que no lleva(test)) → New → Kotlin Class/File → Class →Jugador. Android Studio escribe un esqueletoclass Jugador {}: bórralo, porque lo sustituyes por tu clase. -
Crea
JugadorTest.kten el paquete que lleva(test), donde estáPuntosTest.kt(New → Kotlin Class/File → File →JugadorTest). Sus seis tests no los escribes: son comprobaciones como las dePuntosTest. El fichero nace casi vacío y se rellena así:- Pulsa el botón Copiar fichero de abajo. Solo copia el contenido al portapapeles, como si lo hubieras seleccionado y copiado tú: no escribe nada en el proyecto.
- Haz clic dentro de
JugadorTest.kt, selecciona todo (Cmd+A) y pega (Cmd+V). Lo pegado ya trae su propia líneapackage, así que reemplaza la que puso Android Studio. Android Studio es un IDE: el editor donde escribes, ejecutas y revisas tu código.
Hasta que escribas
Jugador, el fichero sale en rojo y./gradlew :torneo:testno compila: es normal, los tests piden una clase que todavía no existe.
En ese fichero hay dos comprobaciones que aún no has usado. assertNull(x) pasa si x es null. Y assertFailsWith comprueba que un trozo de código lanza un error; así se usa, con otros datos:
import kotlin.test.Test
import kotlin.test.assertFailsWith
// n!! afirma que n no es null; con null, el programa se para con NullPointerException
fun forzar(n: Int?): Int = n!!
class ForzarTest {
@Test
fun sinNumeroLanza() {
// Lo de entre ángulos, <NullPointerException>, es el tipo de error que esperas (se explica con más detalle en Genéricos); el código va entre llaves.
// Si el bloque lanza ese error, el test pasa; si no lanza nada, el test falla.
assertFailsWith<NullPointerException> { forzar(null) }
}
}Lo que va entre ángulos es una forma de pasar un tipo como parámetro que verás en Genéricos.
Intenta escribirlo tú. Un Jugador se crea con un nombre y un dorsal que puede faltar, los dos como propiedades de solo lectura, y tiene como métodos cuatro de las funciones del ejercicio de Nulabilidad: dorsalOGuion, dorsalForzado, esPortero y longitudDelNombre, ahora sin parámetros, porque usan el nombre y el dorsal del propio objeto. presentar se queda fuera de la clase y recibe un jugador que puede faltar. Al final de main, guarda en una lista (listOf(a, b, c)) a Ana (9), Luis (sin dorsal) y Marta (1), presenta a cada uno con un for y después presenta a nadie. Tras las líneas de Bucles y tu primer test, debe verse:
Ana (#9)
Luis (-)
Marta (#1)
Sin jugadorLos bloques de abajo llevan como título la ruta del fichero en tu proyecto.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
package com.sebastiannarvaez.marcador.torneo
// `Int?` es un Int que puede ser null: en el amateur hay jugadores sin dorsal.
class Jugador(val nombre: String, val dorsal: Int?) {
// `?.let`: el bloque solo corre si el dorsal no es null.
// `?:` (elvis): el valor de reserva cuando lo de la izquierda es null.
fun dorsalOGuion(): String = dorsal?.let { "#$it" } ?: "-"
// `!!` promete «esto no es null». Si mientes, salta NullPointerException.
// Existe para que la lección lea la pila de llamadas; no lo uses en código real.
fun dorsalForzado(): Int = dorsal!!
// Smart cast: tras comprobar `dorsal != null`, el compilador ya lo trata
// como Int (no como Int?) dentro del `if`.
fun esPortero(): Boolean {
if (dorsal != null) {
return dorsal == 1
}
return false
}
// `?.` encadena: si algo es null, todo el resultado es null.
fun longitudDelNombre(): Int? = nombre.takeIf { it.isNotBlank() }?.length
}
// Smart cast sobre un parámetro: `jugador` es Jugador? y se convierte a Jugador.
fun presentar(jugador: Jugador?): String {
if (jugador == null) return "Sin jugador"
return "${jugador.nombre} (${jugador.dorsalOGuion()})"
}En Main.kt, pega esto en lugar de la última } de main (la llave de cierre del final del fichero, que viene después del último println de Bucles y tu primer test): el bloque trae su propia }, así que esa llave se borra y no queda duplicada.
// Nulabilidad: jugadores con y sin dorsal.
val jugadores = listOf(Jugador("Ana", 9), Jugador("Luis", null), Jugador("Marta", 1))
for (jugador in jugadores) println(presentar(jugador))
println(presentar(null))
}El comentario de dorsalForzado se refiere a la pila de llamadas que leíste en el Playground en Nulabilidad. En el proyecto, dorsalForzado, esPortero y longitudDelNombre solo los usan los tests: son la práctica de esa lección, guardada en la clase.
Punto de control
./gradlew :torneo:runtermina conAna (#9),Luis (-),Marta (#1)ySin jugador../gradlew :torneo:testtermina enBUILD SUCCESSFUL. Abre el informe (como en Bucles y tu primer test) y comprueba que pone 11 tests: los 5 dePuntosTesty los 6 deJugadorTest.- Sabes decir, con tus palabras, qué es una clase, qué es un objeto y qué es un método, y por qué
Jugador("Ana", 9)yJugador("Luis", null)son dos objetos del mismo molde.
La siguiente lección, Clases que se validan, añade Equipo: una clase que se niega a existir si sus datos no tienen sentido.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.Con
class Libro(val titulo: String), ¿qué esLibro("Rayuela")?2.¿Qué diferencia hay entre una propiedad y un método?
3.Creas
val a = Libro("Rayuela", 600)yval b = Libro("Diario", 120). Llamas aa.prestar(), que cambia la propiedadprestadode ese objeto. ¿Qué pasa conb.prestado?4.Una clase se declara
class Mascota(val nombre: String, var edad: Int?). ¿Qué puedes hacer con un objetolunade esa clase?5.¿Qué comprueba
assertFailsWith<NullPointerException> { forzar(null) }en un test?