
En mayo de 2017, Google anunció que Kotlin se había convertido en el lenguaje de desarrollo oficial para Android. Luego, alguien escuchó el nombre de este idioma por primera vez, alguien escribió sobre él durante mucho tiempo, pero a partir de ese momento quedó claro que todos los que están cerca del desarrollo de Android ahora simplemente están obligados a conocerlo. Esto fue seguido por respuestas entusiastas "¡Finalmente!" Y terrible indignación "¿Por qué necesitamos un nuevo idioma?" ¿Qué no hizo Java, por favor? etc. etc.
Ha pasado suficiente tiempo desde entonces, y aunque el debate sobre si Kotlin es bueno o malo todavía no ha disminuido, cada vez se escribe más código para Android. E incluso los desarrolladores bastante conservadores también se están cambiando. Además, en la red puede tropezar con la información de que la velocidad de desarrollo después de dominar este lenguaje aumenta en un 30% en comparación con Java.
Hoy, Kotlin ya ha logrado recuperarse de varias enfermedades infantiles, con muchas preguntas y respuestas sobre Stack Overflow. A simple vista, tanto sus ventajas como sus debilidades se hicieron visibles.
Y en esta ola, se me ocurrió la idea de analizar en detalle los elementos individuales de un lenguaje joven pero popular. Preste atención a puntos complejos y compárelos con Java para mayor claridad y mejor comprensión. Para entender la pregunta un poco más profundo que esto se puede hacer leyendo la documentación. Si este artículo despierta interés, lo más probable es que sentará las bases para toda una serie de artículos. Mientras tanto, comenzaré con cosas bastante básicas, que, sin embargo, esconden muchas trampas. Hablemos de constructores e inicializadores en Kotlin.
Como en Java, en Kotlin, la creación de nuevos objetos, entidades de cierto tipo, se produce llamando al constructor de la clase. También puede pasar argumentos al constructor, y puede haber varios constructores. Si observa este proceso desde afuera, la única diferencia con respecto a Java es la falta de la nueva palabra clave cuando se llama al constructor. Ahora eche un vistazo más profundo y vea qué sucede dentro de la clase.
Una clase puede tener constructores primarios y secundarios.
Un constructor se declara utilizando la palabra clave constructor. Si el constructor principal no tiene modificadores de acceso y anotaciones, se puede omitir la palabra clave.
Una clase puede no tener constructores declarados explícitamente. En este caso, después de la declaración de la clase no hay construcciones, procedemos inmediatamente al cuerpo de la clase. Si dibujamos una analogía con Java, esto es equivalente a la ausencia de una declaración explícita de constructores, como resultado de lo cual el constructor predeterminado (sin parámetros) se generará automáticamente en la etapa de compilación. Se ve como se esperaba:
class MyClassA
Esto es equivalente a la siguiente entrada:
class MyClassA constructor()
Pero si escribe de esta manera, se le pedirá cortésmente que elimine el constructor primario sin parámetros.
El constructor primario es el que siempre se llama cuando se crea un objeto en caso de que exista. Si bien tenemos esto en cuenta y lo analizaremos con más detalle más adelante, cuando pasemos a los constructores secundarios. Por consiguiente, recordamos que si no hay constructores en absoluto, entonces de hecho hay uno (primario), pero no lo vemos.
Si, por ejemplo, queremos que el constructor primario sin parámetros no tenga acceso público, entonces junto con la modificación
private
tendremos que declararlo explícitamente con la palabra clave del
constructor
.
La característica principal del constructor primario es que no tiene cuerpo, es decir no puede contener código ejecutable. Simplemente toma parámetros en sí mismo y los pasa a la clase para su uso futuro. En el nivel de sintaxis, se ve así:
class MyClassA constructor(param1: String, param2: Int, param3: Boolean)
Los parámetros pasados de esta manera se pueden usar para varias inicializaciones, pero no más. En su forma pura, no podemos usar estos argumentos en el código de trabajo de la clase. Sin embargo, podemos inicializar los campos de la clase aquí mismo. Se ve así:
class MyClassA constructor(val param1: String, var param2: Int, param3: Boolean)
Aquí,
param1
y
param2
se pueden usar en el código como campos de la clase, que es equivalente a lo siguiente:
class MyClassA constructor(p1: String, p2: Int, param3: Boolean)
Bueno, si compara con Java, entonces se vería así (y, por cierto, en este ejemplo puede evaluar cuánto Kotlin puede reducir la cantidad de código):
public class MyClassAJava { private final String param1; private Integer param2; public MyClassAJava(String p1, Integer p2, Boolean param3) { this.param1 = p1; this.param2 = p2; } public String getParam1() { return param1; } public Integer getParam2() { return param2; } public void setParam2(final Integer param2) { this.param2 = param2; }
Hablemos de diseñadores adicionales. Son más reminiscentes de los constructores ordinarios en Java: aceptan parámetros y pueden tener un bloque ejecutable. Al declarar constructores adicionales, se requiere la palabra clave constructor. Como se mencionó anteriormente, a pesar de la posibilidad de crear un objeto llamando a un constructor adicional, el constructor primario (si lo hay) también debería llamarse con la ayuda de
this
. En el nivel de sintaxis, esto se organiza de la siguiente manera:
class MyClassA(val p1: String) { constructor(p1: String, p2: Int, p3: Boolean) : this(p1) {
Es decir el constructor adicional es, por así decirlo, el heredero del primario.
Ahora, si creamos un objeto llamando a un constructor adicional, sucederá lo siguiente:
llamar a un constructor adicional;
llamar al constructor principal;
inicialización de un campo de clase
p1
en el constructor principal;
ejecución de código en el cuerpo de un constructor adicional.
Esto es similar a tal construcción en Java:
class MyClassAJava { private final String param1; public MyClassAJava(String p1) { param1 = p1; } public MyClassAJava(String p1, Integer p2, Boolean param3) { this(p1);
Recuerde que en Java podemos llamar a un constructor desde otro utilizando la
this
solo al comienzo del cuerpo del constructor. En Kotlin, este problema se decidió radicalmente: hicieron que esa llamada formara parte de la firma del constructor. Por si acaso, noto que está prohibido llamar a cualquier constructor (primario o adicional) directamente desde el cuerpo del adicional.
Un constructor adicional siempre debe referirse al principal (si lo hay), pero puede hacerlo indirectamente, refiriéndose a otro constructor adicional. La conclusión es que al final de la cadena todavía llegamos a lo principal. La activación de los constructores obviamente ocurrirá en el orden inverso de los diseñadores que recurren entre sí:
class MyClassA(p1: String) constructor(p1: String, p2: Int, p3: Boolean, p4: String) : this(p1, p2, p3)
Ahora la secuencia es:
- llamar a un constructor adicional con 4 parámetros;
- llamar a un constructor adicional con 3 parámetros;
- llamar al constructor primario;
- inicialización de un campo de clase p1 en el constructor primario;
- ejecución de código en el cuerpo del constructor con 3 parámetros;
- ejecución de código en el cuerpo del constructor con 4 parámetros.
En cualquier caso, el compilador nunca nos dejará olvidar llegar al constructor primario.
Sucede que una clase no tiene un constructor primario, mientras que puede tener uno o más adicionales. Entonces los constructores adicionales no están obligados a referirse a alguien, pero también pueden referirse a otros constructores adicionales de esta clase. Anteriormente, descubrimos que el constructor principal, no especificado explícitamente, se genera automáticamente, pero esto se aplica a los casos en que no hay ningún constructor en la clase. Si hay al menos un constructor adicional, no se crea un constructor primario sin parámetros:
class MyClassA {
Podemos crear un objeto de clase llamando a:
val myClassA = MyClassA()
En este caso:
class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) {
Podemos crear un objeto solo con esta llamada:
val myClassA = MyClassA(“some string”, 10, True)
No hay nada nuevo en Kotlin en comparación con Java.
Por cierto, al igual que el constructor primario, el constructor adicional puede no tener un cuerpo si su tarea es solo pasar parámetros a otros constructores.
class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) : this(p1, p2, p3, "") constructor(p1: String, p2: Int, p3: Boolean, p4: String) {
También vale la pena prestar atención al hecho de que, a diferencia del constructor primario, la inicialización de los campos de clase en la lista de argumentos del constructor adicional está prohibida.
Es decir dicho registro será inválido:
class MyClassA { constructor(val p1: String, var p2: Int, p3: Boolean){
Por separado, vale la pena señalar que el constructor adicional, como el primario, puede estar sin parámetros:
class MyClassA { constructor(){
Hablando de constructores, uno no puede dejar de mencionar una de las características convenientes de Kotlin: la capacidad de asignar valores predeterminados para argumentos.
Ahora supongamos que tenemos una clase con varios constructores que tienen un número diferente de argumentos. Daré un ejemplo en Java:
public class MyClassAJava { private String param1; private Integer param2; private boolean param3; private int param4; public MyClassAJava(String p1) { this (p1, 5); } public MyClassAJava(String p1, Integer p2) { this (p1, p2, true); } public MyClassAJava(String p1, Integer p2, boolean p3) { this(p1, p2, p3, 20); } public MyClassAJava(String p1, Integer p2, boolean p3, int p4) { this.param1 = p1; this.param2 = p2; this.param3 = p3; this.param4 = p4; }
Como muestra la práctica, tales diseños son bastante comunes. Veamos cómo se puede escribir lo mismo en Kotlin:
class MyClassA (var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){
Ahora, démosle una palmada a Kotlin por cuánto cortó el código. Por cierto, además de reducir el número de líneas, obtenemos más orden. Recuerde, debe haber visto algo como esto más de una vez:
public MyClassAJava(String p1, Integer p2, boolean p3) { this(p3, p1, p2, 20); } public MyClassAJava(boolean p1, String p2, Integer p3, int p4) {
Cuando vea esto, desea encontrar a la persona que lo escribió, tomarlo con un botón, llevarlo a la pantalla y preguntar con voz triste: "¿Por qué?"
Aunque puedes repetir esta hazaña en Kotlin, pero no es necesario.
Sin embargo, hay un detalle que, en el caso de una notación tan abreviada en Kotlin, es necesario tener en cuenta: si queremos llamar al constructor con valores predeterminados de Java, debemos agregarle la anotación
@JvmOverloads
:
class MyClassA @JvmOverloads constructor(var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20)
De lo contrario, obtenemos un error.
Ahora hablemos de
inicializadores .
Un inicializador es un bloque de código marcado con la palabra clave
init
. En este bloque, puede realizar algo de lógica para inicializar los elementos de la clase, incluido el uso de los valores de los argumentos que vienen en el constructor primario. También podemos llamar a funciones desde este bloque.
Java también tiene bloques de inicialización, pero estos no son lo mismo. En ellos, no podemos, como en Kotlin, pasar un valor desde el exterior (los argumentos del constructor primario). El inicializador es muy similar al cuerpo del constructor primario, extraído en un bloque separado. Pero es a primera vista. De hecho, esto no es del todo cierto. Vamos a hacerlo bien.
También puede existir un inicializador cuando no hay un constructor primario. Si es así, su código, como todos los procesos de inicialización, se ejecuta antes que el código del constructor adicional. Puede haber más de un inicializador. En este caso, el orden de su llamada coincidirá con el orden de su ubicación en el código. También tenga en cuenta que la inicialización del campo de clase puede ocurrir fuera de los bloques de
init
. En este caso, la inicialización también ocurre de acuerdo con la disposición de los elementos en el código, y esto debe tenerse en cuenta al llamar a métodos desde el bloque inicializador. Si lo toma descuidadamente, existe la posibilidad de que se encuentre con un error.
Te daré algunos casos interesantes de trabajar con inicializadores.
class MyClassB { init { testParam = "some string" showTestParam() } init { testParam = "new string" } var testParam: String = "after" constructor(){ Log.i("wow", "in constructor testParam = $testParam") } fun showTestParam(){ Log.i("wow", "in showTestParam testParam = $testParam") } }
Este código es bastante válido, aunque no del todo obvio. Si observa, puede ver que la asignación de un valor al campo
testParam
en el bloque inicializador ocurre antes de que se declare el parámetro. Por cierto, esto solo funciona si tenemos un constructor adicional en la clase, pero no tenemos uno primario (si elevamos la declaración del campo
testParam
encima del bloque
init
, funcionará sin un constructor). Si descompilamos el código de bytes de esta clase en Java, obtenemos lo siguiente:
public class MyClassB { @NotNull private String testParam = "some string"; @NotNull public final String getTestParam() { return this.testParam; } public final void setTestParam(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.testParam = var1; } public final void showTestParam() { Log.i("wow", "in showTestParam testParam = " + this.testParam); } public MyClassB() { this.showTestParam(); this.testParam = "new string"; this.testParam = "after"; Log.i("wow", "in constructor testParam = " + this.testParam); } }
Aquí vemos que la primera llamada al campo durante la inicialización (en el bloque
init
o fuera de él) es equivalente a su inicialización habitual en Java. Todas las demás acciones asociadas con la asignación de un valor durante el proceso de inicialización, excepto la primera (la primera asignación de un valor se combina con la declaración de campo), se transfieren al constructor.
Si llevamos a cabo experimentos con descompilación, resulta que si no hay un constructor, se genera el constructor primario, y toda la magia ocurre en él. Si hay varios constructores adicionales que no se refieren entre sí, y no hay uno primario, en el código Java de esta clase, todas las asignaciones posteriores del valor al campo
testParam
duplican en todos los constructores adicionales. Si hay un constructor primario, solo en el primario. Fuf ...
Y lo más interesante para
testParam
: cambiemos la firma
testParam
de
var
a
val
:
class MyClassB { init { testParam = "some string" showTestParam() } init { testParam = "new string" } val testParam: String = "after" constructor(){ Log.i("wow", "in constructor testParam = $testParam") } fun showTestParam(){ Log.i("wow", "in showTestParam testParam = $testParam") } }
Y en algún lugar del código llamamos:
MyClassB myClassB = new MyClassB();
Todo se compiló sin errores, comenzó y ahora vemos la salida de los registros:
en showTestParam testParam = alguna cadena
en el constructor testParam = after
Resulta que el campo declarado como
val
cambió el valor durante la ejecución del código. Por qué Creo que esto es una falla en el compilador de Kotlin, y en el futuro, tal vez esto no se compilará, pero hoy todo está como está.
Al sacar conclusiones de los casos anteriores, solo podemos aconsejarle que no produzca bloques de inicialización y que no los distribuya por la clase, para evitar la asignación repetida de valores durante el proceso de inicialización, para llamar solo funciones puras a partir de bloques de inicio. Todo esto se hace para evitar posibles confusiones.
Entonces
Los inicializadores son un cierto bloque de código que debe ejecutarse al crear un objeto, independientemente del constructor con el que se cree este objeto.Parece resuelto. Considere la interacción de constructores e inicializadores. Dentro de una clase, todo es bastante simple, pero debe recordar:
- llamar a un constructor adicional;
- llamar al constructor primario;
- inicialización de campos de clase y bloques de inicializador en el orden de su ubicación en el código;
- ejecución de código en el cuerpo de un constructor adicional.
Los casos con herencia se ven más interesantes.
Vale la pena señalar que como Object es la base para todas las clases en Java, Any es tal en Kotlin. Sin embargo, Any y Object no son lo mismo.
Para comenzar sobre cómo funciona la herencia. La clase descendiente, como la clase padre, puede o no tener un constructor primario, pero debe referirse a un constructor específico de la clase padre.
Si la clase descendiente tiene un constructor primario, este constructor debe apuntar a un constructor específico de la clase base. En este caso, todos los constructores adicionales de la clase sucesora deben referirse al constructor principal de su clase.
class MyClassC(p1: String): MyClassA(p1) { constructor(p1: String, p2: Int): this(p1) { //some code } //some code }
Si la clase descendiente no tiene un constructor primario, cada uno de los constructores adicionales debe acceder al constructor de la clase padre utilizando la palabra clave
super
. En este caso, diferentes constructores adicionales de la clase sucesora pueden acceder a diferentes constructores de la clase padre:
class MyClassC : MyClassA constructor(p1: String, p2: Int): super(p1, p2)
Además, no olvide la posibilidad de llamar indirectamente al constructor de la clase padre a través de otros constructores de la clase derivada:
class MyClassC : MyClassA constructor(p1: String, p2: Int): this (p1)
Si la clase descendiente no tiene ningún constructor, simplemente agregamos la llamada al constructor de la clase padre después del nombre de la clase descendiente:
class MyClassC: MyClassA(“some string”) {
Sin embargo, todavía hay una opción con herencia, en la que no se requiere una referencia al constructor de la clase principal. Tal registro es válido:
class MyClassC : MyClassB constructor(p1: String)
Pero solo si la clase padre tiene un constructor sin parámetros, que es el constructor predeterminado (primario u opcional, no importa).
Ahora considere el orden de invocación de inicializadores y constructores durante la herencia:
- llame al constructor adicional del heredero;
- llame al constructor primario del heredero;
- llamar al constructor adicional del padre;
- llamar al constructor primario del padre;
init
bloques init
primarios- ejecución del código del cuerpo de un constructor padre adicional;
- ejecución del bloque
init
del heredero; - ejecución del código del cuerpo del constructor adicional del heredero.
Hablemos de la comparación con Java, en la que, de hecho, no existe un análogo del constructor primario de Kotlin. En Java, todos los constructores son pares y pueden ser llamados o no entre sí. En Java y Kotlin hay un constructor predeterminado, es un constructor sin parámetros, pero adquiere un estado especial solo cuando se hereda. Aquí vale la pena prestar atención a lo siguiente: al heredar en Kotlin, debemos decirle explícitamente a la clase sucesora qué constructor de la clase padre usar: el compilador no nos permitirá olvidarlo. En Java, no podemos indicar explícitamente esto. Tenga cuidado: en este caso, se llamará al constructor predeterminado de la clase principal (si existe).
En esta etapa, asumiremos que estudiamos a los diseñadores e inicializadores bastante profundamente y ahora sabemos casi todo sobre ellos. ¡Descansaremos un poco y cavaremos en la otra dirección!