Advertisement
  1. Code
  2. Kotlin

Kotlin Desde Cero: Propiedades y Clases Avanzadas

Scroll to top
Read Time: 16 min
This post is part of a series called Kotlin From Scratch.
Kotlin From Scratch: Classes and Objects
Kotlin From Scratch: Abstract Classes, Interfaces, Inheritance, and Type Alias

() translation by (you can also view the original English article)

Kotlin es un lenguaje de programación moderno que compila en código de bytes Java. Es gratuito y de código abierto, y promete hacer que la codificación para Android sea aún más divertida.

En el artículo anterior, aprendiste sobre clases y objetos en Kotlin. En este tutorial, continuaremos aprendiendo más sobre las propiedades y también buscaremos los tipos avanzados de clases en Kotlin explorando lo siguiente:

  • propiedades inicializadas en tiempo de desarrollo
  • propiedades en línea
  • propiedades de extensión
  • clases data, enum, anidadas y selladas

1. Propiedades inicializadas en tiempo de tiempo

Podemos declarar una propiedad no nula en Kotlin como inicializada en tiempo de desarrollo. Esto significa que una propiedad no nula no se inicializará en el momento de la declaración con un valor (la inicialización real no se producirá a través de ningún constructor), sino que se inicializará tarde mediante un método o una inserción de dependencias.

Veamos un ejemplo para entender este modificador de propiedad única.

1
class Presenter {
2
    private var repository: Repository? = null
3
    
4
    fun initRepository(repo: Repository): Unit {
5
        this.repository = repo
6
    }
7
}
8
9
class Repository {
10
    fun saveAmount(amount: Double) {}
11
} 

En el código anterior, declaramos una propiedad de repository que acepta valores nulo que es de tipo Repository dentro de la clase Presenter, y luego inicializamos esta propiedad en nulo durante la declaración. Tenemos un método initRepository() en la clase Presenter que reinicializa esta propiedad más adelante con una instancia de Repository real. Tenga en cuenta que a esta propiedad también se le puede asignar un valor mediante un inyector de dependencias como Dagger.

Ahora, para que podamos invocar métodos o propiedades en esta propiedad de repository , tenemos que hacer una comprobación nula o usar el operador de llamada segura. ¿por qué? Dado que la propiedad del repository es de tipo que acepta valores nulo (¿Repository?). (Si necesita un repaso sobre la nulabilidad en Kotlin, visite La nulabilidad, bucles y condiciones).

1
// Inside Presenter class

2
fun save(amount: Double) {
3
    repository?.saveAmount(amount)
4
}

Para evitar tener que hacer comprobaciones nulas cada vez que necesitemos invocar el método de una propiedad, podemos marcar esa propiedad con el modificador lateinit, esto significa que hemos declarado esa propiedad (que es una instancia de otra clase) como inicializada en tiempo de ejecución (lo que significa que la propiedad se inicializará más adelante).

1
class Presenter {
2
3
    private lateinit var repository: Repository
4
    //...

5
}

Ahora, siempre que esperemos hasta que la propiedad haya recibido un valor, estamos seguros de tener acceso a los métodos de la propiedad sin realizar ninguna comprobación nula. La inicialización de la propiedad puede producirse en un método establecedor o mediante la inserción de dependencias.

1
repository.saveAmount(amount)

Tenga en cuenta que si intentamos acceder a los métodos de la propiedad antes de que se haya inicializado, obtendremos un kotlin. UninitializedPropertyAccessException en lugar de NullPointerException. En este caso, el mensaje de excepción será "lateinit property repository has not been initialized".

Tenga en cuenta también las siguientes restricciones impuestas al retrasar la inicialización de una propiedad con lateinit:

  • Debe ser mutable (declarado con var).
  • El tipo de propiedad no puede ser un tipo primitivo, por ejemplo, Int, Double, Float, etc.
  • La propiedad no puede tener un captador o establecedor personalizado.

2. Propiedades en línea

En Funciones avanzadas, introduje el modificador inline para las funciones de orden superior, lo que ayuda a optimizar las funciones de orden superior que aceptan una expresión lambda como parámetro.

En Kotlin, también podemos usar este modificador inline en las propiedades. El uso de este modificador optimizará el acceso a la propiedad.

Veamos un ejemplo práctico.

1
class Student {
2
    val nickName: String
3
        get() {
4
            println("Nick name retrieved")
5
            return "koloCoder"
6
        }
7
}
8
9
10
fun main(args: Array<String>) {
11
    val student = Student()
12
    print(student.nickName)
13
}

En el código anterior, tenemos una propiedad normal, nickName, que no tiene el modificador inline. Si descompilar el fragmento de código, usando la función Mostrar código de bytes de Kotlin (si estás en IntelliJ IDEA o Android Studio, usa Herramientas > Kotlin > Mostrar código de bytes de Kotlin), veremos el siguiente código Java:

1
public final class Student {
2
   @NotNull
3
   public final String getNickName() {
4
      String var1 = "Nick name retrieved";
5
      System.out.println(var1);
6
      return "koloCoder";
7
   }
8
}
9
10
public final class InlineFunctionKt {
11
   public static final void main(@NotNull String[] args) {
12
      Intrinsics.checkParameterIsNotNull(args, "args");
13
      Student student = new Student();
14
      String var2 = student.getNickName();
15
      System.out.print(var2);
16
   }
17
}

En el código Java generado anteriormente (algunos elementos del código generado se eliminaron por razones de brevedad), puede ver que dentro del método main() el compilador creó un objeto Student, llamado método getNickName(), y luego imprimió su valor devuelto.

Ahora vamos a especificar la propiedad como inline en su lugar y comparar el código de bytes generado.

1
// ...

2
inline val nickName: String
3
// ... 

Simplemente insertamos el modificador inline antes del modificador variable: var o val. Este es el código de bytes generado para esta propiedad en línea:

1
// ... 

2
public static final void main(@NotNull String[] args) {
3
  Intrinsics.checkParameterIsNotNull(args, "args");
4
  Student student = new Student();
5
  String var3 = "Nick name retrieved";
6
  System.out.println(var3);
7
  String var2 = "koloCoder";
8
  System.out.print(var2);
9
}
10
// ... 

De nuevo se eliminó parte del código, pero la clave a tener en cuenta es el método main(). El compilador ha copiado el cuerpo de la función get() de la propiedad y lo ha pegado en el sitio de llamada (este mecanismo es similar a las funciones insertadas).

Nuestro código se ha optimizado debido a que no es necesario crear un objeto y llamar al método del captador de propiedades. Pero, como se describe en la publicación de funciones en línea, tendríamos un código de bytes más grande que antes, así que úsalo con precaución.

Tenga en cuenta también que este mecanismo funcionará para las propiedades que no tienen un campo de respaldo (recuerde, un campo de respaldo es simplemente un campo que usan las propiedades cuando desea modificar o usar esos datos de campo).

3. Propiedades de extensión

En Funciones avanzadas también hablé de las funciones de extensión, que nos dan la capacidad de extender una clase con nueva funcionalidad sin tener que heredar de esa clase. Kotlin también proporciona un mecanismo similar para las propiedades, denominado propiedades de extensión.

1
val String.upperCaseFirstLetter: String
2
    get() = this.substring(0, 1).toUpperCase().plus(this.substring(1))

En la publicación Funciones avanzadas definimos una función de extensión uppercaseFirstLetter() con el tipo de receptor String. Aquí, lo hemos convertido en una propiedad de extensión de nivel superior en su lugar. Tenga en cuenta que tiene que definir un método de captador en su propiedad para que esto funcione.

Por lo tanto, con este nuevo conocimiento sobre las propiedades de extensión, sabrá que si alguna vez ha deseado que una clase tenga una propiedad que no estaba disponible, puede crear una propiedad de extensión de esa clase.

4. Clases de datos

Comencemos con una clase Java típica o POJO (Plain Old Java Object).

1
public class BlogPost {
2
    private final String title;
3
    private final URI url;
4
    private final String description;
5
    private final Date publishDate;
6
7
    //.. constructor not included for brevity's sake

8
9
    @Override
10
    public boolean equals(Object o) {
11
        if (this == o) return true;
12
        if (o == null || getClass() != o.getClass()) return false;
13
14
        BlogPost blogPost = (BlogPost) o;
15
16
        if (title != null ? !title.equals(blogPost.title) : blogPost.title != null) return false;
17
        if (url != null ? !url.equals(blogPost.url) : blogPost.url != null) return false;
18
        if (description != null ? !description.equals(blogPost.description) : blogPost.description != null)
19
            return false;
20
        return publishDate != null ? publishDate.equals(blogPost.publishDate) : blogPost.publishDate == null;
21
    }
22
23
    @Override
24
    public int hashCode() {
25
        int result = title != null ? title.hashCode() : 0;
26
        result = 31 * result + (url != null ? url.hashCode() : 0);
27
        result = 31 * result + (description != null ? description.hashCode() : 0);
28
        result = 31 * result + (publishDate != null ? publishDate.hashCode() : 0);
29
        return result;
30
    }
31
32
    @Override
33
    public String toString() {
34
        return "BlogPost{" +
35
                "title='" + title + '\'' +
36
                ", url=" + url +
37
                ", description='" + description + '\'' +
38
                ", publishDate=" + publishDate +
39
                '}';
40
    }
41
    //.. setters and getters also ignored for brevity's sake

42
}

Como puede ver, necesitamos codificar explícitamente los descriptores de acceso de propiedad de clase: el captador y el establecedor, así como el hashcode, equals y los métodos toString (aunque IntelliJ IDEA, Android Studio o la biblioteca AutoValue pueden ayudarnos a generarlos). Vemos este tipo de código reutilizable principalmente en la capa de datos de un proyecto Java típico. (Quité los descriptores de acceso de campo y el constructor por razones de brevedad).

Lo bueno es que el equipo de Kotlin nos proporcionó el modificador de data para las clases para eliminar la escritura de estos repetitivos.

Ahora vamos a escribir el código anterior en Kotlin en su lugar.

1
data class BlogPost(var title: String, var url: URI, var description: String, var publishDate: Date)

¡impresionante! Simplemente especificamos el modificador de data antes de la palabra clave class para crear una clase de datos, al igual que lo que hicimos en nuestra clase BlogPost Kotlin anterior. Ahora los métodos equals, hashcode, toString, copy y varios componentes se crearán bajo el capó para nosotros. Tenga en cuenta que una clase de datos puede extender otras clases (esta es una nueva característica de Kotlin 1.1).

The método equals

Este método compara la igualdad de dos objetos y devuelve true si son iguales o false en caso contrario. En otras palabras, compara si las dos instancias de clase contienen los mismos datos.

1
student.equals(student3)
2
// using the == in Kotlin

3
student == student3 // same as using equals()

En Kotlin, el uso del operador de igualdad == llamará al método equals en segundo plano.

El método hashCode

Este método devuelve un valor entero utilizado para el almacenamiento y la recuperación rápidos de los datos almacenados en una estructura de datos de colección basada en hash, por ejemplo en los tipos de colección HashMap y HashSet.

El método toString

Este método devuelve una representación string de un objeto.

1
data class Person(var firstName: String, var lastName: String)
2
3
val person = Person("Chike", "Mgbemena")
4
println(person) // prints "Person(firstName=Chike, lastName=Mgbemena)"

Simplemente llamando a la instancia de clase, obtenemos un objeto de cadena devuelto a nosotros: Kotlin llama al objeto toString() bajo el capó para nosotros. Pero si no ponemos la palabra clave data, vea cuál sería nuestra representación de cadena de objeto:

1
com.chike.kotlin.classes.Person@2f0e140b

¡Mucho menos informativo!

El método copy

Este método nos permite crear una nueva instancia de un objeto con los mismos valores de propiedad. En otras palabras, crea una copia del objeto.

1
val person1 = Person("Chike", "Mgbemena")
2
println(person1) // Person(firstName=Chike, lastName=Mgbemena)

3
val person2 = person1.copy()
4
println(person2) // Person(firstName=Chike, lastName=Mgbemena)

Una cosa interesante sobre el método de copy en Kotlin es la capacidad de cambiar las propiedades durante la copia.

1
val person3 = person1.copy(lastName = "Onu")
2
println(person3) //Person3(firstName=Chike, lastName=Onu)

Si eres un codificador de Java, este método es similar al método clone() con el que ya estás familiarizado. Pero el método de copy kotlin tiene características más potentes.

Declaración destructiva

En la clase Person, también tenemos dos métodos generados automáticamente para nosotros por el compilador debido a la palabra clave data colocada en la clase. Estos dos métodos llevan el prefijo "component", seguido de un sufijo numérico: component1(), component2(). Cada uno de estos métodos representa las propiedades individuales del tipo. Tenga en cuenta que el sufijo corresponde al orden de las propiedades declaradas en el constructor principal.

Por lo tanto, en nuestro ejemplo, llamar a component1() devolverá el nombre y llamar a component2() devolverá el apellido.

1
println(person3.component1()) // Chike

2
println(person3.component2()) // Onu

Sin embargo, llamar a las propiedades con este estilo es difícil de entender y leer, por lo que llamar al nombre de la propiedad explícitamente es mucho mejor. Sin embargo, estas propiedades creadas implícitamente tienen un propósito muy útil: nos permiten hacer una declaración de desestructuración, en la que podemos asignar cada componente a una variable local.

1
val (firstName, lastName) = Person("Angelina", "Jolie")
2
println(firstName + " " + lastName) // Angelina Jolie

Lo que hemos hecho aquí es asignar directamente la primera y la segunda propiedad (firstName y lastName) del tipo Person a las variables firstName y lastName respectivamente. También hablé de este mecanismo conocido como declaración de desestructuración en la última sección de la publicación Paquetes y funciones básicas.

5. Clases anidadas

En la publicación Más diversión con funciones, le dije que Kotlin tiene soporte para funciones locales o anidadas, una función que se declara dentro de otra función. Bueno, Kotlin también admite de manera similar clases anidadas, una clase creada dentro de otra clase.

1
class OuterClass {
2
3
    class NestedClass {
4
        fun nestedClassFunc() { }
5
    }
6
}

Incluso llamamos a las funciones públicas de la clase anidada como se ve a continuación: una clase anidada en Kotlin es equivalente a una clase anidada static en Java. Tenga en cuenta que las clases anidadas no pueden almacenar una referencia a su clase externa.

1
val nestedClass = OuterClass.NestedClass()
2
nestedClass.nestedClassFunc()

También somos libres de establecer la clase anidada como privada, esto significa que solo podemos crear una instancia de NestedClass dentro del ámbito de OuterClass.

Clase interna

Las clases internas, por otro lado, pueden hacer referencia a la clase externa en la que se declaró. Para crear una clase interna, colocamos la palabra clave inner antes de la palabra clave class en una clase anidada.

1
class OuterClass() {
2
    val oCPropt: String = "Yo"
3
4
    inner class InnerClass {
5
        
6
        fun innerClassFunc() {
7
            val outerClass = this@OuterClass
8
            print(outerClass.oCPropt) // prints "Yo"

9
        }
10
    }
11
}

Aquí hacemos referencia a OuterClass desde InnerClass mediante this@OuterClass.

6. Clases de enumeración

Un tipo de enumeración declara un conjunto de constantes representadas por identificadores. Este tipo especial de clase se crea mediante la palabra clave enum que se especifica antes de la palabra clave class.

1
enum class Country {
2
    NIGERIA,
3
    GHANA,
4
    CANADA
5
}

Para recuperar un valor de enumeración basado en su nombre (al igual que en Java), hacemos esto:

1
Country.valueOf("NIGERIA")

O podemos usar el método auxiliar Kotlin enumValueOf<T>() para acceder a constantes de una manera genérica:</T>

1
enumValueOf<Country>("NIGERIA")

Además, podemos obtener todos los valores (como para una enumeración de Java) como este:

1
Country.values()

Finalmente, podemos usar el método auxiliar Kotlin enumValues<T>() para obtener todas las entradas de enumeración de una manera genérica:</T>

1
enumValues<Country>()

Esto devuelve una matriz que contiene las entradas de enumeración.

Contructores Enum

Al igual que una clase normal, el tipo enum puede tener su propio constructor con propiedades asociadas a cada constante de enumeración.

1
enum class Country(val callingCode: Int) {
2
    NIGERIA (234),
3
    USA (1),
4
    GHANA (233)
5
}

En el constructor principal de tipo de enumeración Country, definimos la propiedad inmutable callingCodes para cada constante de enumeración. En cada una de las constantes, pasamos un argumento al constructor.

Entonces podemos acceder a la propiedad constants de esta manera:

1
val country = Country.NIGERIA
2
print(country.callingCode) // 234

7. Clases selladas

Una clase sellada en Kotlin es una clase abstracta (nunca se pretende crear objetos a partir de ella) que otras clases pueden extender. Estas subclases se definen dentro del cuerpo de la clase sellada, en el mismo archivo. Debido a que todas estas subclases se definen dentro del cuerpo de la clase sellada, podemos conocer todas las subclases posibles simplemente viendo el archivo.

Veamos un ejemplo práctico.

1
// shape.kt

2
3
sealed class Shape
4
5
class Circle : Shape()
6
class Triangle : Shape()
7
class Rectangle: Shape()

Para declarar una clase como sealed, insertamos el modificador sealed antes del modificador class en el encabezado de declaración de clase, en nuestro caso, declaramos la clase Shape como sealed. Una clase sellada está incompleta sin sus subclases, al igual que una clase abstracta típica, por lo que tenemos que declarar las subclases individuales dentro del mismo archivo (shape.kt en este caso). Tenga en cuenta que no puede definir una subclase de una clase sellada a partir de otro archivo.

En nuestro código anterior, hemos especificado que la clase Shape solo se puede extender mediante las clases Circle, Triangle y Rectangle.

Las clases selladas en Kotlin tienen las siguientes reglas adicionales:

  • Podemos agregar el modificador abstract a una clase sellada, pero esto es redundante porque las clases selladas son abstractas de forma predeterminada.
  • Las clases sealed no pueden tener el modificador open o final.
  • También somos libres de declarar clases de datos y objetos como subclases a una clase sellada (todavía necesitan ser declarados en el mismo archivo).
  • No se permite que las clases selladas tengan constructores públicos, sus constructores son privados de forma predeterminada.

Las clases que extienden subclases de una clase sellada se pueden colocar en el mismo archivo o en otro archivo. La subclase de clase sellada tiene que estar marcada con el modificador open (aprenderás más sobre la herencia en Kotlin en la siguiente publicación).

1
// employee.kt

2
sealed class Employee
3
open class Artist : Employee()
4
5
// musician.kt

6
class Musician : Artist()

Una clase sellada y sus subclases son realmente útiles en una expresión when. por ejemplo:

1
fun whatIsIt(shape: Shape) = when (shape) {
2
    is Circle -> println("A circle")
3
    is Triangle -> println("A triangle")
4
    is Rectangle -> println("A rectangle")
5
}

Aquí el compilador es inteligente para asegurarse de que cubrimos todo lo posible when los casos. Esto significa que no hay necesidad de añadir la cláusula else.

Si en su lugar hiciéramos lo siguiente:

1
fun whatIsIt(shape: Shape) = when (shape) {
2
    is Circle -> println("A circle")
3
    is Triangle -> println("A triangle")
4
}

El código no se compilaría, porque no hemos incluido todos los casos posibles. Tendríamos el siguiente error:

1
Kotlin: 'when' expression must be exhaustive, add necessary 'is Rectangle' branch or 'else' branch instead.

Por lo tanto, podríamos incluir el caso is Rectangle o incluir la cláusula else para completar la expresión when.

conclusión

En este tutorial, aprendiste más sobre las clases en Kotlin. Tratamos lo siguiente sobre las propiedades de clase:

  • inicialización tardía
  • propiedades en línea
  • propiedades de extensión

Además, aprendió acerca de algunas clases interesantes y avanzadas, como datos, enumeración, clases anidadas y selladas. En el siguiente tutorial de la serie Kotlin From Scratch, se te presentarán las interfaces y la herencia en Kotlin. ¡Nos vemos luego!

Para obtener más información sobre el idioma Kotlin, recomiendo visitar la documentación de Kotlin. O echa un vistazo a algunos de nuestros otros mensajes de desarrollo de aplicaciones para Android aquí en Envato Tuts!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.