Kotlin: creuser plus profondément. Constructeurs et initialiseurs



En mai 2017, Google a annoncé que Kotlin était devenu le langage de développement officiel pour Android. Quelqu'un a ensuite entendu le nom de cette langue pour la première fois, quelqu'un a écrit dessus pendant longtemps, mais à partir de ce moment, il est devenu clair que toute personne proche du développement Android est maintenant obligée de la connaître. Cela a été suivi par des réponses enthousiastes "Enfin!" Et une terrible indignation "Pourquoi avons-nous besoin d'une nouvelle langue?" Qu'est-ce qui n'a pas plu à Java? etc. etc.

Depuis, assez de temps s'est écoulé, et bien que le débat sur la bonne ou la mauvaise Kotlin ne soit toujours pas résolu, de plus en plus de code pour Android y est écrit. Et même des développeurs assez conservateurs y passent également. De plus, sur le réseau, vous pouvez tomber sur des informations selon lesquelles la vitesse de développement après la maîtrise de ce langage est augmentée de 30% par rapport à Java.

Aujourd'hui, Kotlin a déjà réussi à se remettre de plusieurs maladies infantiles, envahies par de nombreuses questions et réponses sur Stack Overflow. À l'œil nu, ses avantages et ses faiblesses sont devenus visibles.

Et sur cette vague, l'idée m'est venue d'analyser en détail les éléments individuels d'un langage jeune mais populaire. Faites attention aux points complexes et comparez-les avec Java pour plus de clarté et une meilleure compréhension. Pour comprendre la question un peu plus profondément que cela peut être fait en lisant la documentation. Si cet article suscite l'intérêt, il posera très probablement les bases de toute une série d'articles. En attendant, je vais commencer par des choses assez basiques, qui cachent cependant pas mal d'embûches. Parlons des constructeurs et des initialiseurs dans Kotlin.

Comme en Java, dans Kotlin, la création de nouveaux objets - entités d'un certain type - se produit en appelant le constructeur de classe. Vous pouvez également passer des arguments au constructeur, et il peut y avoir plusieurs constructeurs. Si vous regardez ce processus de l'extérieur, la seule différence avec Java est le manque de nouveau mot-clé lors de l'appel du constructeur. Maintenant, jetez un œil plus profond et voyez ce qui se passe à l'intérieur de la classe.

Une classe peut avoir des constructeurs primaires et secondaires.
Un constructeur est déclaré à l'aide du mot-clé constructeur. Si le constructeur principal n'a pas de modificateurs d'accès et d'annotations, le mot-clé peut être omis.
Une classe peut ne pas avoir de constructeurs déclarés explicitement. Dans ce cas, après la déclaration de la classe il n'y a pas de constructions, on passe immédiatement au corps de la classe. Si nous établissons une analogie avec Java, cela équivaut à l'absence d'une déclaration explicite de constructeurs, à la suite de quoi le constructeur par défaut (sans paramètres) sera généré automatiquement au stade de la compilation. Il semble comme prévu:

class MyClassA 

Cela équivaut à l'entrée suivante:

 class MyClassA constructor() 

Mais si vous écrivez de cette façon, il vous sera poliment demandé de supprimer le constructeur principal sans paramètres.

Le constructeur principal est celui qui est toujours appelé lorsqu'un objet est créé au cas où il existe. Bien que nous en tenions compte et que nous analyserons plus en détail plus tard, lorsque nous passerons aux constructeurs secondaires. En conséquence, nous nous souvenons que s'il n'y a pas de constructeurs du tout, alors en fait il y en a un (principal), mais nous ne le voyons pas.

Si, par exemple, nous voulons que le constructeur principal sans paramètres n'ait pas d'accès public, alors avec la modification private , nous devrons le déclarer explicitement avec le mot-clé constructor .

La principale caractéristique du constructeur principal est qu'il n'a pas de corps, c'est-à-dire ne peut pas contenir de code exécutable. Il prend simplement les paramètres en eux-mêmes et les transmet profondément dans la classe pour une utilisation future. Au niveau de la syntaxe, cela ressemble à ceci:

 class MyClassA constructor(param1: String, param2: Int, param3: Boolean){ // some code } 

Les paramètres passés de cette manière peuvent être utilisés pour diverses initialisations, mais pas plus. Dans sa forme pure, nous ne pouvons pas utiliser ces arguments dans le code de travail de la classe. Cependant, nous pouvons initialiser les champs de la classe ici. Cela ressemble à ceci:

 class MyClassA constructor(val param1: String, var param2: Int, param3: Boolean){ // some code } 

Ici, param1 et param2 peuvent être utilisés dans le code en tant que champs de la classe, ce qui équivaut à ce qui suit:

 class MyClassA constructor(p1: String, p2: Int, param3: Boolean){ val param1 = p1 var param2 = p2 // some code } 

Eh bien, si vous comparez avec Java, cela ressemblerait à ceci (et au fait, dans cet exemple, vous pouvez évaluer dans quelle mesure Kotlin peut réduire la quantité de code):

 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; } // some code } 

Parlons de designers supplémentaires. Ils rappellent plus les constructeurs ordinaires en Java: ils acceptent des paramètres et peuvent avoir un bloc exécutable. Lors de la déclaration de constructeurs supplémentaires, le mot-clé constructeur est requis. Comme mentionné précédemment, malgré la possibilité de créer un objet en appelant un constructeur supplémentaire, le constructeur principal (le cas échéant) doit également être appelé à l'aide du this . Au niveau de la syntaxe, cela est organisé comme suit:

 class MyClassA(val p1: String) { constructor(p1: String, p2: Int, p3: Boolean) : this(p1) { // some code } // some code } 

C'est-à-dire le constructeur supplémentaire est, pour ainsi dire, l'héritier du primaire.
Maintenant, si nous créons un objet en appelant un constructeur supplémentaire, ce qui se passera:

appeler un constructeur supplémentaire;
appeler le constructeur principal;
initialisation d'un champ de classe p1 dans le constructeur principal;
exécution de code dans le corps d'un constructeur supplémentaire.

Ceci est similaire à une telle construction en Java:

 class MyClassAJava { private final String param1; public MyClassAJava(String p1) { param1 = p1; } public MyClassAJava(String p1, Integer p2, Boolean param3) { this(p1); // some code } // some code } 

Rappelons qu'en Java on ne peut appeler un constructeur d'un autre en utilisant le this qu'au début du corps du constructeur. Chez Kotlin, ce problème était fondamentalement résolu - ils ont fait de cet appel une partie de la signature du constructeur. Juste au cas où, je note qu'il est interdit d'appeler un constructeur (principal ou supplémentaire) directement à partir du corps du constructeur supplémentaire.

Un constructeur supplémentaire doit toujours faire référence au constructeur principal (le cas échéant), mais peut le faire indirectement, en faisant référence à un autre constructeur supplémentaire. L'essentiel est qu'à la fin de la chaîne, nous arrivons toujours à l'essentiel. Le déclenchement des constructeurs se fera évidemment dans l'ordre inverse des concepteurs se tournant les uns vers les autres:

 class MyClassA(p1: String) { constructor(p1: String, p2: Int, p3: Boolean) : this(p1) { // some code } constructor(p1: String, p2: Int, p3: Boolean, p4: String) : this(p1, p2, p3) { // some code } // some code } 

Maintenant, la séquence est:

  • appeler un constructeur supplémentaire avec 4 paramètres;
  • appeler un constructeur supplémentaire avec 3 paramètres;
  • appeler le constructeur principal;
  • initialisation d'un champ de classe p1 dans le constructeur primaire;
  • exécution de code dans le corps du constructeur avec 3 paramètres;
  • exécution de code dans le corps du constructeur avec 4 paramètres.

Dans tous les cas, le compilateur n'oubliera jamais de se rendre au constructeur principal.

Il arrive qu'une classe n'ait pas de constructeur principal, alors qu'elle peut en avoir un ou plusieurs supplémentaires. Ensuite, les constructeurs supplémentaires ne sont pas tenus de faire référence à quelqu'un, mais ils peuvent également faire référence à d'autres constructeurs supplémentaires de cette classe. Plus tôt, nous avons découvert que le constructeur principal, non spécifié explicitement, est généré automatiquement, mais cela s'applique aux cas où il n'y a aucun constructeur dans la classe. S'il existe au moins un constructeur supplémentaire, un constructeur principal sans paramètres n'est pas créé:

 class MyClassA { // some code } 

Nous pouvons créer un objet de classe en appelant:

 val myClassA = MyClassA() 

Dans ce cas:

 class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) { // some code } // some code } 

Nous ne pouvons créer un objet qu'avec cet appel:

 val myClassA = MyClassA(“some string”, 10, True) 

Il n'y a rien de nouveau dans Kotlin par rapport à Java.

Soit dit en passant, comme le constructeur principal, le constructeur supplémentaire peut ne pas avoir de corps si sa tâche consiste uniquement à transmettre des paramètres à d'autres constructeurs.

 class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) : this(p1, p2, p3, "") constructor(p1: String, p2: Int, p3: Boolean, p4: String) { // some code } // some code } 

Il convient également de prêter attention au fait que, contrairement au constructeur principal, l'initialisation des champs de classe dans la liste d'arguments du constructeur supplémentaire est interdite.
C'est-à-dire un tel enregistrement sera invalide:

 class MyClassA { constructor(val p1: String, var p2: Int, p3: Boolean){ // some code } // some code } 

Séparément, il convient de noter que le constructeur supplémentaire, comme le principal, peut bien être sans paramètres:

 class MyClassA { constructor(){ // some code } // some code } 

En parlant de constructeurs, on ne peut que mentionner l'une des fonctionnalités pratiques de Kotlin - la possibilité d'attribuer des valeurs par défaut aux arguments.

Supposons maintenant que nous ayons une classe avec plusieurs constructeurs qui ont un nombre d'arguments différent. Je vais donner un exemple 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; } // some code } 

Comme le montre la pratique, ces conceptions sont assez courantes. Voyons comment la même chose peut être écrite sur Kotlin:

 class MyClassA (var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){ // some code } 

Maintenant, tapotons Kotlin ensemble pour combien il a coupé le code. Soit dit en passant, en plus de réduire le nombre de lignes, nous obtenons plus d'ordre. Rappelez-vous, vous devez avoir vu quelque chose comme ça plus d'une fois:

  public MyClassAJava(String p1, Integer p2, boolean p3) { this(p3, p1, p2, 20); } public MyClassAJava(boolean p1, String p2, Integer p3, int p4) { // some code } 

Lorsque vous voyez cela, vous voulez trouver la personne qui l'a écrit, le prendre par un bouton, le porter à l'écran et demander d'une voix triste: "Pourquoi?"
Bien que vous puissiez répéter cet exploit sur Kotlin, mais pas nécessaire.

Il y a cependant un détail que, dans le cas d'une telle notation abrégée sur Kotlin, il est nécessaire de prendre en compte: si nous voulons appeler le constructeur avec des valeurs par défaut à partir de Java, alors nous devons lui ajouter l'annotation @JvmOverloads :

 class MyClassA @JvmOverloads constructor(var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){ // some code } 

Sinon, nous obtenons une erreur.

Parlons maintenant des initialiseurs .

Un initialiseur est un bloc de code marqué avec le mot-clé init . Dans ce bloc, vous pouvez exécuter une logique pour initialiser les éléments de la classe, notamment en utilisant les valeurs des arguments fournis par le constructeur principal. Nous pouvons également appeler des fonctions à partir de ce bloc.

Java a également des blocs d'initialisation, mais ce n'est pas la même chose. En eux, on ne peut pas, comme dans Kotlin, passer une valeur de l'extérieur (les arguments du constructeur primaire). L'initialiseur est très similaire au corps du constructeur principal, extrait dans un bloc séparé. Mais c'est à première vue. En fait, ce n'est pas entièrement vrai. Faisons les choses correctement.

Un initialiseur peut également exister en l'absence de constructeur principal. Si c'est le cas, son code, comme tous les processus d'initialisation, est exécuté avant le code du constructeur supplémentaire. Il peut y avoir plusieurs initialiseurs. Dans ce cas, l'ordre de leur appel coïncidera avec l'ordre de leur emplacement dans le code. Notez également que l'initialisation du champ de classe peut se produire en dehors des blocs init . Dans ce cas, l'initialisation se produit également conformément à la disposition des éléments dans le code, et cela doit être pris en compte lors de l'appel de méthodes à partir du bloc d'initialisation. Si vous le prenez imprudemment, il y a une chance de se tromper.

Je vais vous donner quelques cas intéressants de travail avec des initialiseurs.

 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") } } 

Ce code est tout à fait valide, mais pas tout à fait évident. Si vous regardez, vous pouvez voir que l'affectation d'une valeur au champ testParam dans le bloc d'initialisation se produit avant la déclaration du paramètre. Soit dit en passant, cela ne fonctionne que si nous avons un constructeur supplémentaire dans la classe, mais nous n'avons pas de constructeur principal (si nous élevons la déclaration du champ testParam au-dessus du bloc init , cela fonctionnera sans constructeur). Si nous décompilons le code octet de cette classe en Java, nous obtenons ce qui suit:

 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); } } 

On voit ici que le premier appel au champ lors de l'initialisation (dans le bloc init ou en dehors) équivaut à son initialisation habituelle en Java. Toutes les autres actions associées à l'affectation d'une valeur pendant le processus d'initialisation, à l'exception de la première (la première affectation d'une valeur est combinée avec la déclaration de champ), sont transférées au constructeur.
Si nous menons des expériences de décompilation, il s'avère que s'il n'y a pas de constructeur, alors le constructeur principal est généré, et toute la magie s'y produit. S'il y a plusieurs constructeurs supplémentaires qui ne se réfèrent pas l'un à l'autre, et qu'il n'y en a pas de principal, alors dans le code Java de cette classe toutes les affectations suivantes au champ testParam dupliquées dans tous les constructeurs supplémentaires. S'il y a un constructeur principal, alors seulement dans le primaire. Fuf ...

Et la chose la plus intéressante pour les testParam : changeons la signature testParam de var en 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") } } 

Et quelque part dans le code que nous appelons:

 MyClassB myClassB = new MyClassB(); 

Tout a été compilé sans erreur, a commencé, et maintenant nous voyons la sortie des journaux:

dans showTestParam testParam = une chaîne
dans constructeur testParam = après

Il s'avère que le champ déclaré comme val changé la valeur lors de l'exécution du code. Pourquoi Je pense que c'est une faille dans le compilateur Kotlin, et à l'avenir, cela ne compilera peut-être pas, mais aujourd'hui tout est comme ça.

En tirant des conclusions des cas ci-dessus, on ne peut que conseiller de ne pas produire de blocs d'initialisation et de ne pas les disperser dans la classe, pour éviter l'affectation répétée de valeurs pendant le processus d'initialisation, pour appeler uniquement des fonctions pures à partir de blocs init. Tout cela est fait pour éviter une éventuelle confusion.

Alors. Les initialiseurs sont un certain bloc de code qui doit être exécuté lors de la création d'un objet, quel que soit le constructeur avec lequel cet objet est créé.

Cela semble réglé. Considérez l'interaction des constructeurs et des initialiseurs. Dans une classe, tout est assez simple, mais vous devez vous rappeler:

  • appeler un constructeur supplémentaire;
  • appeler le constructeur principal;
  • initialisation des champs de classe et des blocs d'initialisation dans l'ordre de leur emplacement dans le code;
  • exécution de code dans le corps d'un constructeur supplémentaire.

Les cas avec héritage semblent plus intéressants.

Il convient de noter que, comme Object est la base de toutes les classes en Java, Any est tel dans Kotlin. Cependant, Any et Object ne sont pas la même chose.

Pour commencer sur le fonctionnement de l'héritage. La classe descendante, comme la classe parente, peut ou non avoir un constructeur principal, mais elle doit faire référence à un constructeur spécifique de la classe parente.

Si la classe descendante a un constructeur principal, ce constructeur doit pointer vers un constructeur spécifique de la classe de base. Dans ce cas, tous les constructeurs supplémentaires de la classe successeur doivent faire référence au constructeur principal de leur classe.

 class MyClassC(p1: String): MyClassA(p1) { constructor(p1: String, p2: Int): this(p1) { //some code } //some code } 

Si la classe descendante n'a pas de constructeur principal, chacun des constructeurs supplémentaires doit accéder au constructeur de la classe parente à l'aide du super mot clé. Dans ce cas, différents constructeurs supplémentaires de la classe successeur peuvent accéder à différents constructeurs de la classe parente:

 class MyClassC : MyClassA { constructor(p1: String): super(p1) { //some code } constructor(p1: String, p2: Int): super(p1, p2) { //some code } //some code } 

N'oubliez pas non plus la possibilité d'appeler indirectement le constructeur de la classe parente via d'autres constructeurs de la classe dérivée:

 class MyClassC : MyClassA{ constructor(p1: String): super(p1){ //some code } constructor(p1: String, p2: Int): this (p1){ //some code } //some code } 

Si la classe descendante n'a pas de constructeur, alors nous ajoutons simplement l'appel constructeur de la classe parente après le nom de la classe descendante:

 class MyClassC: MyClassA(“some string”) { //some code } 

Cependant, il existe toujours une option avec héritage, dans laquelle une référence au constructeur de la classe parente n'est pas requise. Un tel enregistrement est valide:

 class MyClassC : MyClassB { constructor(){ //some code } constructor(p1: String){ } //some code } 

Mais seulement si la classe parente a un constructeur sans paramètres, qui est le constructeur par défaut (principal ou facultatif - cela n'a pas d'importance).

Considérons maintenant l'ordre d'invocation des initialiseurs et des constructeurs lors de l'héritage:

  • appeler le constructeur supplémentaire de l'héritier;
  • appeler le constructeur principal de l'héritier;
  • appeler le constructeur supplémentaire du parent;
  • appeler le constructeur principal du parent;
  • init des blocs init parentaux
  • exécution du code du corps du constructeur supplémentaire du parent;
  • exécution du bloc init de l'héritier;
  • exécution du code du corps du constructeur supplémentaire de l'héritier

Parlons de comparaison avec Java, dans lequel, en fait, il n'y a pas d'analogue du constructeur principal de Kotlin. En Java, tous les constructeurs sont des pairs et peuvent être appelés ou non les uns des autres. En Java et Kotlin, il y a un constructeur par défaut, c'est un constructeur sans paramètres, mais il n'acquiert un statut spécial qu'en héritant. Ici, il convient de prêter attention aux éléments suivants: lors de l'héritage dans Kotlin, nous devons explicitement dire à la classe successeur quel constructeur de la classe parent utiliser - le compilateur ne nous laissera pas l'oublier. En Java, nous ne pouvons pas l'indiquer explicitement. Attention: dans ce cas, le constructeur par défaut de la classe parent sera appelé (le cas échéant).

À ce stade, nous supposerons que nous avons étudié les concepteurs et les initialiseurs assez profondément et maintenant nous savons presque tout à leur sujet. Nous allons nous reposer un peu et creuser dans l'autre sens!

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


All Articles