Kotlin: estática que no existe


Este artículo hablará sobre el uso de estática en Kotlin.
Empecemos
Kotlin no tiene estática!

Esto se afirma en la documentación oficial.

Y parece que esto podría terminar el artículo. Pero déjame, ¿cómo es eso? Después de todo, si inserta código Java en un archivo Kotlin en Android Studio, entonces el convertidor inteligente hará la magia, convertirá todo en código en el idioma correcto y ¡funcionará! Pero, ¿qué pasa con la compatibilidad total con Java?

En este punto, cualquier desarrollador, al enterarse de la falta de estática en Kotlin, ingresará a la documentación y los foros para tratar este problema. Reunámonos, pensativa y minuciosamente. Intentaré mantener la menor cantidad de preguntas posible al final de este artículo.

¿Qué es la estática en Java? Hay:
  • campos estáticos de clase
  • métodos de clase estática
  • clases anidadas estáticas


Hagamos un experimento (esto es lo primero que se me viene a la mente).

Crea una clase Java simple:
public class SimpleClassJava1 { public static String staticField = "Hello, static!"; public static void setStaticValue (String value){ staticField = value; } } 

Aquí todo es fácil: en la clase creamos un campo estático y un método estático. Hacemos todo públicamente para experimentos con acceso desde el exterior. Conectamos el campo y el método lógicamente.

Ahora cree una clase de Kotlin vacía e intente copiar todo el contenido de la clase SimpleClassJava1 en ella. Respondemos "sí" a la pregunta resultante sobre la conversión y vemos qué sucedió:

 class SimpleClassKotlin1 { var staticField = "Hello, static!" fun setStaticValue(value: String) { staticField = value } } 

Parece que esto no es exactamente lo que necesitamos ... Para asegurarnos de esto, convertiremos el código de bytes de esta clase a código Java y veremos qué sucedió:
 public final class SimpleClassKotlin1 { @NotNull private String staticField = "Hello, static!"; @NotNull public final String getStaticField() { return this.staticField; } public final void setStaticField(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.staticField = var1; } public final void setStaticValue(@NotNull String value) { Intrinsics.checkParameterIsNotNull(value, "value"); this.staticField = value; } } 

Si Todo es exactamente como parecía. Aquí no huele a estática. El convertidor simplemente cortó el modificador estático en la firma, como si no estuviera allí. Por si acaso, llegaremos inmediatamente a una conclusión: no confíes ciegamente en el convertidor, a veces puede traer sorpresas desagradables.

Por cierto, hace unos seis meses, la conversión del mismo código Java a Kotlin habría mostrado un resultado ligeramente diferente. Así que de nuevo: ¡tenga cuidado con la conversión automática!

Experimentamos más.

Vamos a cualquier clase en Kotlin e intentamos llamar a los elementos estáticos de la clase Java en ella:
 SimpleClassJava1.setStaticValue("hi!") SimpleClassJava1.staticField = "hello!!!" 

¡Aquí está cómo! Todo se llama perfectamente, ¡incluso el autocompletado del código nos dice todo! Bastante curioso

Ahora pasemos a la parte más sustancial. De hecho, los creadores de Kotlin decidieron alejarse de la estática en la forma en que estamos acostumbrados a usarlo. Por qué hicimos eso y no discutiremos lo contrario: hay muchas disputas y opiniones sobre este tema en la red. Solo descubriremos cómo vivir con él. Naturalmente, no solo estábamos privados de estática. Kotlin nos brinda un conjunto de herramientas con las que podemos compensar la pérdida. Son adecuados para uso en interiores. Y la compatibilidad total prometida con el código Java. Vamos!

Lo más rápido y fácil que puede darse cuenta y comenzar a usar es la alternativa que se nos ofrece en lugar de métodos estáticos: funciones de nivel de paquete. Que es esto Esta es una función que no pertenece a ninguna clase. Es decir, este tipo de lógica que está en el vacío en algún lugar del espacio del paquete. Podemos describirlo en cualquier archivo dentro del paquete que nos interese. Por ejemplo, nombre este archivo JustFun.kt y colóquelo en el paquete com.example.mytestapplication
 package com.example.mytestapplication fun testFun(){ // some code } 


Convierta el código de bytes de este archivo en Java y mire dentro:
 public final class JustFunKt { public static final void testFun() { // some code } } 

Vemos que en Java se crea una clase cuyo nombre tiene en cuenta el nombre del archivo en el que se describe la función, y la función en sí misma se convierte en un método estático.

Ahora, si queremos llamar a la función testFun en Kotlin desde una clase (o la misma función) ubicada en el paquete de package com.example.mytestapplication (es decir, el mismo paquete que la función), entonces simplemente podemos acceder a ella sin trucos adicionales. Si lo llamamos desde otro paquete, debemos importar, que nos es familiar y generalmente aplicable a las clases:
 import com.example.pavka.mytestapplication.testFun 

Si hablamos de llamar a la función t estFun desde el código Java, siempre necesitamos importar la función, independientemente del paquete desde el que la llamemos:
 import static com.example.pavka.mytestapplication.ForFunKt.testFun; 

La documentación dice que en la mayoría de los casos, en lugar de métodos estáticos, es suficiente para que usemos funciones de nivel de paquete. Sin embargo, en mi opinión personal (que no tiene que coincidir con la opinión de todos los demás), este método de implementación de estadísticas es adecuado solo para proyectos pequeños.
Resulta que estas funciones no pertenecen explícitamente a ninguna clase. Visualmente, su llamada parece una llamada al método de clase (o su padre) en el que estamos ubicados, lo que a veces puede ser confuso. Bueno, y lo más importante: solo puede haber una función con ese nombre en el paquete. Incluso si intentamos crear la función del mismo nombre en otro archivo, el sistema nos dará un error. Si hablamos de grandes proyectos, con bastante frecuencia tenemos, por ejemplo, diferentes fábricas que tienen métodos estáticos del mismo nombre.

Veamos otras alternativas para implementar métodos y campos estáticos.

Recordemos cuál es el campo estático de una clase. Este es un campo de clase que pertenece a la clase en la que se declara, pero no pertenece a una instancia específica de la clase, es decir, se crea en una sola instancia para toda la clase.

Para estos fines, Kotlin nos ofrece utilizar alguna entidad adicional, que también existe en una sola copia. En otras palabras, singleton.

Kotlin tiene una palabra clave de objeto para declarar singletones.

 object MySingltoneClass { // some code } 


Dichos objetos se inicializan perezosamente, es decir, en el momento de la primera llamada a ellos.

Ok, también hay singletones en Java, ¿dónde están las estadísticas?

Para cualquier clase en Kotlin, podemos crear un compañero u objeto complementario. Un singleton atado a una clase específica. Esto se puede hacer usando 2 palabras clave companion object juntas:

 class SimpleClassKotlin1 { companion object{ var companionField = "Hello!" fun companionFun (vaue: String){ // some code } } } 


Aquí tenemos la clase SimpleClassKotlin1 , dentro de la cual declaramos un singleton con la palabra clave objeto y lo vinculamos al objeto dentro del cual se declara con la palabra clave compañera. Aquí puede prestar atención al hecho de que, a diferencia de la declaración singleton anterior (MySingltoneClass), el nombre de la clase singleton no está indicado. Si el objeto se declara compañero, se le permite no indicar su nombre. Entonces se llamará automáticamente Companion . Si es necesario, podemos obtener una instancia de la clase complementaria de esta manera:
 val companionInstance = SimpleClassKotlin1.Companion 

Sin embargo, una llamada a las propiedades y métodos de una clase complementaria se puede hacer directamente, a través de una llamada a la clase a la que está asociada:
 SimpleClassKotlin1.companionField SimpleClassKotlin1.companionFun("Hi!") 

Ya se parece mucho a llamar a campos y clases estáticos, ¿verdad?

Si es necesario, podemos dar un nombre a la clase compañera, pero en la práctica esto rara vez se hace. De las características interesantes de las clases que lo acompañan, se puede observar que, como cualquier clase ordinaria, puede implementar interfaces, lo que a veces puede ayudarnos a agregar un poco más de orden al código:

 interface FactoryInterface<T> { fun factoryMethod(): T } class SimpleClassKotlin1 { companion object : FactoryInterface<MyClass> { override fun factoryMethod(): MyClass = MyClass() } } 


Una clase complementaria solo puede tener una clase. Sin embargo, nadie nos prohíbe declarar ningún número de objetos singleton dentro de la clase, pero en este caso debemos especificar explícitamente el nombre de esta clase y, en consecuencia, indicar este nombre al referirnos a los campos y al método de esta clase.

Hablando de las clases declaradas como objeto, podemos decir que también podemos declarar objetos anidados en ellas, pero no podemos declarar un objeto complementario en ellas.

Es hora de mirar "debajo del capó". Toma nuestra clase simple:

 class SimpleClassKotlin1 { companion object{ var companionField = "Hello!" fun companionFun (vaue: String){ } } object OneMoreObject { var value = 1 fun function(){ } } 


Ahora descompila su bytecode en Java:
 public final class SimpleClassKotlin1 { @NotNull private static String companionField = "Hello!"; public static final SimpleClassKotlin1.Companion Companion = new SimpleClassKotlin1.Companion((DefaultConstructorMarker)null); public static final class OneMoreObject { private static int value; public static final SimpleClassKotlin1.OneMoreObject INSTANCE; public final int getValue() { return value; } public final void setValue(int var1) { value = var1; } public final void function() { } static { SimpleClassKotlin1.OneMoreObject var0 = new SimpleClassKotlin1.OneMoreObject(); INSTANCE = var0; value = 1; } } public static final class Companion { @NotNull public final String getCompanionField() { return SimpleClassKotlin1.companionField; } public final void setCompanionField(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); SimpleClassKotlin1.companionField = var1; } public final void companionFun(@NotNull String vaue) { Intrinsics.checkParameterIsNotNull(vaue, "vaue"); } private Companion() { } // $FF: synthetic method public Companion(DefaultConstructorMarker $constructor_marker) { this(); } } } 

Miramos lo que pasó.

La propiedad del objeto complementario se representa como un campo estático de nuestra clase:
 private static String companionField = "Hello!"; 


Esto parece ser exactamente lo que queríamos. Sin embargo, este campo es privado y se accede a través del getter y setter de nuestra clase complementaria, que se presenta aquí como una public static final class , y su instancia se presenta como una constante:
 public static final SimpleClassKotlin1.Companion Companion = new SimpleClassKotlin1.Companion((DefaultConstructorMarker)null); 


La función companionFun no se convirtió en el método estático de nuestra clase (probablemente no debería). Seguía siendo la función de un singleton inicializado en la clase SimpleClassKotlin1. Sin embargo, si lo piensas, entonces lógicamente se trata de lo mismo.

Con la clase OneMoreObject situación es muy similar. Vale la pena señalar solo que aquí, a diferencia del compañero, el campo de la clase de valor no se movió a la clase OneMoreObject , sino que permaneció en OneMoreObject , sino que también se volvió estático y recibió el getter y setter generado.

Tratemos de comprender todo lo anterior.
Si queremos implementar campos estáticos o métodos de clase en Kotlin, entonces para esto debemos usar el objeto complementario declarado dentro de esta clase.
Llamar a estos campos y funciones desde Kotlin se verá exactamente igual que llamar a estadísticas en Java. Pero, ¿qué pasa si intentamos llamar a estos campos y funciones en Java?

Autocompletar nos dice que las siguientes llamadas están disponibles:
 SimpleClassKotlin1.Companion.companionFun("hello!"); SimpleClassKotlin1.Companion.setCompanionField("hello!"); SimpleClassKotlin1.Companion.getCompanionField(); 

Es decir, aquí no vamos a indicar directamente el nombre del compañero. En consecuencia, el nombre que se asignó al objeto complementario predeterminado se usa aquí. No es muy conveniente, ¿verdad?

Sin embargo, los creadores de Kotlin hicieron posible que pareciera más familiar en Java. Y hay varias formas de hacer esto.
 @JvmField var companionField = "Hello!" 

Si aplicamos esta anotación al campo companionField de nuestro objeto companionField , al convertir el código de bytes a Java, vemos que el campo estático companionField SimpleClassKotlin1 ya no es privado, sino público, y el captador y configurador para companionField se han ido en la clase Companion estática. Ahora podemos acceder a companionField desde el código Java de la forma habitual.

La segunda forma es especificar un modificador lateinit para las propiedades del lateinit complementario, propiedades con inicialización tardía. No olvide que esto se aplica solo a las propiedades var, y su tipo no debe ser nulo y no debe ser primitivo. Bueno, no te olvides de las reglas para lidiar con tales propiedades.

 lateinit var lateinitField: String 

Y una forma más: podemos declarar la propiedad del objeto complementario una constante especificando el modificador const. Es fácil adivinar que esto debería ser una propiedad val.
 const val myConstant = "CONSTANT" 

En cada uno de estos casos, el código Java generado contendrá el campo estático público habitual, en el caso de const, este campo también será final. Por supuesto, vale la pena entender que cada uno de estos 3 casos tiene su propio propósito lógico, y solo el primero de ellos está diseñado específicamente para facilitar su uso con Java, el resto obtiene este "moño" como si estuviera en una carga.

Debe notarse por separado que el modificador const se puede usar para propiedades de objetos, objetos complementarios y para propiedades del nivel de paquete. En el último caso, obtenemos lo mismo que usar las funciones de nivel de paquete y con las mismas restricciones. El código Java se genera con un campo público estático en la clase, cuyo nombre tiene en cuenta el nombre del archivo en el que describimos la constante. Un paquete solo puede tener una constante con el nombre especificado.

Si queremos que la función del objeto complementario también se convierta en un método estático al generar código Java, entonces para esto debemos aplicar la anotación @JvmStatic a esta función.
También está permitido aplicar la anotación @JvmStatic a las propiedades de los objetos complementarios (y solo a los objetos singleton). En este caso, la propiedad no se convertirá en un campo estático, pero se generará un captador y definidor estático para esta propiedad. Para una mejor comprensión, mira esta clase de Kotlin:
 class SimpleClassKotlin1 { companion object{ @JvmStatic fun companionFun (vaue: String){ } @JvmStatic var staticField = 1 } } 


En este caso, las siguientes llamadas son válidas desde Java:
 int x; SimpleClassKotlin1.companionFun("hello!"); x = SimpleClassKotlin1.getStaticField(); SimpleClassKotlin1.setStaticField(10); SimpleClassKotlin1.Companion.companionFun("hello"); x = SimpleClassKotlin1.Companion.getStaticField(); SimpleClassKotlin1.Companion.setStaticField(10); 


Las siguientes llamadas son válidas desde Kotlin:
 SimpleClassKotlin1.companionFun("hello!") SimpleClassKotlin1.staticField SimpleClassKotlin1.Companion.companionFun("hello!") SimpleClassKotlin1.Companion.staticField 


Está claro que para Java debe usar los primeros 3 y para Kotlin los primeros 2. El resto de las llamadas son válidas.

Ahora queda por aclarar lo último. ¿Qué pasa con las clases anidadas estáticas? Aquí todo es simple: el análogo de tal clase en Kotlin es una clase anidada regular sin modificadores:
 class SimpleClassKotlin1 { class LooksLikeNestedStatic { } } 


Después de convertir el código de bytes a Java, vemos:
 public final class SimpleClassKotlin1 { public static final class LooksLikeNestedStatic { } } 


De hecho, esto es lo que necesitamos. Si no queremos que la clase sea final, en el código de Kotlin especificamos el modificador abierto para ella. Lo recordaba por si acaso.

Creo que puedes resumir. De hecho, en Kotlin, como se dijo, no hay estática en la forma en que estamos acostumbrados a verlo. Pero el conjunto de herramientas propuesto nos permite implementar todo tipo de estadísticas en el código Java generado. También se proporciona compatibilidad total con Java, y podemos llamar directamente a Kotlin a campos estáticos y métodos de clases Java.
En la mayoría de los casos, implementar una estadística en Kotlin requiere algunas líneas de código más. Quizás este sea uno de los pocos, o quizás el único caso en el que necesite escribir más en Kotlin. Sin embargo, te acostumbras rápidamente.
Creo que en los proyectos donde se comparten el código de Kotlin y Java, puede acercarse de manera flexible a la elección del idioma utilizado. Por ejemplo, me parece que Java es más adecuado para almacenar constantes. Pero aquí, como en muchas otras cosas, vale la pena guiarse por el sentido común y las reglas para escribir código en el proyecto.

Y al final del artículo, aquí hay tal información. Quizás en el futuro, Kotlin seguirá teniendo un modificador estático que elimina muchos problemas y facilita la vida de los desarrolladores, y el código es más corto. Hice esta suposición al encontrar el texto apropiado en el párrafo 17 de las descripciones de las características de Kotlin .
Es cierto que este documento data de mayo de 2017, y en el patio ya es finales de 2018.

Eso es todo para mí. Creo que el tema se resolvió con cierto detalle. Las preguntas escriben en los comentarios.

Source: https://habr.com/ru/post/es430836/


All Articles