Von außen mag es so aussehen, als hätte Kotlin die Android-Entwicklung vereinfacht, ohne neue Schwierigkeiten zu verursachen: Die Sprache ist Java-kompatibel, sodass selbst ein großes Java-Projekt schrittweise in das Projekt übersetzt werden kann, ohne jemanden zu stören, oder? Aber wenn Sie genauer hinschauen, gibt es in jeder Schachtel einen doppelten Boden und im Schminktisch eine Geheimtür. Programmiersprachen sind zu komplexe Projekte, um ohne knifflige Nuancen kombiniert zu werden.
Dies bedeutet natürlich nicht, dass "alles schlecht ist und Sie Kotlin nicht mit Java verwenden müssen", sondern dass Sie die Nuancen kennen und berücksichtigen sollten. Auf unserer
Mobius- Konferenz
sprach Sergei Ryabov
darüber, wie man Code auf Kotlin schreibt, auf den bequem von Java aus zugegriffen werden kann. Das Publikum mochte den Bericht so sehr, dass wir nicht nur beschlossen, ein Video zu veröffentlichen, sondern auch eine Textversion für Habr erstellten:
Ich schreibe Kotlin seit mehr als drei Jahren, jetzt nur noch darauf, aber zuerst habe ich Kotlin in bestehende Java-Projekte hineingezogen. Daher stellte sich mir oft die Frage, wie man Java und Kotlin zusammenhält.
Wenn Sie einem Projekt Kotlin hinzufügen, können Sie häufig sehen, wie dies ...
compile 'rxbinding:xyx' compile 'rxbinding-appcompat-v7:xyx' compile 'rxbinding-design:xyx' compile 'autodispose:xyz' compile 'autodispose-android:xyz' compile 'autodispose-android-archcomponents:xyz'
... verwandelt sich in:
compile 'rxbinding:xyx' compile 'rxbinding-kotlin:xyx' compile 'rxbinding-appcompat-v7:xyx' compile 'rxbinding-appcompat-v7-kotlin:xyx' compile 'rxbinding-design:xyx' compile 'rxbinding-design-kotlin:xyx' compile 'autodispose:xyz' compile 'autodispose-kotlin:xyz' compile 'autodispose-android:xyz' compile 'autodispose-android-kotlin:xyz' compile 'autodispose-android-archcomponents:xyz' compile 'autodispose-android-archcomponents-kotlin:xyz'
Die Besonderheiten der letzten Jahre: Die beliebtesten Bibliotheken erwerben Wrapper, damit sie von Kotlin idiomatischer verwendet werden können.
Wenn Sie in Kotlin geschrieben haben, wissen Sie, dass es coole Erweiterungsfunktionen, Inline-Funktionen und Lambda-Ausdrücke gibt, die in Java 6 verfügbar sind. Und das ist cool, es zieht uns zu Kotlin an, aber die Frage stellt sich. Eine der größten und bekanntesten Funktionen der Sprache ist die Interoperabilität mit Java. Wenn Sie alle aufgeführten Funktionen berücksichtigen, warum nicht einfach Bibliotheken in Kotlin schreiben? Mit Java funktionieren sie alle sofort einsatzbereit, und Sie müssen nicht alle diese Wrapper unterstützen. Alle sind glücklich und zufrieden.
Aber in der Praxis ist natürlich nicht alles so rosig wie in Broschüren, es gibt immer ein "kleines Schriftattribut", es gibt scharfe Kanten an der Kreuzung von Kotlin und Java, und heute werden wir ein wenig darüber sprechen.
Scharfe Kanten
Beginnen wir mit den Unterschieden. Wissen Sie zum Beispiel, dass es in Kotlin keine Schlüsselwörter gibt, die flüchtig, synchronisiert, streng, vorübergehend sind? Sie werden durch gleichnamige Anmerkungen im Paket kotlin.jvm ersetzt. Der größte Teil der Konversation wird sich also mit dem Inhalt dieses Pakets befassen.
Es gibt
Timber - eine solche Bibliotheksabstraktion über Holzfäller der berüchtigten
Zheka Vartanov . Sie können es überall in Ihrer Anwendung verwenden, und alles, wo Sie Protokolle senden möchten (an logcat oder an Ihren Server zur Analyse oder zur Absturzberichterstattung usw.), wird zu Plug-Ins.
Stellen wir uns zum Beispiel vor, wir möchten eine ähnliche Bibliothek nur für Analysen schreiben. Auch auskuppeln.
object Analytics { fun send(event: Event) {} fun addPlugins(plugs: List<Plugin>) {} fun getPlugins(): List<Plugin> {} } interface Plugin { fun init() fun send(event: Event) fun close() } data class Event( val name: String, val context: Map<String, Any> = emptyMap() )
Wir nehmen das gleiche Konstruktionsmuster, wir haben einen Einstiegspunkt - das ist Analytics. Wir können dort Ereignisse senden, Plugins hinzufügen und sehen, was wir dort bereits hinzugefügt haben.
Plugin ist eine Plugin-Schnittstelle, die eine bestimmte Analyse-API abstrahiert.
Und tatsächlich die Ereignisklasse, die den Schlüssel und unsere Attribute enthält, die wir senden. In diesem Bericht geht es nicht darum, ob es sich lohnt, Singletones zu verwenden. Lassen Sie uns also keinen Holivar züchten, aber wir werden sehen, wie man das Ganze kämmt.
Jetzt ein kleiner Tauchgang. Hier ist ein Beispiel für die Verwendung unserer Bibliothek in Kotlin:
private fun useAnalytics() { Analytics.send(Event("only_name_event")) val props = mapOf( USER_ID to 1235, "my_custom_attr" to true ) Analytics.send(Event("custom_event", props)) val hasPlugins = Analytics.hasPlugins Analytics.addPlugin(EMPTY_PLUGIN)
Im Prinzip sieht es wie erwartet aus. Ein Einstiegspunkt sind Methoden, die a la statics genannt werden. Ereignis ohne Parameter, Ereignis mit Attributen. Wir überprüfen, ob wir Plugins haben, schieben ein leeres Plugin hinein, um nur eine Art "Trockenlauf" zu machen. Oder fügen Sie einige andere Plugins hinzu, zeigen Sie sie an und so weiter. Im Allgemeinen hoffe ich, dass bei Standardbenutzerfällen bisher alles klar ist.
Nun wollen wir sehen, was in Java passiert, wenn wir dasselbe tun:
private static void useAnalytics() { Analytics.INSTANCE.send(new Event("only_name_event", Collections.emptyMap())); final Map<String, Object> props = new HashMap<>(); props.put(USER_ID, 1235); props.put("my_custom_attr", true); Analytics.INSTANCE.send(new Event("custom_event", props)); boolean hasPlugins = Analytics.INSTANCE.getHasPlugins(); Analytics.INSTANCE.addPlugin(Analytics.INSTANCE.getEMPTY_PLUGIN());
Die Aufregung mit INSTANCE rauscht mir sofort in die Augen, was sich ausdehnt, das Vorhandensein expliziter Werte für den Standardparameter mit Attributen, einige Getter mit dummen Namen. Da wir uns im Allgemeinen hier versammelt haben, um daraus etwas Ähnliches wie die vorherige Datei mit Kotlin zu machen, lassen Sie uns jeden Moment durchgehen, den wir nicht mögen, und versuchen, es irgendwie anzupassen.
Beginnen wir mit Event. Wir entfernen den Parameter Colletions.emptyMap () aus der zweiten Zeile, und ein Compilerfehler wird angezeigt. Was ist der Grund dafür?
data class Event( val name: String, val context: Map<String, Any> = emptyMap() )
Unser Konstruktor hat einen Standardparameter, an den wir den Wert übergeben. Wir kommen von Java nach Kotlin. Es ist logisch anzunehmen, dass das Vorhandensein eines Standardparameters zwei Konstruktoren generiert: einen vollständigen mit zwei Parametern und einen partiellen, für den nur der Name angegeben werden kann. Offensichtlich glaubt der Compiler das nicht. Mal sehen, warum er denkt, wir liegen falsch.
Unser Hauptwerkzeug zur Analyse aller Wendungen, wie Kotlin zu einem JVM-Bytecode wird - Kotlin Bytecode Viewer. In Android Studio und IntelliJ IDEA befindet es sich im Menü Extras - Kotlin - Kotlin-Bytecode anzeigen. Sie können einfach Cmd + Umschalt + A drücken und Kotlin Bytecode in die Suchleiste eingeben.

Hier sehen wir überraschenderweise einen Bytecode dessen, was aus unserer Kotlin-Klasse wird. Ich erwarte nicht, dass Sie über ausgezeichnete Kenntnisse des Bytecodes verfügen, und vor allem erwarten IDE-Entwickler dies auch nicht. Daher haben sie eine Schaltfläche Dekompilieren gemacht.
Nachdem wir darauf geklickt haben, sehen wir einen ungefähr guten Java-Code:
public final class Event { @NotNull private final String name; @NotNull private final Map context; @NotNull public final String getName() { return this.name; } @NotNull public final Map getContext() { return this.context; } public Event(@NotNull String name, @NotNull Map context) { Intrinsics.checkParameterIsNotNull(name, "name"); Intrinsics.checkParameterIsNotNull(context, "context"); super(); this.name = name; this.context = context; }
Wir sehen unsere Felder, Getter, den erwarteten Konstruktor mit zwei Parametern, Name und Kontext, alles läuft gut. Und unten sehen wir den zweiten Konstruktor, und hier ist er mit einer unerwarteten Signatur: nicht mit einem Parameter, sondern aus irgendeinem Grund mit vier.
Hier kann es Ihnen peinlich sein, aber Sie können etwas tiefer klettern und herumwühlen. Wenn wir anfangen zu verstehen, werden wir verstehen, dass DefaultConstructorMarker eine private Klasse aus der Kotlin-Standardbibliothek ist, die hier hinzugefügt wurde, damit es keine Konflikte mit den geschriebenen Konstruktoren gibt, da wir den Parameter vom Typ DefaultConstructorMarker nicht mit unseren Händen festlegen können. Und das Interessante an int var3 ist die Bitmaske, welche Standardwerte wir verwenden sollten. In diesem Fall wissen wir, wenn varmask mit den beiden übereinstimmt, dass var2 nicht gesetzt ist, unsere Attribute nicht gesetzt sind und wir den Standardwert verwenden.
Wie können wir die Situation beheben? Zu diesem Zweck gibt es eine wundersame Anmerkung @JvmOverloads aus dem Paket, über das ich bereits gesprochen habe. Wir müssen es an den Konstruktor hängen.
data class Event @JvmOverloads constructor( val name: String, val context: Map<String, Any> = emptyMap() )
Und was wird sie tun? Wenden wir uns demselben Werkzeug zu. Jetzt sehen wir unseren vollständigen Konstruktor und den Konstruktor mit DefaultConstructorMarker und, siehe da, einen Konstruktor mit einem Parameter, der jetzt in Java verfügbar ist:
@JvmOverloads public Event(@NotNull String name) { this.name, (Map)null, 2, (DefaultConstructorMarker)null); }
Und wie Sie sehen, delegiert er die gesamte Arbeit mit Standardparametern mit Bitmasken an unseren Konstruktor. Daher erzeugen wir keine Informationen darüber, welchen Standardwert wir dort einfügen müssen, sondern delegieren einfach alles in einen Konstruktor. Schön. Wir überprüfen, was wir von der Java-Seite bekommen: Der Compiler ist glücklich und nicht empört.
Mal sehen, was uns als nächstes nicht gefällt. Wir mögen diesen INSTANZ nicht, der in IDEA ein schwieliger in lila ist. Ich mag keine lila Farbe :)

Lassen Sie uns überprüfen, was sich herausstellt. Schauen wir uns den Bytecode noch einmal an.
Zum Beispiel markieren wir die Init-Funktion und stellen sicher, dass Init tatsächlich nicht statisch generiert wird.

Das heißt, was auch immer man sagen mag, wir müssen mit einer Instanz dieser Klasse arbeiten und diese Methoden darauf aufrufen. Wir können jedoch die Generierung all dieser Methoden als statisch erzwingen. Hierfür gibt es eine wunderbare Anmerkung @JvmStatic. Fügen wir es dem Init hinzu und senden Sie Funktionen und überprüfen Sie, was der Compiler jetzt darüber denkt.
Wir sehen, dass das statische Schlüsselwort zu public final init () hinzugefügt wurde, und wir haben uns vor der Arbeit mit INSTANCE bewahrt. Wir werden dies im Java-Code überprüfen.
Der Compiler teilt uns nun mit, dass wir die statische Methode aus dem INSTANCE-Kontext aufrufen. Dies kann korrigiert werden: Drücken Sie Alt + Eingabetaste, wählen Sie "Bereinigungscode" und voila, INSTANZ verschwindet, alles sieht ungefähr so aus wie in Kotlin:
Analytics.send(new Event("only_name_event"));
Jetzt haben wir ein Schema für die Arbeit mit statischen Methoden. Fügen Sie diese Anmerkung hinzu, wo immer es uns wichtig ist:

Und der Kommentar: Wenn die Methoden, die wir haben, offensichtlich die Instanzmethoden sind, dann ist zum Beispiel bei Eigenschaften nicht alles so offensichtlich. Die Felder selbst (z. B. Plugins) werden als statisch generiert. Getter und Setter arbeiten jedoch als Instanzmethoden. Daher müssen Sie für Eigenschaften auch diese Anmerkung hinzufügen, um Setter und Getter statisch zu machen. Zum Beispiel sehen wir die Variable isInited, fügen die Annotation @JvmStatic hinzu und jetzt sehen wir im Kotlin Bytecode Viewer, dass die Methode isInited () statisch geworden ist, alles ist in Ordnung.
Gehen wir jetzt zum Java-Code, "zum Aufräumen", und alles sieht ungefähr wie Kotlin aus, bis auf die Semikolons und das Wort "Neu" - Sie werden sie nicht los.
public static void useAnalytics() { Analytics.send(new Event("only_name_event")); final Map<String, Object> props = new HashMap<>(); props.put(USER_ID, 1235); props.put("my_custom_attr", true); Analytics.send(new Event("custom_event", props)); boolean hasPlugins = Analytics.getHasPlugins(); Analytics.addPlugin(Analytics.INSTANCE.getEMPTY_PLUGIN());
Nächster Schritt: Wir sehen diesen dumm benannten getHasPlugins-Getter mit zwei Präfixen gleichzeitig. Natürlich bin ich kein großer Kenner der englischen Sprache, aber es scheint mir, dass hier etwas anderes impliziert wurde. Warum passiert das?
Wie sie eng mit Kotlin wussten, werden Eigenschaftsnamen für Getter und Setter gemäß den JavaBeans-Regeln generiert. Dies bedeutet, dass Getter im Allgemeinen Get-Präfixe und Setter Setzpräfixe haben. Es gibt jedoch eine Ausnahme: Wenn Sie ein Boolesches Feld haben und dessen Name das Präfix is hat, wird dem Getter das Präfix is vorangestellt. Dies ist im Beispiel des obigen Feldes isInited zu sehen.
Leider sollten bei weitem nicht immer boolesche Felder durchgerufen werden. isPlugins würde nicht ganz das erfüllen, was wir semantisch mit Namen zeigen wollen. Wie geht es uns
Und es ist nicht schwer für uns, dafür gibt es unsere eigene Anmerkung (wie Sie bereits verstanden haben, werde ich dies heute oft wiederholen). Mit der Annotation @JvmName können Sie einen beliebigen Namen angeben (natürlich von Java unterstützt). Fügen Sie es hinzu:
@JvmStatic val hasPlugins @JvmName("hasPlugin") get() = plugins.isNotEmpty()
Lassen Sie uns überprüfen, was wir in Java erhalten haben: Die Methode getHasPlugins ist nicht mehr vorhanden, aber hasPlugins ist etwas für sich. Dies löste unser Problem erneut mit einer Anmerkung. Jetzt lösen wir alle Anmerkungen!
Wie Sie sehen können, setzen wir hier die Anmerkung direkt auf den Getter. Was ist der Grund dafür? Mit der Tatsache, dass unter der Eigenschaft viel von allem ist, und es ist nicht klar, wofür @JvmName gilt. Wenn Sie die Anmerkung an val hasPlugins selbst übertragen, versteht der Compiler nicht, worauf sie angewendet werden soll.
Kotlin kann jedoch auch angeben, wo Anmerkungen direkt darin verwendet werden. Sie können den Ziel-Getter, die gesamte Datei, den Parameter, den Delegaten, das Feld, die Eigenschaften, die Empfängererweiterungsfunktionen, den Setter und den Setter-Parameter angeben. In unserem Fall ist Getter interessant. Und wenn Sie dies mögen, hat dies den gleichen Effekt wie beim Aufhängen der Anmerkung auf get:
@get:JvmName("hasPlugins") @JvmStatic val hasPlugins get() = plugins.isNotEmpty()
Wenn Sie keinen benutzerdefinierten Getter haben, können Sie ihn direkt an Ihr Eigentum anhängen, und alles ist in Ordnung.
Der nächste Punkt, der uns ein wenig verwirrt, ist "Analytics.INSTANCE.getEMPTY_PLUGIN ()". Hier ist die Sache nicht mehr einmal auf Englisch, sondern einfach: WARUM? Die Antwort ist ungefähr gleich, aber zuerst eine kleine Einführung.
Um ein Feld konstant zu machen, haben Sie zwei Möglichkeiten. Wenn Sie eine Konstante als primitiven Typ oder als Zeichenfolge und auch innerhalb des Objekts definieren, können Sie das Schlüsselwort const verwenden, und dann werden Getter-Setter und andere Dinge nicht generiert. Es wird eine gewöhnliche Konstante sein - eine private endgültige Statik - und es wird eingefügt, das heißt, eine absolut gewöhnliche Java-Sache.
Wenn Sie jedoch aus einem Objekt, das sich von der Zeichenfolge unterscheidet, eine Konstante erstellen möchten, können Sie das Wort const hierfür nicht verwenden. Hier haben wir val EMPTY_PLUGIN = EmptyPlugin (), demnach wurde dieser schreckliche Getter offensichtlich generiert. Wir können @JvmName mit einer Anmerkung umbenennen, dieses get-Präfix entfernen, aber es bleibt eine Methode - mit Klammern. Alte Lösungen funktionieren also nicht, wir suchen nach neuen.
Und hier dazu die Anmerkung @JvmField, die besagt: "Ich will hier keine Getter, ich will keine Setter, mach mich zu einem Feld." Stellen Sie es vor val EMPTY_PLUGIN und überprüfen Sie, ob alles wahr ist.

Kotlin Bytecode Viewer zeigt das hervorgehobene Stück an, auf dem Sie gerade in der Datei stehen. Wir stehen jetzt auf EMPTY_PLUGIN, und Sie sehen, dass hier eine Art Initialisierung im Konstruktor geschrieben ist. Tatsache ist, dass der Getter nicht mehr da ist und der Zugriff nur zur Aufnahme dient. Und wenn Sie auf Dekompilieren klicken, sehen wir, dass "public static final EmptyPlugin EMPTY_PLUGIN" angezeigt wurde. Genau das haben wir erreicht. Schön. Wir prüfen, ob alles allen gefällt, insbesondere dem Compiler. Das Wichtigste, was Sie beschwichtigen müssen, ist der Compiler.
Generika
Machen wir eine Pause vom Code und schauen uns die Generika an. Dies ist ein ziemlich heißes Thema. Oder rutschig, wer mag das nicht mehr. Java hat seine eigenen Komplexitäten, aber Kotlin ist anders. Zunächst geht es uns um Variationen. Was ist das?
Variabilität ist eine Möglichkeit, Informationen über eine Typhierarchie von Basistypen auf Ableitungen zu übertragen, z. B. auf Container oder Generika. Hier haben wir die Tier- und Hundeklassen mit einem sehr offensichtlichen Zusammenhang: Hund ist ein Subtyp, Tier ist ein Subtyp, der Pfeil kommt vom Subtyp.

Und welche Verbindung werden ihre Derivate haben? Schauen wir uns einige Fälle an.
Der erste ist Iterator. Um festzustellen, was ein Subtyp und was ein Subtyp ist, orientieren wir uns an der Substitutionsregel Barbara Liskov. Es kann wie folgt formuliert werden: "Der Subtyp sollte nicht mehr erfordern und nicht weniger liefern."
In unserer Situation gibt Iterator uns nur typisierte Objekte, zum Beispiel Animal. Wenn wir Iterator irgendwo akzeptieren, können wir Iterator gut dort einsetzen und Animal von der next () -Methode erhalten, da der Hund auch Animal ist. Wir bieten nicht weniger, sondern mehr, weil ein Hund ein Subtyp ist.

Ich wiederhole: Wir lesen nur von diesem Typ, daher bleibt hier die Beziehung zwischen dem Typ und dem Subtyp erhalten. Und solche Typen werden Kovarianten genannt.
Ein anderer Fall: Aktion. Aktion ist eine Funktion, die nichts zurückgibt, einen Parameter akzeptiert und nur an Aktion schreibt, dh einen Hund oder ein Tier von uns nimmt.

Wir liefern hier also nicht mehr, sondern fordern, und wir dürfen nicht mehr verlangen. Dies bedeutet, dass sich unsere Abhängigkeit ändert. "Nicht mehr" haben wir Tier (Tier weniger als ein Hund). Und solche Typen werden als kontravariante bezeichnet.
Es gibt einen dritten Fall - zum Beispiel ArrayList, aus dem wir lesen und schreiben. Daher verstoßen wir in diesem Fall gegen eine der Regeln, wir benötigen mehr für eine Aufzeichnung (ein Hund, kein Tier). Solche Typen sind in keiner Weise verwandt und werden als invariant bezeichnet.

In Java, als es vor Version 1.5 entworfen wurde (wo Generika erschienen), machten sie Arrays standardmäßig kovariant. Dies bedeutet, dass Sie dem Array von Objekten ein Array von Zeichenfolgen zuweisen können, es dann an die Methode übergeben können, an der das Array von Objekten benötigt wird, und versuchen können, das Objekt dorthin zu verschieben, obwohl dies ein Array von Zeichenfolgen ist. Alles wird auf dich fallen.
Nachdem sie aus bitterer Erfahrung gelernt hatten, dass dies nicht möglich ist, entschieden sie beim Entwerfen von Generika: "Wir werden die Kollektionen unveränderlich machen, wir werden nichts mit ihnen machen."
Und am Ende stellt sich heraus, dass in so einer scheinbar offensichtlichen Sache alles in Ordnung sein sollte, aber eigentlich nicht in Ordnung:
Aber wir müssen irgendwie herausfinden, was wir schließlich können: Wenn wir nur von diesem Blatt lesen, warum nicht die Möglichkeit geben, die Liste der Hunde hier zu übertragen? Daher ist es möglich, mit einem Platzhalter zu charakterisieren, welche Art von Variation dieser Typ haben wird:
List<Dog> dogs = new ArrayList<>(); List<? extends Animal> animals = dogs;
Wie Sie sehen können, ist diese Variante am Verwendungsort angegeben, an dem wir die Hunde zuordnen. Daher wird dies als Use-Site-Varianz bezeichnet.
Was sind die Nachteile davon? Die negative Seite ist, dass Sie diesen beängstigenden Platzhalter überall dort angeben müssen, wo Sie Ihre API verwenden, und all dies ist im Code sehr fruchtbar. Aber in Kotlin funktioniert so etwas aus irgendeinem Grund sofort, und Sie müssen nichts angeben:
val dogs: List<Dog> = ArrayList() val animals: List<Animal> = dogs
Was ist der Grund dafür? Mit der Tatsache, dass die Blätter tatsächlich unterschiedlich sind. Liste in Java bedeutet Schreiben, während es in Kotlin schreibgeschützt ist und nicht impliziert. Daher können wir im Prinzip sofort sagen, dass wir nur von hier aus lesen, daher können wir kovariant sein. Und dies wird genau in der Typdeklaration festgelegt, wobei das Schlüsselwort out den Platzhalter ersetzt:
interface List<out E> : Collection<E>
Dies wird als Varianz der Deklarationsstelle bezeichnet. Daher haben wir alles an einer Stelle angegeben, und wo wir es verwenden, berühren wir dieses Thema nicht mehr. Und das ist Nishtyak.
Zurück zum Code
Gehen wir zurück in unsere Tiefen. Hier haben wir die addPlugins-Methode, es braucht eine Liste:
@JvmStatic fun addPlugins (plugs: List<Plugin>) { plugs.forEach { addPlugin(it) } } , , List<EmptyPlugin>, , : <source lang="java"> final List<EmptyPlugin> pluginsToSet = Arrays.asList(new LoggerPlugin("Alog"), new SegmentPlugin());
Aufgrund der Tatsache, dass List in Kotlin kovariant ist, können wir die Liste der Plugin-Erben hier leicht übergeben. Alles wird funktionieren, der Compiler hat nichts dagegen. Aufgrund der Tatsache, dass wir eine Abweichung von der Deklarationsstelle haben, in der wir alles angegeben haben, können wir die Verbindung mit Java zum Zeitpunkt der Verwendung nicht steuern. Aber was passiert, wenn wir dort wirklich ein Plugin-Blatt wollen, wir wollen dort keine Erben? Es gibt keine Modifikatoren dafür, aber was? Das stimmt, es gibt eine Anmerkung. Und die Annotation heißt @JvmSuppressWildcards, das heißt, wir denken standardmäßig, dass hier ein Typ mit Platzhalter ist, der Typ ist kovariant.
@JvmStatic fun addPlugins(plugs: List<@JvmSuppressWildcards Plugin>) { plugs.forEach { addPlugin(it) } }
Wenn wir SuppressWildcards sprechen, unterdrücken wir alle diese Fragen und unsere Signatur ändert sich tatsächlich. Darüber hinaus zeige ich Ihnen, wie alles im Bytecode aussieht:

Ich werde die Anmerkung vorerst aus dem Code entfernen. Hier ist unsere Methode. Sie wissen wahrscheinlich, dass eine Löschung vorliegt. Und in Ihrem Bytecode gibt es keine Informationen darüber, welche Art von Fragen es gab, also Generika im Allgemeinen. Aber der Compiler folgt dem und signiert es in den Kommentaren zum Bytecode: und dies ist der Typ mit der Frage.

Jetzt fügen wir die Anmerkung erneut ein und sehen, dass dies unser Typ ist, ohne zu hinterfragen.

Jetzt wird unser vorheriger Code nicht mehr kompiliert, weil wir Wildcards abgeschnitten haben. Sie können selbst sehen.
Wir haben kovariante Typen erkannt. Jetzt ist das Gegenteil der Fall.
Wir glauben, dass List eine Frage hat.
Es ist offensichtlich anzunehmen, dass dieses Blatt, wenn es von getPlugins zurückkehrt, auch eine Frage enthält. Was bedeutet das? Dies bedeutet, dass wir nicht in der Lage sein werden, darauf zu schreiben, da der Typ kovariant und nicht kontravariant ist. Werfen wir einen Blick darauf, was in Java passiert. final List<Plugin> plugins = Analytics.getPlugins(); displayPlugins(plugins); Analytics.getPlugins().add(new EmptyPlugin());
Niemand ist empört, dass wir in der letzten Zeile etwas schreiben, was bedeutet, dass jemand hier falsch liegt. Wenn wir uns den Bytecode ansehen, werden wir von der Richtigkeit unserer Vermutungen überzeugt sein. Wir haben keine Anmerkungen und den Typ aus irgendeinem Grund ohne Frage aufgehängt.Überraschung basiert darauf. Kotlin postuliert sich selbst als pragmatische Sprache. Als all dies entworfen wurde, wurden Statistiken gesammelt, da in Java im Allgemeinen Platzhalter verwendet werden. Es stellte sich heraus, dass die Eingabe meistens eine Varianz zulässt, dh die Typen kovariant macht. Nun, es ist nützlich, wo immer wir wollen, dass eine Liste ein Blatt eines beliebigen Plugin-Erben dort ablegen kann. Und hier, wo wir zurückkehren, wollen wir im Gegenteil reine Typen haben: Da es ein Plugin-Blatt gibt, wird es zurückgegeben.Und hier sehen wir aus erster Hand ein Beispiel dafür. Es scheint ein wenig eingängig zu sein, aber es generiert weniger wundervolle Anmerkungen in Ihrem Code, da dies der häufigste Anwendungsfall ist und wenn Sie keine Witze verwenden, funktioniert alles sofort.In diesem Fall sehen wir jedoch, dass eine solche Situation nichts für uns ist, weil wir nicht möchten, dass dort etwas aufgezeichnet wird. Und wir wollen auch nicht, dass dies von Java aus möglich ist. In Kotlin ist List hier ein schreibgeschützter Typ, und wir können dort nichts schreiben, aber der Client unserer Bibliothek kam aus Java und hat alles hineingestopft - wer möchte das? Daher werden wir diese Methode zwingen, eine Liste mit Platzhalter zurückzugeben. Und wir können klarstellen, wie. Wenn wir die Annotation @JvmWildcard hinzufügen, sagen wir: Generieren Sie einen Typ mit einer Frage für uns, alles ist ganz einfach. Nun wollen wir sehen, was an diesem Ort in Java passiert. Java sagt "Was machst du?":
Hier können wir sogar auf die richtige Liste werfen <? erweitert Plugin>, aber sie sagt immer noch "Was machst du?" Und im Prinzip passt diese Situation bisher zu uns. Aber es gibt ein Skript-Kind, das sagt: "Ich habe die Quelle gesehen, es ist eine Open Source, ich weiß, dass es eine ArrayList gibt, und ich werde dich hacken." Und alles wird funktionieren, denn es gibt wirklich eine ArrayList und er weiß, was dort geschrieben werden kann. ((ArrayList<Plugin>) Analytics.getPlugins()).add(new EmptyPlugin());
Hängen Sie daher natürlich coole Anmerkungen auf, aber Sie müssen immer noch das seit langem bekannte defensive Kopieren verwenden. Soryan, nirgendwo ohne ihn, wenn Sie möchten, dass Script-Kiddies Sie nicht stören. @JvmStatic fun getPlugins(): List<@JvmWildcard Plugin> = plugin.toImmutableList()
Ich möchte nur hinzufügen, dass die Annotation @JvmSuppressWildcard sowohl an den Parameter als auch an die Funktion und an die gesamte Klasse angehängt werden kann. Dann wird der Abdeckungsbereich erweitert.Alles scheint in Ordnung zu sein, wir haben unsere Analysen aussortiert. Und jetzt die andere Seite, mit der wir uns nähern können: das Plugin.Wir wollen das Plugin auf der Java-Seite implementieren. Wie die Guten werden wir seine Ausnahme melden: @Override public void send(@NotNull Event event) throws IOException
Hier ist alles sichtbar: interface Plugin { fun init() fun send(event: Event)
Kotlin checked exception. : . , , . Java -. : « Throws - , »:

-, Kotlin? , …
@Throws, . throws- . , IOExeption:
open class EmptyPlugin : Plugin { @Throws(IOException::class) override fun send(event: Event) {}
:
interface Plugin { fun init() @Throws(IOException::class) fun send(event: Event)
? , Java, exception, . , . , - , , @JvmName. .
, Java . …
package util fun List<Int>.printReversedSum() { println(this.foldRight(0) { it, acc -> it + acc }) } @JvmName("printReversedConcatenation") fun List<String>.printReversedSum() { println(this.foldRight(StringBuilder()) { it, acc -> acc.append(it) }) }
Angenommen, es ist uns in Java hier egal, entfernen Sie die Anmerkung. Fehler, jetzt zeigt die IDE einen Fehler in beiden Funktionen. Was denkst du ist der Grund dafür? Ja, ohne Anmerkung werden sie mit demselben Namen generiert, aber hier steht geschrieben, dass einer auf der Liste steht, der andere auf der Liste. Richtig, geben Sie Löschen ein. Wir können diesen Fall sogar überprüfen:
Sie wissen bereits, wie ich es verstehe, dass alle Funktionen der obersten Ebene in einem statischen Kontext generiert werden. Und ohne diese Anmerkung werden wir versuchen, printReversedSum aus List und darunter eine andere ebenfalls aus List zu generieren. Denn der Kotlin-Compiler kennt sich mit Generika aus, der Java-Bytecode jedoch nicht. Daher ist dies der einzige Fall, in dem die Anmerkungen aus dem Paket kotlin.jvm nicht benötigt werden, damit Java gut und bequem ist, sondern damit Ihr Kotlin kompiliert. Wir setzen einen neuen Namen - sobald wir mit Strings arbeiten, verwenden wir die Verkettung - und alles funktioniert einwandfrei, jetzt wird alles kompiliert.Und der zweite Anwenderfall. Es ist damit verbunden. Wir haben eine umgekehrte Erweiterungsfunktion. inline fun String.reverse() = StringBuilder(this).reverse().toString() inline fun <reified T> reversedClassName() = T::class.java.simpleName.reverse() inline fun <T> Iterable<T>.forEachReversed(action: (T) -> Unit) { for (element in this.reversed()) action(element) }
Diese Umkehrung wird in eine statische Klassenmethode namens ReverserKt kompiliert. private static void useUtils() { System.out.println(ReverserKt.reverse("Test")); SumsKt.printReversedSum(asList(1, 2, 3, 4, 5)); SumsKt.printReversedConcatenation(asList("1", "2", "3", "4", "5")); }
Ich denke, das ist nichts Neues für Sie. Die Nuance ist, dass Leute, die unsere Bibliothek in Java benutzen, vermuten können, dass etwas nicht stimmt. Wir haben die Details der Implementierung unserer Bibliothek auf der Seite des Benutzers durchgesickert und möchten unsere Spuren verwischen. Wie können wir das machen? Wie bereits klar, die Anmerkung @JvmName, über die ich jetzt spreche, aber es gibt eine Einschränkung.Zunächst geben wir ihr den gewünschten Namen, wir brennen nicht und es ist wichtig zu sagen, dass wir diese Anmerkung für die Datei verwenden. Wir müssen die Datei umbenennen. @file:Suppress("NOTHING_TO_INLINE") @file:JvmName("ReverserUtils")
Jetzt mag der Java-Compiler ReverserKt nicht, aber es wird erwartet, dass wir es durch ReverserUtils ersetzen und alle sind glücklich. Ein solcher „Benutzerfall 2.1“ ist ein häufiger Fall, wenn Sie mehrere Dateimethoden der obersten Ebene unter einer Klasse und unter einer Fassade sammeln möchten. Zum Beispiel möchten Sie nicht, dass die Methoden der obigen sums.kt von SumsKt aufgerufen werden, sondern dass Sie sich nur um das Umkehren und Zucken von ReverserUtils kümmern. Dann fügen wir dort diese wunderbare @ JvmName-Annotation hinzu, schreiben "ReverserUtils", im Prinzip ist alles in Ordnung, Sie können sogar versuchen, dieses Ding zu kompilieren, aber nein.Obwohl die Umgebung Sie nicht im Voraus warnt, werden Sie beim Kompilieren darauf hingewiesen, dass "Sie zwei Klassen im selben Paket mit demselben Namen, ata, generieren möchten". Was muss getan werden? Fügen Sie die letzte Anmerkung @JvmMultifileClass in dieses Paket ein, die besagt, dass der Inhalt mehrerer Dateien zu einer Klasse wird, dh es gibt nur eine Fassade dafür.In beiden Fällen fügen wir "@file: JvmMultifileClass" hinzu, und Sie können SumsKt durch ReverserUtils ersetzen. Alle sind glücklich - glauben Sie mir. Mit Anmerkungen fertig!Wir haben mit Ihnen über dieses Paket gesprochen, über alle Anmerkungen. Im Prinzip ist bereits aus ihren Namen ersichtlich, wofür sie jeweils verwendet werden. Es gibt schwierige Fälle, in denen Sie beispielsweise benötigen, dass @JvmName in Kotlin sogar einfach zu verwenden ist.Kotlin-spezifisch
Aber höchstwahrscheinlich ist dies nicht alles, was Sie wissen möchten. Es ist auch wichtig zu beachten, wie man mit Kotlin-spezifischen Dingen arbeitet.Zum Beispiel Inline-Funktionen. Sie sind in Kotlin inline und werden anscheinend sogar über Java in Bytecode zugänglich sein? Es stellt sich heraus, dass dies der Fall sein wird, alles in Ordnung sein wird und die Methoden tatsächlich für Java verfügbar sind. Wenn Sie beispielsweise ein Nur-Kotlin-Projekt schreiben, wirkt sich dies nicht sehr gut auf Ihr Dex-Zähllimit aus. Weil sie in Kotlin nicht benötigt werden, aber in Wirklichkeit werden sie in Bytecode sein.Beachten Sie als Nächstes die Parameter des Reified-Typs. Solche Parameter sind spezifisch für Kotlin. Sie sind nur für Inline-Funktionen verfügbar und ermöglichen es Ihnen, Hacks, die in Java nicht verfügbar sind, mit Reflection umzukehren. Da dies nur für Kotlin gilt, ist es nur für Kotlin verfügbar, und in Java können Sie leider keine Funktionen mit reified verwenden.java.lang.Class. Wenn wir ein wenig nachdenken wollen und unsere Bibliothek auch für Java ist, muss sie unterstützt werden. Sehen wir uns ein Beispiel an. Wir haben so ein "Retrofit", das schnell auf mein Knie geschrieben wurde (ich verstehe nicht, was die Jungs so lange geschrieben haben): class Retrofit private constructor( val baseUrl: String, val client: Client ) { fun <T : Any> create(service: Class<T>): T {...} fun <T : Any> create(service: KClass<T>): T { return create(service.java) } }
Es gibt eine Methode, die mit der Java-Klasse funktioniert. Es gibt eine Methode, die mit der Kotlin-KClass funktioniert. Sie müssen nicht zwei verschiedene Implementierungen durchführen. Sie können Erweiterungseigenschaften verwenden, die Class von KClass und KClass von Class erhalten (im Prinzip heißt sie Kotlin offensichtlich).Das wird alles funktionieren, aber es ist ein wenig nicht idiomatisch. Im Kotlin-Code übergeben Sie KClass nicht, sondern schreiben mit Reified-Typen. Daher ist es besser, die Methode wie folgt zu wiederholen: inline fun <reified T : Any> create(): T { return create(T::class.java.java)
. Kotlin , .
val api = retrofit.create(Api::class) val api = retrofit.create<Api>() , ::class . Reified-, -.
Unit. Unit, , void Java, . . , . - Scala, Scala , - , , , void.
In Kotlin ist dies jedoch nicht der Fall. Kotlin hat nur 22 Schnittstellen, die einen anderen Parametersatz akzeptieren und etwas zurückgeben. Somit gibt das Lambda, das Unit zurückgibt, nicht void zurück, sondern Unit. Und das setzt seine Grenzen. Wie sieht das Lambda aus, das Unit zurückgibt? Schauen Sie sie sich jetzt in diesem Codefragment an. Lernen Sie sich kennen. inline fun <T> Iterable<T>.forEachReversed(action: (T) -> Unit) { for (element in this.reversed()) action(element) }
Verwenden Sie es von Kotlin: Alles ist in Ordnung, wir verwenden sogar eine Methodenreferenz, wenn wir können, und es liest sich perfekt, unsere Augen sind nicht gefühllos. private fun useMisc() { listOf(1, 2, 3, 4).forEachReversed(::println) println(reversedClassName<String>()) }
Was ist los in Java? In Java passiert folgendes Kanu: private static void useMisc() { final List<Integer> list = asList(1, 2, 3, 4); ReverserUtils.forEachReversed(list, integer -> { System.out.println(integer); return Unit.INSTANCE; });
Aufgrund der Tatsache, dass wir hier etwas zurückgeben müssen. Es ist wie eine Leere mit einem Großbuchstaben, wir können nicht einfach darauf punkten. Wir können hier nicht die Referenzmethode verwenden, die leider ungültig zurückgibt. Und dies ist wahrscheinlich das erste, was nach all unseren Manipulationen mit Anmerkungen wirklich die Augen berührt. Leider müssen Sie die Unit-Instanz von hier zurückgeben. Sie können sowieso null, niemand braucht es. Ich meine, niemand braucht einen Rückgabewert.Gehen wir weiter: Typealiasen sind auch eine ziemlich spezifische Sache, es sind nur Aliase oder Synonyme, sie sind nur bei Kotlin erhältlich, und in Java werden Sie leider das verwenden, was unter diesen Aliasen steht. Entweder ist dies ein Müll von dreimal eingeschlossenen Generika oder eine Art verschachtelter Klassen. Java-Programmierer sind es gewohnt, damit zu leben.Nun zum interessanten Teil: Sichtbarkeit. Oder vielmehr interne Sichtbarkeit. Sie wissen wahrscheinlich, dass es in Kotlin kein privates Paket gibt. Wenn Sie ohne Modifikatoren schreiben, wird es öffentlich sein. Aber es gibt interne. Intern ist so eine knifflige Sache, dass wir uns das jetzt sogar ansehen werden. In Retrofit haben wir eine interne Validierungsmethode. internal fun validate(): Retrofit { println("!!!!!! internal fun validate() was called !!!!!!") return this }
Es kann nicht von Kotlin aus aufgerufen werden, und das ist verständlich. Was passiert mit Java? Können wir validate anrufen? Vielleicht ist es für Sie kein Geheimnis, dass intern öffentlich wird. Wenn Sie mir nicht glauben, glauben Sie Kotlin Bytecode Viewer.
Dies ist wirklich öffentlich, aber mit einer so schrecklichen Signatur, die auf eine Person hinweist, dass es wahrscheinlich nicht ganz gedacht war, dass ein solches Kriechen in die öffentliche API kriecht. Wenn jemand 80 Zeichen formatiert hat, passt diese Methode möglicherweise nicht einmal in eine Zeile.In Java haben wir jetzt Folgendes: final Api api = retrofit .validate$production_sources_for_module_library_main() .create(Api.class); api.sendMessage("Hello from Java"); }
Versuchen wir, diesen Fall zu kompilieren. Zumindest wird es nicht kompiliert, nicht schon schlecht. Wir könnten hier aufhören, aber lassen Sie mich Ihnen das erklären. Was ist, wenn mir das gefällt? final Api api = retrofit .validate$library() .create(Api.class); api.sendMessage("Hello from Java"); }
Dann kompiliert. Und die Frage stellt sich: "Warum so?" Was soll ich sagen ... MAGIE!Daher ist es sehr wichtig, dass wenn Sie etwas Kritisches in das Interne stecken, dies schlecht ist, da es in Ihre öffentliche API eindringt. Und wenn das Skript-Kind mit einem Kotlin Bytecode Viewer bewaffnet ist, ist es schlecht. Verwenden Sie bei Methoden mit interner Sichtbarkeit nichts sehr Wichtiges.Wenn Sie mehr Freude haben möchten, empfehle ich zwei Dinge. Um das Arbeiten mit dem Bytecode und das Lesen zu vereinfachen, empfehle ich einen Bericht von Zhenya Vartanov. Es gibt ein kostenloses Video , obwohl es sich um das SkillsMatter-Ereignis handelt. Sehr cool.Und eine ziemlich alte Serievon drei Artikeln von Christophe Bales über die verschiedenen Kotlin-Funktionen. Dort ist alles cool geschrieben, etwas ist jetzt irrelevant, aber im Allgemeinen ist es sehr verständlich. Trotzdem mit dem Kotlin Bytecode Viewer und all dem.Vielen Dank!
Wenn Ihnen der Bericht gefallen hat, achten Sie darauf: Am 8. und 9. Dezember findet der neue Mobius in Moskau statt , und dort gibt es auch viele interessante Dinge. Bereits bekannte Informationen zum Programm finden Sie auf der Website. Dort können Tickets gekauft werden.