
Bereits im Mai 2017 gab Google bekannt, dass Kotlin die offizielle Entwicklungssprache für Android geworden ist. Jemand hörte dann zum ersten Mal den Namen dieser Sprache, jemand schrieb lange darüber, aber von diesem Moment an wurde klar, dass jeder, der der Android-Entwicklung nahe steht, nun einfach verpflichtet ist, ihn kennenzulernen. Es folgten begeisterte Antworten „Endlich!“ Und schreckliche Empörung „Warum brauchen wir eine neue Sprache?“. Was hat Java nicht gefallen? " usw. usw.
Seitdem ist genug Zeit vergangen, und obwohl die Debatte darüber, ob Kotlin gut oder schlecht ist, immer noch nicht abgeklungen ist, wird immer mehr Code für Android darauf geschrieben. Und auch recht konservative Entwickler wechseln dazu. Darüber hinaus können Sie im Netzwerk auf Informationen stoßen, dass die Entwicklungsgeschwindigkeit nach dem Erlernen dieser Sprache im Vergleich zu Java um 30% erhöht ist.
Kotlin hat es bereits heute geschafft, sich von mehreren Kinderkrankheiten zu erholen, die mit vielen Fragen und Antworten zum Stapelüberlauf überwachsen sind. Mit bloßem Auge wurden sowohl seine Vor- als auch seine Schwächen sichtbar.
Und auf dieser Welle kam mir die Idee, die einzelnen Elemente einer jungen, aber populären Sprache im Detail zu analysieren. Achten Sie auf komplexe Punkte und vergleichen Sie sie zur besseren Übersichtlichkeit und zum besseren Verständnis mit Java. Um die Frage etwas tiefer zu verstehen, lesen Sie die Dokumentation. Wenn dieser Artikel Interesse weckt, wird er höchstwahrscheinlich den Grundstein für eine ganze Reihe von Artikeln legen. In der Zwischenzeit werde ich mit ziemlich einfachen Dingen beginnen, die jedoch viele Fallstricke verbergen. Lassen Sie uns über Konstruktoren und Initialisierer in Kotlin sprechen.
Wie in Java erfolgt in Kotlin die Erstellung neuer Objekte - Entitäten eines bestimmten Typs - durch Aufrufen des Klassenkonstruktors. Sie können dem Konstruktor auch Argumente übergeben, und es können mehrere Konstruktoren vorhanden sein. Wenn Sie diesen Prozess von außen betrachten, besteht der einzige Unterschied zu Java darin, dass beim Aufrufen des Konstruktors das neue Schlüsselwort fehlt. Schauen Sie jetzt genauer hin und sehen Sie, was in der Klasse passiert.
Eine Klasse kann primäre und sekundäre Konstruktoren haben.
Ein Konstruktor wird mit dem Schlüsselwort constructor deklariert. Wenn der primäre Konstruktor keine Zugriffsmodifikatoren und Anmerkungen hat, kann das Schlüsselwort weggelassen werden.
In einer Klasse sind Konstruktoren möglicherweise nicht explizit deklariert. In diesem Fall fahren wir nach der Deklaration der Klasse ohne Konstruktionen sofort mit dem Hauptteil der Klasse fort. Wenn wir eine Analogie zu Java ziehen, entspricht dies dem Fehlen einer expliziten Deklaration von Konstruktoren, wodurch der Standardkonstruktor (ohne Parameter) in der Kompilierungsphase automatisch generiert wird. Es sieht aus wie erwartet:
class MyClassA
Dies entspricht dem folgenden Eintrag:
class MyClassA constructor()
Wenn Sie jedoch so schreiben, werden Sie höflich gebeten, den primären Konstruktor ohne Parameter zu entfernen.
Der primäre Konstruktor wird immer aufgerufen, wenn ein Objekt erstellt wird, falls es existiert. Während wir dies berücksichtigen, werden wir es später genauer analysieren, wenn wir zu den sekundären Konstruktoren übergehen. Dementsprechend erinnern wir uns, dass wenn es überhaupt keine Konstruktoren gibt, es tatsächlich einen (primären) gibt, aber wir sehen es nicht.
Wenn wir beispielsweise möchten, dass der primäre Konstruktor ohne Parameter keinen öffentlichen Zugriff hat, müssen wir ihn zusammen mit der
private
Änderung explizit mit dem Schlüsselwort
constructor
deklarieren.
Das Hauptmerkmal des primären Konstruktors ist, dass er keinen Körper hat, d.h. darf keinen ausführbaren Code enthalten. Es nimmt einfach Parameter in sich auf und übergibt sie zur zukünftigen Verwendung tief an die Klasse. Auf der Syntaxebene sieht es so aus:
class MyClassA constructor(param1: String, param2: Int, param3: Boolean)
Auf diese Weise übergebene Parameter können für verschiedene Initialisierungen verwendet werden, jedoch nicht mehr. In seiner reinen Form können wir diese Argumente nicht im Arbeitscode der Klasse verwenden. Hier können wir jedoch die Felder der Klasse initialisieren. Es sieht so aus:
class MyClassA constructor(val param1: String, var param2: Int, param3: Boolean)
Hier können
param1
und
param2
im Code als Felder der Klasse verwendet werden, was dem Folgenden entspricht:
class MyClassA constructor(p1: String, p2: Int, param3: Boolean)
Wenn Sie mit Java vergleichen, sieht es folgendermaßen aus (und in diesem Beispiel können Sie übrigens bewerten, um wie viel Kotlin die Codemenge reduzieren kann):
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; }
Lassen Sie uns über zusätzliche Designer sprechen. Sie erinnern eher an gewöhnliche Konstruktoren in Java: Sie akzeptieren Parameter und haben möglicherweise einen ausführbaren Block. Wenn Sie zusätzliche Konstruktoren deklarieren, ist das Schlüsselwort Konstruktor erforderlich. Wie bereits erwähnt, sollte trotz der Möglichkeit, ein Objekt durch Aufrufen eines zusätzlichen Konstruktors zu erstellen, der primäre Konstruktor (falls vorhanden) auch mit Hilfe
this
aufgerufen werden. Auf Syntaxebene ist dies wie folgt organisiert:
class MyClassA(val p1: String) { constructor(p1: String, p2: Int, p3: Boolean) : this(p1) {
Das heißt, der zusätzliche Konstruktor ist sozusagen der Erbe des Primären.
Wenn wir nun ein Objekt erstellen, indem wir einen zusätzlichen Konstruktor aufrufen, geschieht Folgendes:
einen zusätzlichen Konstruktor aufrufen;
Rufen Sie den Hauptkonstruktor auf.
Initialisierung eines Feldes der Klasse
p1
im Hauptkonstruktor;
Codeausführung im Hauptteil eines zusätzlichen Konstruktors.
Dies ähnelt einer solchen Konstruktion in Java:
class MyClassAJava { private final String param1; public MyClassAJava(String p1) { param1 = p1; } public MyClassAJava(String p1, Integer p2, Boolean param3) { this(p1);
Denken Sie daran, dass wir in Java einen Konstruktor mit dem
this
nur am Anfang des Konstruktorkörpers von einem anderen aufrufen können. Bei Kotlin wurde dieses Problem grundlegend gelöst - ein solcher Aufruf wurde zu einem Teil der Signatur des Konstruktors. Für alle Fälle stelle ich fest, dass es verboten ist, einen (primären oder zusätzlichen) Konstruktor direkt aus dem Hauptteil des zusätzlichen Konstruktors aufzurufen.
Ein zusätzlicher Konstruktor sollte sich immer auf den Hauptkonstruktor beziehen (falls vorhanden), kann dies jedoch indirekt tun und sich auf einen anderen zusätzlichen Konstruktor beziehen. Das Fazit ist, dass wir am Ende der Kette immer noch zur Hauptsache kommen. Das Auslösen der Konstruktoren erfolgt offensichtlich in umgekehrter Reihenfolge, in der sich die Konstrukteure zueinander wenden:
class MyClassA(p1: String) constructor(p1: String, p2: Int, p3: Boolean, p4: String) : this(p1, p2, p3)
Jetzt lautet die Reihenfolge:
- Aufrufen eines zusätzlichen Konstruktors mit 4 Parametern;
- Aufrufen eines zusätzlichen Konstruktors mit 3 Parametern;
- Rufen Sie den primären Konstruktor auf.
- Initialisierung eines Feldes der Klasse p1 im Primärkonstruktor;
- Codeausführung im Hauptteil des Konstruktors mit 3 Parametern;
- Codeausführung im Konstruktorkörper mit 4 Parametern.
In jedem Fall wird der Compiler uns niemals vergessen lassen, zum primären Konstruktor zu gelangen.
Es kommt vor, dass eine Klasse keinen primären Konstruktor hat, während sie möglicherweise einen oder mehrere zusätzliche hat. Dann müssen zusätzliche Konstruktoren nicht auf jemanden verweisen, sondern können auch auf andere zusätzliche Konstruktoren dieser Klasse verweisen. Zuvor haben wir festgestellt, dass der nicht explizit angegebene Hauptkonstruktor automatisch generiert wird. Dies gilt jedoch für Fälle, in denen die Klasse überhaupt keine Konstruktoren enthält. Wenn mindestens ein zusätzlicher Konstruktor vorhanden ist, wird kein Primärkonstruktor ohne Parameter erstellt:
class MyClassA {
Wir können ein Klassenobjekt erstellen, indem wir Folgendes aufrufen:
val myClassA = MyClassA()
In diesem Fall:
class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) {
Wir können ein Objekt nur mit diesem Aufruf erstellen:
val myClassA = MyClassA(“some string”, 10, True)
In Kotlin gibt es im Vergleich zu Java nichts Neues.
Übrigens hat der zusätzliche Konstruktor wie der primäre Konstruktor möglicherweise keinen Body, wenn seine Aufgabe nur darin besteht, Parameter an andere Konstruktoren zu übergeben.
class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) : this(p1, p2, p3, "") constructor(p1: String, p2: Int, p3: Boolean, p4: String) {
Es ist auch zu beachten, dass im Gegensatz zum primären Konstruktor die Initialisierung von Klassenfeldern in der Argumentliste des zusätzlichen Konstruktors verboten ist.
Das heißt, Ein solcher Datensatz ist ungültig:
class MyClassA { constructor(val p1: String, var p2: Int, p3: Boolean){
Unabhängig davon ist anzumerken, dass der zusätzliche Konstruktor, wie der primäre, durchaus ohne Parameter sein kann:
class MyClassA { constructor(){
Wenn man von Konstruktoren spricht, kann man nur eine der praktischen Funktionen von Kotlin erwähnen - die Möglichkeit, Standardwerte für Argumente zuzuweisen.
Nehmen wir nun an, wir haben eine Klasse mit mehreren Konstruktoren, die eine unterschiedliche Anzahl von Argumenten haben. Ich werde ein Beispiel in Java geben:
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; }
Wie die Praxis zeigt, sind solche Designs weit verbreitet. Mal sehen, wie das gleiche auf Kotlin geschrieben werden kann:
class MyClassA (var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){
Lassen Sie uns jetzt Kotlin zusammenklopfen, um wie viel er den Code geschnitten hat. Übrigens, wir reduzieren nicht nur die Anzahl der Zeilen, sondern erhalten auch mehr Ordnung. Denken Sie daran, dass Sie so etwas mehr als einmal gesehen haben müssen:
public MyClassAJava(String p1, Integer p2, boolean p3) { this(p3, p1, p2, 20); } public MyClassAJava(boolean p1, String p2, Integer p3, int p4) {
Wenn Sie dies sehen, möchten Sie die Person finden, die es geschrieben hat, es per Knopfdruck nehmen, auf den Bildschirm bringen und mit trauriger Stimme fragen: „Warum?“
Sie können dieses Kunststück zwar auf Kotlin wiederholen, sind aber nicht notwendig.
Es gibt jedoch ein Detail, das im Fall einer solchen Kurznotation in Kotlin berücksichtigt werden muss: Wenn wir den Konstruktor mit Standardwerten aus Java aufrufen möchten, müssen wir die Annotation
@JvmOverloads
hinzufügen:
class MyClassA @JvmOverloads constructor(var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20)
Andernfalls erhalten wir eine Fehlermeldung.
Lassen Sie uns nun
über Initialisierer sprechen.
Ein Initialisierer ist ein Codeblock, der mit dem Schlüsselwort
init
. In diesem Block können Sie eine Logik ausführen, um die Elemente der Klasse zu initialisieren, einschließlich der Werte der Argumente, die im primären Konstruktor enthalten sind. Wir können auch Funktionen aus diesem Block aufrufen.
Java hat auch Initialisierungsblöcke, aber diese sind nicht dasselbe. In ihnen können wir nicht wie in Kotlin einen Wert von außen übergeben (die Argumente des primären Konstruktors). Der Initialisierer ist dem Körper des primären Konstruktors sehr ähnlich, der in einem separaten Block entfernt wird. Aber es ist auf den ersten Blick. In der Tat ist dies nicht ganz richtig. Lass es uns richtig machen.
Ein Initialisierer kann auch vorhanden sein, wenn kein primärer Konstruktor vorhanden ist. Wenn ja, wird sein Code wie alle Initialisierungsprozesse vor dem Code des zusätzlichen Konstruktors ausgeführt. Es kann mehr als einen Initialisierer geben. In diesem Fall stimmt die Reihenfolge ihres Anrufs mit der Reihenfolge ihres Standorts im Code überein. Beachten Sie auch, dass die Initialisierung von Klassenfeldern außerhalb von
init
Blöcken erfolgen kann. In diesem Fall erfolgt die Initialisierung auch gemäß der Anordnung der Elemente im Code, und dies muss beim Aufrufen von Methoden aus dem Initialisierungsblock berücksichtigt werden. Wenn Sie es nachlässig nehmen, besteht die Möglichkeit, dass Sie auf einen Fehler stoßen.
Ich werde Ihnen einige interessante Fälle der Arbeit mit Initialisierern geben.
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") } }
Dieser Code ist ziemlich gültig, wenn auch nicht ganz offensichtlich. Wenn Sie nachsehen, können Sie sehen, dass die Zuweisung eines Werts zum Feld
testParam
im Initialisierungsblock erfolgt, bevor der Parameter deklariert wird. Dies funktioniert übrigens nur, wenn wir einen zusätzlichen Konstruktor in der Klasse haben, aber keinen primären Konstruktor (wenn wir die Deklaration des
testParam
Felds über den
init
Block heben, funktioniert dies ohne Konstruktor). Wenn wir den Bytecode dieser Klasse in Java dekompilieren, erhalten wir Folgendes:
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); } }
Hier sehen wir, dass der erste Aufruf des Feldes während der Initialisierung (im
init
Block oder außerhalb davon) seiner üblichen Initialisierung in Java entspricht. Alle anderen Aktionen, die mit der Zuweisung eines Werts während des Initialisierungsprozesses verbunden sind, mit Ausnahme der ersten (die erste Zuweisung eines Werts wird mit der Felddeklaration kombiniert), werden an den Konstruktor übertragen.
Wenn wir Experimente mit Dekompilierung durchführen, stellt sich heraus, dass, wenn es keinen Konstruktor gibt, der primäre Konstruktor generiert wird und die gesamte Magie darin geschieht. Wenn mehrere zusätzliche Konstruktoren vorhanden sind, die nicht aufeinander verweisen, und es keinen primären gibt, werden im Java-Code dieser Klasse alle nachfolgenden Zuweisungen des Werts zum Feld
testParam
in allen zusätzlichen Konstruktoren dupliziert. Wenn es einen primären Konstruktor gibt, dann nur im primären. Fuf ...
Und das Interessanteste für den
testParam
: Ändern
testParam
die
testParam
Signatur von
var
in
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") } }
Und irgendwo im Code nennen wir:
MyClassB myClassB = new MyClassB();
Alles ohne Fehler kompiliert, gestartet, und jetzt sehen wir die Ausgabe der Protokolle:
in showTestParam testParam = eine Zeichenfolge
im Konstruktor testParam = after
Es stellt sich heraus, dass das als
val
deklarierte Feld den Wert während der Codeausführung geändert hat. Warum so? Ich denke, dass dies ein Fehler im Kotlin-Compiler ist, und in Zukunft wird dies möglicherweise nicht kompiliert, aber heute ist alles so, wie es ist.
Wenn man aus den obigen Fällen Schlussfolgerungen zieht, kann man nur raten, keine Initialisierungsblöcke zu erzeugen und diese nicht über die Klasse zu verteilen, um eine wiederholte Zuweisung von Werten während des Initialisierungsprozesses zu vermeiden und nur reine Funktionen aus Init-Blöcken aufzurufen. All dies geschieht, um mögliche Verwirrung zu vermeiden.
Also.
Initialisierer sind ein bestimmter Codeblock, der beim Erstellen eines Objekts ausgeführt werden muss, unabhängig davon, mit welchem Konstruktor dieses Objekt erstellt wird.Es scheint geklärt zu sein. Betrachten Sie das Zusammenspiel von Konstruktoren und Initialisierern. Innerhalb einer Klasse ist alles ganz einfach, aber Sie müssen sich daran erinnern:
- einen zusätzlichen Konstruktor aufrufen;
- Rufen Sie den primären Konstruktor auf.
- Initialisierung von Klassenfeldern und Initialisierungsblöcken in der Reihenfolge ihrer Position im Code;
- Codeausführung im Hauptteil eines zusätzlichen Konstruktors.
Fälle mit Vererbung sehen interessanter aus.
Es ist erwähnenswert, dass Object die Basis für alle Klassen in Java ist und Any in Kotlin. Any und Object sind jedoch nicht dasselbe.
Erste Schritte zur Funktionsweise der Vererbung. Die untergeordnete Klasse kann wie die übergeordnete Klasse einen primären Konstruktor haben oder nicht, muss sich jedoch auf einen bestimmten Konstruktor der übergeordneten Klasse beziehen.
Wenn die untergeordnete Klasse einen primären Konstruktor hat, muss dieser Konstruktor auf einen bestimmten Konstruktor der Basisklasse verweisen. In diesem Fall müssen sich alle zusätzlichen Konstruktoren der Nachfolgerklasse auf den Hauptkonstruktor ihrer Klasse beziehen.
class MyClassC(p1: String): MyClassA(p1) { constructor(p1: String, p2: Int): this(p1) { //some code } //some code }
Wenn die untergeordnete Klasse keinen primären Konstruktor hat, muss jeder der zusätzlichen Konstruktoren mit dem Schlüsselwort
super
auf den Konstruktor der übergeordneten Klasse zugreifen. In diesem Fall können verschiedene zusätzliche Konstruktoren der Nachfolgerklasse auf verschiedene Konstruktoren der übergeordneten Klasse zugreifen:
class MyClassC : MyClassA constructor(p1: String, p2: Int): super(p1, p2)
Vergessen Sie auch nicht die Möglichkeit, den Konstruktor der übergeordneten Klasse indirekt über andere Konstruktoren der abgeleiteten Klasse aufzurufen:
class MyClassC : MyClassA constructor(p1: String, p2: Int): this (p1)
Wenn die Nachkommenklasse keine Konstruktoren hat, fügen wir einfach den Konstruktoraufruf der Elternklasse nach dem Namen der Nachkommenklasse hinzu:
class MyClassC: MyClassA(“some string”) {
Es gibt jedoch noch eine Option mit Vererbung, bei der kein Verweis auf den Konstruktor der übergeordneten Klasse erforderlich ist. Eine solche Aufzeichnung ist gültig:
class MyClassC : MyClassB constructor(p1: String)
Aber nur, wenn die übergeordnete Klasse einen Konstruktor ohne Parameter hat, der der Standardkonstruktor ist (primär oder optional - das spielt keine Rolle).
Betrachten Sie nun die Reihenfolge des Aufrufs von Initialisierern und Konstruktoren während der Vererbung:
- Rufen Sie den zusätzlichen Konstruktor des Erben an.
- Rufen Sie den Hauptkonstrukteur des Erben an.
- Aufrufen des zusätzlichen Konstruktors des übergeordneten Elements;
- Rufen Sie den primären Konstruktor des Elternteils auf.
init
Blöcke init
- Ausführung des Body-Codes eines zusätzlichen übergeordneten Konstruktors
- Ausführung des
init
Blocks des Erben; - Ausführung des Body-Codes des zusätzlichen Konstruktors des Erben
Lassen Sie uns über den Vergleich mit Java sprechen, in dem es tatsächlich kein Analogon zum Primärkonstruktor von Kotlin gibt. In Java sind alle Konstruktoren Peers und können entweder voneinander aufgerufen oder nicht voneinander aufgerufen werden. In Java und Kotlin gibt es einen Standardkonstruktor, einen Konstruktor ohne Parameter, der jedoch nur beim Erben einen Sonderstatus erhält. Hier ist Folgendes zu beachten: Wenn wir in Kotlin erben, müssen wir der Nachfolgeklasse explizit mitteilen, welcher Konstruktor der übergeordneten Klasse verwendet werden soll - der Compiler lässt uns dies nicht vergessen. In Java können wir dies nicht explizit angeben. Seien Sie vorsichtig: In diesem Fall wird der Standardkonstruktor der übergeordneten Klasse aufgerufen (falls vorhanden).
In diesem Stadium gehen wir davon aus, dass wir die Designer und Initialisierer ziemlich gründlich studiert haben und jetzt fast alles über sie wissen. Wir ruhen uns ein bisschen aus und graben in die andere Richtung!