Saltar al contenido
K
Kotlin: el lenguajePaso a paso35 min de lectura · 5 min de máquina Básico

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í:

PalabraQué esEn el ejemplo de abajo
claseel molde: dice qué datos tiene una cosa y qué sabe hacerLibro
objetouna cosa concreta hecha con ese molde (también se le llama instancia: cada objeto creado desde una clase)el libro «Rayuela»
propiedadun dato del objetotitulo, paginas
métodouna función que vive dentro de la clase y usa los datos del objetoesLargo()
constructorla receta para crear un objeto: qué datos hay que darlelos 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.

ejemplo mínimo: una clase y sus objetos (con comentarios)
// 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
false

Dos detalles de esa clase:

  • val o var dentro de los paréntesis convierte el dato en una propiedad del objeto. val es de solo lectura (como una variable val: no se puede reasignar) y var se puede cambiar. Si en el ejemplo escribes rayuela.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, paginas y prestado se 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.

ejemplo mínimo: un parámetro sin val ni var (con comentarios)
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
}
3

Si 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:

ejemplo mínimo: el mismo parámetro con val (con comentarios)
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
}
Ana

En 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.

ejemplo mínimo: de funciones sueltas a una clase (con comentarios)
// 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
Diario

Un ú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.

ejemplo mínimo: comparar un nulable con == (con comentarios)
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
false

Reto · 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
false
Ver solución
solución del ejercicio
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
false

La 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().

torneo/
kotlin+java/
com.sebastiannarvaez.marcador.torneo
Jugador.ktse crea
Main.ktse modifica
com.sebastiannarvaez.marcador.torneo (test)
JugadorTest.ktse crea y se rellena con «Copiar fichero»
Lo que tocas, en la vista Android de :torneo.
  1. Crea Jugador.kt: clic derecho sobre el paquete com.sebastiannarvaez.marcador.torneo (el que no lleva (test)) → New → Kotlin Class/File → Class → Jugador. Android Studio escribe un esqueleto class Jugador {}: bórralo, porque lo sustituyes por tu clase.

  2. Crea JugadorTest.kt en 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 de PuntosTest. El fichero nace casi vacío y se rellena así:

    1. 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.
    2. Haz clic dentro de JugadorTest.kt, selecciona todo (Cmd+A) y pega (Cmd+V). Lo pegado ya trae su propia línea package, 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:test no compila: es normal, los tests piden una clase que todavía no existe.

JugadorTest.kt

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:

ejemplo mínimo: comprobar que algo lanza un error (con comentarios)
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 jugador

Los 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
torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Jugador.kt
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.

torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Main.kt
    // 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:run termina con Ana (#9), Luis (-), Marta (#1) y Sin jugador.
  • ./gradlew :torneo:test termina en BUILD SUCCESSFUL. Abre el informe (como en Bucles y tu primer test) y comprueba que pone 11 tests: los 5 de PuntosTest y los 6 de JugadorTest.
  • 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) y Jugador("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. 1.Con class Libro(val titulo: String), ¿qué es Libro("Rayuela")?

  2. 2.¿Qué diferencia hay entre una propiedad y un método?

  3. 3.Creas val a = Libro("Rayuela", 600) y val b = Libro("Diario", 120). Llamas a a.prestar(), que cambia la propiedad prestado de ese objeto. ¿Qué pasa con b.prestado?

  4. 4.Una clase se declara class Mascota(val nombre: String, var edad: Int?). ¿Qué puedes hacer con un objeto luna de esa clase?

  5. 5.¿Qué comprueba assertFailsWith<NullPointerException> { forzar(null) } en un test?

Faltan 5 de 5