Wo Java-Programmierer dazu neigen, über Kotlin zu stolpern

Kotlin wird die offizielle Sprache für Android sein wurde auf der Goole I / O 2017 angekündigt. Ich bin mir sicher, dass viele Java-Programmierer in Zukunft Kotlin starten werden. In diesem Beitrag wird daher erläutert, worüber Java-Programmierer in Kotlin stolpern.

Dieser Beitrag ist so geschrieben, dass er unabhängig verstanden werden kann, aber er ist der zweite in der folgenden Reihe. Da davon ausgegangen wird, dass Sie die grundlegende Syntax von Kotlin verstehen, finden Sie in "Fast dasselbe wie Java" die Grundlagen von Kotlin. ..

  1. Fast das gleiche wie Java
  2. ** Orte, die eine neue Denkweise erfordern und zum Stolpern neigen ← In diesem Beitrag behandelte Inhalte **
  3. Bequeme Dinge, die nur Kotlin gibt

Wo Sie eine neue Denkweise brauchen und zum Stolpern neigen

** Wenn Sie ein neues Konzept lernen, können Sie es nicht gut anwenden, ohne nicht nur zu wissen, was Sie tun können, sondern auch, warum es so ist. ** In diesem Abschnitt werde ich erklären, was meiner Meinung nach besonders wichtig ist, da Kotlin sich von Java unterscheidet und eine neue Denkweise erfordert. ** Basierend auf "Warum ist das so?", "Was kann ich tun?" "erklären**.

Smart Cast

Ich weiß nicht, ob es im Allgemeinen eine allgegenwärtige Idee ist, aber ich denke, dass es in Java eine Tendenz gibt, dass "instanceof" nicht gut ist. Die Idee ist, dass das Verzweigen bei "Instanz" und das anschließende Gießen ein letzter Ausweg ist und es wünschenswert ist, etwas mit Polymorphismus zu tun. Es gibt jedoch einen Sprung nach vorne, dass die Lösung für die schlechte "Instanz von" Polymorphismus ist. Es sollte andere Lösungen geben.

Das Problem mit Javas instanceof ist, dass Sie nicht gleichzeitig Typen überprüfen und umwandeln können.

// Java
Animal animal = new Cat();

if (animal instanceof Cat) {
  Cat cat = (Cat)animal; //Niedergeschlagen
  cat.catMethod(); // `Cat`Verarbeitung, die die Methode von verwendet
}

Ich habe einmal mit "instanceof" bestätigt, dass "animal" "Cat" ist, aber dann musste ich mich auf "Cat" stürzen. Downcasting ist ein unsicherer Prozess und ich möchte ihn nach Möglichkeit nicht verwenden. Wenn Sie einen Fehler machen und hier "(Hund) Tier" verwenden, erhalten Sie zur Laufzeit eine "ClassCastException".

Das Lustige ist, dass die vorherige "if" -Anweisung bestätigt, dass "animal" "Cat" ist, aber Sie müssen es explizit wirken. Im Rahmen dieses "Wenn" ist "Tier" garantiert "Katze", daher wäre es schön, wenn der Compiler "Tier" als "Katze" behandeln könnte. Kotlins ** _Smart Cast _ ** macht genau das.

//Kotlin
val animal = Cat()

if (animal is Cat) { // Smart Cast
  animal.catMethod() // `Cat`Verarbeitung, die die Methode von verwendet
}

Smart Cast selbst sieht aus wie eine Art Feature, aber mit Smart Cast verwendet Kotlin normalerweise Cast (von Smart Cast). Insbesondere ist es für die Null-Sicherheit, die später erläutert wird, unverzichtbar, und die versiegelte Klasse, die beim nächsten Mal erläutert wird, ist auch mit Smart Cast kompatibel. Es ist notwendig, sich von der Idee zu lösen, dass es nicht gut ist, den Typ und den Zweig zu überprüfen.

Null Safety

Die Sprachspezifikation, über die Java-Programmierer am meisten verwirrt sind und die ich an Kotlin am erstaunlichsten finde, ist ** Null Safety **.

In Java kann Code wie ↓ eine NullPointerException verursachen.

// Java
String s = foo(); //Kann null sein
int l = s.length(); //NullPointerException zur Laufzeit, wenn null

Das Schreiben von ähnlichem Code in Kotlin führt jedoch zu einem Kompilierungsfehler.

// Kotlin
val s = foo() //Kann null sein
val l = s.length //Kompilierungsfehler

Der Kotlin-Compiler erkennt Code, der zur Laufzeit einen Fehler verursachen kann, und teilt Ihnen dies zur Kompilierungszeit mit. Im Allgemeinen ist es umso schwieriger, mit einem Fehler umzugehen, je verzögerter er ist. Wenn Laufzeitfehler durch Testen nicht erkannt werden können, werden sie möglicherweise als potenzielle Fehler in das Produkt eingebettet und freigegeben. Wenn es sich um einen Kompilierungsfehler handelt, wird er nicht freigegeben, ohne behoben zu werden. ** **.

Dieser Kompilierungsfehler kann durch Aktivieren von "null" behoben werden.

// Kotlin
val s = foo() //Kann null sein
if (s != null) {
  val l = s.length // OK
}

Hier bietet sich Smart Cast an. Die "Null" -Prüfung garantiert, dass "s" nicht "null" ist, und im Rahmen dieses "if" kann "s" als "Zeichenfolge" behandelt werden, die nicht "null" ist.

Welchen Typ hat Smart Cast Casting in diesem Fall für welchen Typ?

Kotlin behandelt die Arten möglicher Werte als "null" und die Arten von Werten, die garantiert nicht null sind. Ersteres heißt ** _Nullable Type _ ** und letzteres heißt ** _ Non-Null Type _ **.

Wie bei Java ist es ein Nicht-Null-Typ, wenn Sie "String" usw. schreiben. In diesem Fall können Sie "null" nicht ersetzen.

// Kotlin
val s: Sting = null //Kompilierungsfehler

Wenn Sie dem ursprünglichen Typ "?" Hinzufügen und "String?" Usw. schreiben, wird der Typ "Nullable". Sie können dem nullbaren Typ "null" zuweisen.

// Kotlin
val s: String? = null // OK

Es ist wichtig zu beachten, dass String und String? Völlig unterschiedliche Typen sind. ** Sie können die Methode "String" nicht für einen Wert vom Typ "String?" Aufrufen. ** Im ersten Beispiel verursacht "s.length" einen Kompilierungsfehler, nicht weil "s" "null" sein kann, sondern "der Typ" String "hat Eigenschaften wie" length ". Nicht weil **. Es ist "String", der "Länge" hat, nicht "String".

Und eine andere wichtige Sache ist, dass ** String ein Subtyp von String? Ist. ** Daher können Sie vom Typ "String" dem Typ "String?" Zuweisen, aber nicht vom Typ "String?" Bis zum Typ "String".

// Kotlin
val a: String = "abc"
val b: String? = a // OK

val c: String? = "xyz"
val d: String = c //Kompilierungsfehler

Dies entspricht der Zuweisung von "Katze" zu "Tier", jedoch nicht umgekehrt.

// Kotlin
val a: Cat = Cat()
val b: Animal = a // OK

val c: Animal = Animal()
val d: Cat = c //Kompilierungsfehler

Zurück zur ursprünglichen Geschichte: Welchen Typ hat Smart Cast mit dem Null-Check auf welchen Typ übertragen? Die Antwort lautet: Ich habe "String" in "String" umgewandelt.

// Kotlin
val s: String? = foo() //Hier`String?`
if (s != null) { //In diesem Rahmen`s`Ist`String`
  val l = s.length
}

Mit anderen Worten, s! = Null funktioniert wies is String. Tatsächlich können Sie das gleiche Ergebnis erzielen, indem Sie wie ↓ schreiben.

// Kotlin
val s: String? = "abc"
if (s is String) { //Smart Cast mit ist statt Nullprüfung
  val l = s.length
}

So verhindert Kotlin "NullPointerException". Wenn Sie Java schreiben, ist es normal, auf "NullPointerException" zu stoßen. Wäre es nicht großartig, Null Safety dort zu haben, wo es nicht passiert?

!!

Null Safety ist großartig, aber der Typ ist nicht universell einsetzbar.

Zum Beispiel hat Kotlins "Liste" (um genau zu sein "Iterable") eine Methode namens "max". Dies gibt das größte Element in "Liste" zurück.

// Kotlin
listOf(2, 7, 5, 3).max() // 7

Der Rückgabewert der "max" -Methode von "List " ist jedoch nicht "Int". Es ist "Int?". Dies liegt daran, dass die leere Liste nicht das größte Element enthält.

// Kotlin
listOf<Int>().max() // null

Angenommen, Sie haben immer eine Liste , die ein oder mehrere Elemente enthält, und Sie möchten das größte Element erhalten und es quadrieren. In Java ist es in Ordnung, den Rückgabewert von "max" ohne nachzudenken zu quadrieren.

// Java
List<Integer> list = ...; //Muss ein oder mehrere Elemente enthalten
int maxValue = Collections.max(list);
int square = maxValue * maxValue; // OK

Aber Kotlin funktioniert nicht.

// Kotlin
val list: List<Int> = ...; //Muss ein oder mehrere Elemente enthalten
val maxValue = list.max()
val square = maxValue * maxValue //Kompilierungsfehler

Weil der Typ von maxValue in ↑ Int? Anstelle von Int ist. Es gibt keinen * Operator, der zwischen Int? Berechnet.

Das Aktivieren von "null", um dies zu lösen, führt zu schrecklichem Code.

// Kotlin
val list: List<Int> = ...; //Muss ein oder mehrere Elemente enthalten
val maxValue = list.max() //maxValue ist`Int?`Schimmel
if (maxValue != null) { //In diesem Rahmen`maxValue`Ist`Int`Schimmel
  val square = maxValue * maxValue // OK
} else {
  throw RuntimeException("Never reaches here.") //Komm niemals her
}

Sie fragen sich vielleicht, ob es eine "else" -Klausel gibt, aber wenn Sie sie nicht schreiben, besteht die Gefahr, dass Sie den Fehler in dem unwahrscheinlichen Fall, dass die "Liste" aufgrund eines Fehlers leer ist, quetschen. ** Crushing-Fehler sind das Schlimmste, was Sie tun können, um das Auffinden eines Fehlers zu verzögern, anstatt einen Laufzeitfehler. ** **.

Es ist jedoch mühsam, jedes Mal einen nutzlosen Null-Check wie ↑ zu schreiben. Dies ist ein unnötiger Prozess in Java. Wenn Sie diesen Code jedes Mal für Null Safety schreiben müssen, ist Null Safety nicht sehr gut. Außerdem wird die mühsame Verarbeitung tendenziell weggelassen. Einige Leute werden die "else" -Klausel nicht schreiben. Dann wird der Fehler beseitigt und es ist der schlimmste.

Das Problem mit diesem Problem ist, dass es schwierig ist, mit einem Typ zu lösen. Kotlin hat keinen Typ wie "eine Liste, die immer mehr als ein Element enthält", und selbst wenn dies der Fall ist, gibt es Fälle, in denen dies nicht gelöst werden kann.

// Kotlin
val list: MutableList<Int> = mutableListOf() //An dieser Stelle leer

...

list.add(42)
val maxValue = list.max() //Es ist hier definitiv nicht leer`maxValue`Ist`Int?`

In der letzten Zeile von ↑ ist list absolut nicht leer, aber der Inhalt von list ist nur eine Instanz von MutableList. Selbst wenn es einen Typ wie "NonEmptyMutableList" (nicht leere Liste) gibt, wird er nicht zu einer Instanz von "NonEmptyMutableList", wenn die Instanz von "MutableList" nicht leer ist.

Ein Postfix-Operator namens !! behandelt solche Probleme. !! konvertiert T? zu T. Wenn der Wert jedoch "null" ist, wird eine Ausnahme ausgelöst.

// Kotlin
val list: List<Int> = ...; //Muss ein oder mehrere Elemente enthalten
val maxValue = list.max()!! // `maxValue`Ist`Int`Schimmel
val square = maxValue * maxValue // OK

!! löst eine NullPointerException aus, wenn sie nicht ungleich Null ist, was die Sicherheit von Null Safety beeinträchtigt. Missbrauche es niemals. Verwenden Sie !! nur, wenn Sie wissen, dass es nicht ** null ist und niemals fehlschlagen wird, wie im obigen Beispiel ** [^ 1]. Obwohl es für den Typ nullbar ist, sollte es nur verwendet werden, wenn gesagt werden kann, dass es aufgrund des Verarbeitungsflusses absolut nicht null ist.

?.

Sie können "?" Verwenden, um eine Methode von "T" für einen Wert vom Typ "T" aufzurufen. Nur wenn er nicht "null" ist.

// Kotlin
val s: String? = ...
val l: Int? = s?.length

Beachten Sie, dass l in ↑ Int? Ist. Wenn s`` null ist, ist das Ergebnis von s? .Length`` null. Daher ist der Rückgabewert von "Länge" "Int", aber in "s? .Length" ist es der nullfähige Typ "Int".

? . Ist praktisch, weil es als foo? .Bar? .Baz geschrieben werden kann, wenn mehrere Prozesse geschrieben werden, die null sein können. Der Missbrauch von "?" Führt jedoch nicht zu sehr guten Ergebnissen. Insbesondere empfehlen wir nicht, Werte als ** Nullable zu behandeln. ** Wenn Sie versuchen, einen Wert zu verwenden, der "null" ist, kennen Sie die Quelle von "null" nicht. Wenn der Grund, warum es "null" wurde, auf einen Fehler zurückzuführen ist, ist es schwierig, den Fehler zu beheben, da Sie nicht wissen, wo das "null" aufgetreten ist. Um eine solche Situation zu verhindern, wird empfohlen, ** Nullable-Werte bereits bei null zu überprüfen und sie ungleich null zu machen. ** **.

?:

Es ist häufig der Fall, dass Sie den Anfangswert eingeben möchten, wenn kein Wert vorhanden ist. In solchen Fällen ist der Operator ?: Nützlich. ?: Ist in der Form foo?: Bar geschrieben, und wenn foo`` null ist, wird es stattdessen bar.

Wenn Sie beispielsweise No Name anzeigen möchten, wenn der Benutzername nicht festgelegt ist, können Sie als ↓ schreiben.

val name: String? = ...
val displayName: String = name ?: "No Name"

Wichtig hierbei ist, dass der Typ von "displayName" "String" anstelle von "String?" Ist. ?: Verwendet die alternative Zeichenfolge No Name "` wenn `name null ist, also immer nicht null`. Daher lautet der Ergebnistyp "String" anstelle von "String?".

Noch bequemer ist die Möglichkeit, Anweisungen wie "return" auf die rechte Seite von "?:" Zu schreiben. Es ist schwierig, mit der "Null" -Prüfung durch "Wenn" zu schreiben, und es ist nützlich, weil ein frühes Entkommen gleichzeitig mit der Substitution realisiert werden kann.

fun foo(list: List<String>) {
  val length: Int = list.min()?.length ?: return // `list`Entkomme früh, wenn es leer ist
  //Verarbeitung, die Länge verwendet
}

?.let

Da ich den Kommentar von ↓ erhalten habe, werde ich kurz erklären, welche Methode in einem solchen Fall verwendet werden kann.

kitakokomo Für Nullable funktioniert "Liste von (1,2) Liste [0] +5 gut, aber mapOf ( 1 bis 11, 2 bis 22) map [1] +5 ist nullable und es ist traurig, dass es nicht funktioniert. "

? . Kann verwendet werden, um Eigenschaften und Methoden in Form von foo? .Bar aufzurufen, aber Nullable-Werte können nicht als Argumente oder Operatoroperanden übergeben werden. In solchen Fällen kann die Methode "let" verwendet werden (ich werde die Methode "let" beim nächsten Mal ausführlich erläutern, daher werde ich diese Methode hier nur vorstellen).

// Kotlin
map[1]?.let { it + 5 }

Auf diese Weise können Sie das Ergebnis des Hinzufügens von "Int?" Und "5", die die Rückgabewerte von "map [1]" sind, als "Int?" Erhalten. Es ist jedoch nicht lesbar und kann bei Missbrauch dazu führen, dass Werte als Nullwert behandelt werden. Verwenden Sie es daher auf ein Minimum und überprüfen Sie das Ergebnis der Berechnung sofort ?: Wir empfehlen, dass Sie es nicht null machen.

Abgesehen davon ist Kotlins Operator ein Methodensyntaxzucker (den ich beim nächsten Mal erläutern werde). Wenn Sie also nur addieren möchten, verwenden Sie die Plus-Methode, die der Hauptteil von + ist, wie ↓ Sie können auch schreiben.

map[1]?.plus(5)

Inline und nicht lokale Rückgabe

Neben Null Safety können Java-Programmierer die nicht-lokale Rückgabe mit Inline-Erweiterung als störend empfinden.

In Kotlin werden Methoden mit dem Modifikator "Inline" inline erweitert. Die Inline-Erweiterung ist ein Mechanismus, mit dem der Compiler die Methodenimplementierung auf den Aufrufer erweitert, um den Aufwand für Methodenaufrufe zu verringern.

↓ ist ein Bild der Inline-Erweiterung.

// Kotlin
//Vor der Inline-Bereitstellung
inline fun square(value: Int): Int {
  return value * value
}

for (i in 1..100) {
  println(square(i))
}
//Nach der Inline-Bereitstellung
for (i in 1..100) {
  println(i * i)
}

In Kotlin ist es jedoch wichtig, dass der Lambda-Ausdruck, der an das Argument der "Inline" -Methode übergeben wird, auch "Inline" erweitert wird.

// Kotlin
//Vor der Inline-Bereitstellung
inline fun forEach(list: List<Int>, operation: (Int) -> Unit) {
  for (element in list) {
    operation(element)
  }
}

val list: List<Int> = ...
forEach(list) { element ->
  println(element)
}
//Nach der Inline-Bereitstellung
val list: List<Int> = ...
for (element in list) {
  println(element)
}

Dies ergibt die gleiche Leistung wie in einer Schleife mit "forEach", "map", "filter", "fold" ("fold" entspricht "redu", was den Anfangswert von Java ergibt) usw. Es kann realisiert werden. Ich habe das "forEach" von ↑ selbst erstellt, aber selbst in der Standardbibliothek sind alle Methoden wie "forEach", "map", "filter" und "fold" von "List" "inline".

Kotlins "Inline" soll in Verbindung mit solchen Lambda-Ausdrücken verwendet werden. Wenn Sie versuchen, eine gewöhnliche Funktion oder Methode wie "↑" Quadrat "" inline "zu machen, sieht der Compiler wie ↓ aus Ich werde eine Warnung ausgeben.

Expected performance impact of inlining 'public inline fun square(value: Int): Int defined in root package' can be insignificant. Inlining works best for functions with lambda parameters

Bisher ist es keine besondere Geschichte. Das verwirrendste für Java-Programmierer ist ein Mechanismus namens ** _Non-local return _ **. Nicht-lokale Rückgabe ist ein Mechanismus, mit dem Sie die Methode außerhalb des Lambda-Ausdrucks aus dem Lambda-Ausdruck heraus "zurückgeben" oder die Schleife außerhalb des Lambda-Ausdrucks "unterbrechen" können.

Dies wird auch anhand eines Beispiels erläutert. Stellen Sie sich eine Methode und vor, die das logische Produkt (&&) aller Elemente von List <Boolean> verwendet. Wenn zum Beispiel "[wahr, wahr, wahr]" ist, ist das Ergebnis von "und" wahr ", aber wenn auch nur ein" falsch "wie" [wahr, falsch, wahr] "gemischt wird, ist das Ergebnis" falsch ". Es wird `.

Die und Methode ist einfach in der erweiterten for Anweisung zu implementieren.

// Java
static boolean and(List<Boolean> list) {
  for (boolean x : list) {
    if (!x) {
      return false;
    }
  }
  return true;
}

In Java können Sie dies jedoch nicht mit "forEach" tun, was fast "for" entspricht. Dies liegt daran, dass es nicht möglich ist, eine Methode außerhalb des Lambda-Ausdrucks innerhalb des Lambda-Ausdrucks zurückzugeben.

// Java
static boolean and(List<Boolean> list) {
  list.forEach(x -> {
    if (!x) {
      return false; //Kompilierungsfehler
    }
  });
  return true;
}

Kotlin erlaubt dies.

// Kotlin
fun and(list: List<Boolean>): Boolean {
  list.forEach { x ->
    if (!x) {
      return false // OK
    }
  }
  return true
}

Dies ist eine nicht lokale Rückgabe.

Es ist unangenehm, die Außenseite des Lambda-Ausdrucks vom Lambda-Ausdruck "zurückgeben" zu können. Wie kannst du das tun? Dies liegt daran, dass "forEach" inline ist. Das Bild nach der Inline-Erweiterung des Codes in ↑ ist ↓.

// Kotlin
fun and(list: List<Boolean>): Boolean {
  for (x in list) {
    if (!x) {
      return false
    }
  }
  return true
}

Es ist kein Wunder, dass Sie in diesem Fall "zurückkehren" können. Die "Rückkehr" innerhalb des "Wenn", die ursprünglich innerhalb des Lambda-Ausdrucks war, ist jetzt die "Rückkehr" des "und", die außerhalb des Lambda-Ausdrucks war.

In Kotlin wirkt "return", das in einen Lambda-Ausdruck geschrieben wird, immer außerhalb des Lambda-Ausdrucks. Sie können "return" nicht verwenden, um den Rückgabewert des Lambda-Ausdrucks selbst zurückzugeben. Der Rückgabewert des Lambda-Ausdrucks ist der Wert des zuletzt ausgewerteten Ausdrucks innerhalb des Lambda-Ausdrucks.

Was ist, wenn Sie "return" in einen Lambda-Ausdruck schreiben, der an eine Methode übergeben wird, die nicht "inline" ist? Es wird nicht inline erweitert, sodass es nicht auf die Außenseite des Lambda-Ausdrucks einwirken kann. In diesem Fall tritt ein Kompilierungsfehler auf.

// Kotlin
// `inline`Wenn nicht
fun foo(f: () -> Int): Int {
  ...
}

fun bar(): Int {
  return foo {
    if (flag) {
      return 0 //Kompilierungsfehler
    }

    42
  }
}

Im Beispiel von ↑ gibt es kein Problem, wenn foo wie ↓ inline` ist.

// Kotlin
// `inline`Im Falle von
inline fun foo(f: () -> Int): Int {
  ...
}

fun bar(): Int {
  return foo {
    if (flag) {
      return 0 // OK: `bar`von`return`
    }

    42
  }
}

** return in einem Lambda-Ausdruck wirkt immer außerhalb des Lambda-Ausdrucks ** ist eine ziemlich drastische Spezifikation aus anderen Sprachen, einschließlich Java, aber hier wird mit Kotlins Lambda-Ausdruck umgegangen. Wenn Sie es nicht gedrückt halten, werden Sie süchtig danach.

Eine nicht lokale Rückgabe ist zunächst unangenehm, aber je nach Verwendung nützlich. In Java können Sie beispielsweise problemlos einen anonymen Bereich mit "{}" erstellen. In Kotlin wird "{}" jedoch einem Lambda-Ausdruck zugewiesen und Sie können keinen anonymen Bereich erstellen. Wenn Sie keinen anonymen Bereich haben, treten Probleme auf, wenn Sie den Bereich in kleinere Teile schneiden möchten, um doppelte Variablennamen zu vermeiden. Kotlin verwendet in solchen Fällen "run".

// Java
for (int x : list) {
  { //Anonymer Geltungsbereich
    int a = 2;
    ...
    if (x < a) {
      break;
    }
  }

  {
    int a = 3; //Weil der Umfang anders ist`a`Kann gemacht werden
    ...
  }
}
// Kotlin
for (x in list) {
  run { //Anstelle eines anonymen Bereichs
    val a = 2
    ...
    if (x < a) {
      break //Lambda-Stil, aber außerhalb der Schleife`break`es kann
    }
  }

  run {
    val a = 3 //Weil der Umfang anders ist`a`Kann gemacht werden
    ...
  }
}

Auf diese Weise können Sie auch die nicht-lokale Rückgabe verwenden, um so etwas wie eine benutzerdefinierte Steuerungssyntax zu erstellen. Missbrauch kann die Lesbarkeit beeinträchtigen, aber es ist eine gute Eigenschaft, dass Sie eine neue Steuerelement-Syntax mit dem Sprachmechanismus lösen können, anstatt die Sprachspezifikation aufzublähen. Lassen Sie uns mit der nicht lokalen Rückkehr gut auskommen.

Anonyme Funktion

Wenn Sie in einem Lambda-Ausdruck nicht "zurückkehren" können, was ist, wenn Sie früh fliehen möchten?

// Kotlin
numbers.forEach { number ->
  if (number % 2 == 1) {
    //Ich möchte hier früh fliehen, was soll ich tun?
  }

  ...
}

In diesem Fall ist es besser, ** _ Anonymous Function _ ** anstelle des Lambda-Ausdrucks zu verwenden.

// Kotlin
numbers.forEach(fun(number: Int) {
  if (number % 2 == 1) {
    return //Frühe Flucht
  }

    ...
})

Im Gegensatz zu Lambda-Ausdrücken bedeutet "return" in einer anonymen Funktion "return" in der anonymen Funktion selbst.

Any und Any?

In Java war die Stammklasse "Objekt". In Kotlin ist das "Any". Bei Verwendung von Java-Methoden aus Kotlin sieht Kotlin überall dort, wo "Object" in Argumenten verwendet wird und Werte in Java zurückgibt, wie "Any" aus. Dies ist jedoch nicht nur eine Namensänderung. Es ist möglich, Javas Object in Kotlin [^ 2] zu verwenden. In Kotlin ist "Object" nicht die Stammklasse, und "Any" ist eine Oberklasse von "Object".

Wofür brauchst du "Any"? In Java war für die Zuweisung von einem primitiven Typ zu "Object" ein einmaliges Boxen in eine Wrapper-Klasse wie "Integer" erforderlich. Es gibt keine typbasierte Ableitung zwischen "int" und "Object". Kotlins "Int" usw. verhalten sich jedoch wie Klassen, so dass "Int" ein Subtyp von "Any" ist. Daher können Sie einer Variablen vom Typ "Any" einfach eine Instanz der Klasse "Int" zuweisen.

// Kotlin
val a: Int = 42
val b: Any = a
if (b is Int) { //Direkt`Int`Überprüfen Sie, ob es sich um eine Instanz von handelt
  //In diesem Bereich`b`Zu`Int`Kann verwendet werden als
  println(b + 1) // 43
}

Dies bedeutet jedoch nicht, dass zur Laufzeit kein internes Boxen vorhanden ist. Ich konnte nichts finden, was direkt in der Dokumentation erwähnt wurde, aber Zuweisungen an "Any" wie "Int" sind im Prinzip [Verhalten für Zuweisungen an Nullable Type](https: //). In Analogie zu kotlinlang.org/docs/reference/basic-types.html#representation) scheint es, dass es intern verpackt wird. Im Gegensatz zu Java kann es syntaktisch verwendet werden, ohne das Boxen zu kennen [^ 3], aber beachten Sie, dass das Zuweisen von "Int" zu "Any" einen Leistungsaufwand hat.

Wenn Sie in Java beim Deklarieren einer Klasse nicht explizit eine Oberklasse mit "extensives" angegeben haben, würde diese Klasse von "Object" erben. Ebenso erbt Kotlin "Any", wenn Sie keine Oberklasse angeben.

Der schwierige Teil ist jedoch, dass Kotlin ** Any-Werte hat, die nicht zugewiesen werden können **. Es ist "null". Dies bedeutet, dass "Any" kein Supertyp vom Typ Nullable ist, wie z. B. "Int?". ** In Kotlin ist der oberste Typ, der im Stammverzeichnis der Typhierarchie vorhanden ist, "Beliebig?". ** Es gibt nichts, was nicht "Any?" Zugeordnet werden kann. Es ist verwirrend, obwohl es ein Name ist, der jedem zugewiesen werden kann, "Any", aber die Typhierarchie kann wie folgt grafisch dargestellt werden [^ 4].

                 [Any?]
                   |
           +---------------+
           |               |
         [Any]  [Alle anderen Nullable]
           |
[Alle anderen Non-null]

Der schwierige Teil, wenn Sie dies nicht verstehen, ist bei der Verwendung von Generika. Wenn Sie verstehen, dass ? Nur ein Symbol ist, mit dem Sie eine Null setzen können, neigen Sie dazu, etwas wie ↓ zu implementieren.

// Kotlin
//Ich möchte auch Nullable Type übergeben können
fun <T> id(x: T?): T? {
    return x
}

Das ** ↑ ? Ist jedoch nicht erforderlich. ** Weil T einen nullbaren Typ darstellen kann. Typparameter dienen als Platzhalter für alle Typen, einschließlich Nullable Type, wenn sie nicht eingeschränkt sind (die Obergrenze ist standardmäßig "Any?").

// Kotlin
//Sie können den Wert Nullable Type weiterhin übergeben
fun <T> id(x: T): T {
    return x
}

Und wenn Sie nur den Wert vom Typ Nicht-Null übergeben möchten, ist dies wie ↓.

// Kotlin
//Nullable Type kann nicht übergeben werden
fun <T: Any> id(x: T): T {
    return x
}

Dies ist eine Einschränkung, dass nur der Typ am Ende des Zweigs "Any" links vom vorherigen Typhierarchiediagramm als "T" verwendet werden kann (die obere Grenze ist "Any"). Es unterscheidet sich nicht davon, etwas wie "<T: Animal>" einzuschränken. Wenn Sie die Untertypisierung von Nullable Type und Non-Null Type nicht verstehen und verstehen, dass ? Ein Symbol ist, mit dem Sie null setzen können, warum T: Any> Sie werden nicht verstehen, ob `jetzt einen Nicht-Null-Typ darstellt.

"Nichts" und "Nichts?"

Die Existenz, die mit "Any" und "Any" gepaart ist, ist "Nothing". Nothing wird als unterster Typ bezeichnet und ist ein Subtyp aller Typen. Dies bedeutet, dass alle Klassen mehrfach vererbt werden und jede Methode aufgerufen werden kann. Natürlich ist ein solcher Typ nicht realisierbar, daher gibt es keine Instanz von "Nichts". Was bedeutet es, an ein solches Fantasiemuster zu denken?

In Kotlin können Sie "Nothing" als Rückgabetyp der Methode angeben. Wie oben erwähnt, gibt es jedoch keine Instanz von "Nichts". Wenn Sie den Typ "Nothing" als Rückgabetyp angeben, den Wert des Typs "Nothing" jedoch nicht "zurückgeben" können, tritt ein Kompilierungsfehler auf. Die einzige Möglichkeit, dies durch die Kompilierung zu erreichen, besteht darin, eine Ausnahme in der Methode auszulösen. Daher zeigt in Kotlin eine Methode, die "Nothing" zurückgibt, immer nicht an, dass sie eine Ausnahme "auslöst".

Dies ist nützlich, wenn Compiler-Vollständigkeitsprüfungen durchgeführt werden. Angenommen, Sie haben eine Methode namens "alwaysFail", die immer fehlschlägt und eine Ausnahme "throw" macht. Betrachten Sie den folgenden Code.

// Java
static void alwaysFail() {
  throw new RuntimeException();
}

static String foo(int bar) {
  switch (bar) {
    case 0:
      return "A";
    case 1:
      return "B";
    case 2:
      return "C";
    default:
      alwaysFail();
  }
} //Kompilierungsfehler

alwaysFail macht immer eine Ausnahme throw, sodass es nie das Ende dieser Methode erreicht. Syntaktisch ist das Ende der Methode jedoch im Fall von "default" erreicht, sodass "foo" zu einem Kompilierungsfehler führt, da einige Flows keinen Rückgabewert von "return" haben.

In Kotlin stellt der Compiler durch Festlegen des Rückgabewerts von "alwaysFail" vom Typ "Nothing" fest, dass die nachfolgende Verarbeitung nicht ausgeführt wird und die Kompilierung auch in dem Fall wie ↑ erfolgreich ist.

// Kotlin
fun alwaysFail(): Nothing {
    throw RuntimeException()
}

fun foo(bar: Int): String {
    when (bar) {
        0 -> return "A"
        1 -> return "B"
        2 -> return "C"
        else -> alwaysFail()
    }
}

Es ist bequem!

Übrigens ist dies ein bisschen beiseite, aber wenn Sie "Irgendwelche?" Haben, sollten Sie auch "Nichts?" Haben. Was bedeutet "Nichts"?

"Nichts?" Zeigt an, dass es "Nichts" oder "Null" ist. Die einzige Instanz von "Nothing" ist "null", da es keine Instanz von "Nothing" gibt. Das heißt, "Nichts?" Ist ein Typ, der nur "Null" enthalten kann. In gewissem Sinne ist "null" wie eine Instanz der "Nothing?" - Klasse (natürlich gibt es wirklich keine Klasse namens "Nothing?").

"Nichts?" Ist auch ein Subtyp aller nullbaren Typen. Es sieht aus wie ↓ in der Abbildung.

                 [Any?]
                   |
           +---------------+
           |               |
         [Any]  [Alle anderen Nullable]
           |               |
[Alle anderen Non-null]   |
           |               |
           |           [Nothing?]
           |               |
           +-------+-------+
                   |
               [Nothing]

Ich glaube nicht, dass Sie viele Chancen haben, "Nothing?" Zu verwenden. Wenn Sie jedoch einen Typ verwenden möchten, der nur "null" enthalten kann, kann es nützlich sein, sich daran zu erinnern, "Nothing?" Zu verwenden.

Generika und Degeneration

Wenn Sie keine Kenntnisse über Java-Platzhalter und -Änderungen haben, können Sie den Inhalt von ↓ nicht verstehen. In diesem Fall empfehlen wir, zuerst [hier] zu lesen (http://qiita.com/koher/items/7dea63e0ce52ebda9873).

Java verwendet Platzhalter wie "List <? Extends Animal>", um die Qualifikation auf der Seite zu steuern, die den generischen Typ verwendet. Dies ist eine Methode namens Use-Site-Varianz. Auf der anderen Seite nutzten C # und Scala die Reflexion von Java und übernahmen eine Methode namens Varianz der Deklarationsstelle. Die Varianz der Deklarationsstelle steuert, wie der Name schon sagt, die Degeneration auf der deklarierenden Seite und nicht auf der Seite, die den Typ verwendet. Kotlin kann beides, aber im Grunde denke ich, dass es besser ist, es so zu gestalten, dass es nur durch Abweichungen von der Deklarationsstelle verwaltet werden kann. Die Standardbibliothek von Kotlin verwendet außerdem überall die Varianz der Deklarationsstelle.

Schauen wir uns ein konkretes Beispiel für die Varianz der Deklarationsstelle an.

Beispielsweise verwendet "Iterable " den Typparameter "T" nur im Rückgabewert. In diesem Fall ist es praktisch, wenn der Benutzer nicht "Iterable <? Extends Animal>" (Java) / "Iterable " (Kotlin) ausführen muss, sondern das "T" von Anfang an kovariant ist.

Andererseits verwendet Comparable <T> den Typparameter T nur als Argument. In diesem Fall wäre es praktisch, wenn der Benutzer nicht "Comparable <? Super Cat>" (Java) / "Comparable " (Kotlin) ausführen müsste, aber das "T" von Anfang an rebellisch ist.

Um dies zu erreichen, fügen Kotlins "Iterable" und "Comparable" "out" und "in" hinzu, wenn Typparameter deklariert werden, um Co-Modifikation und Anti-Modifikation anzugeben. Dies ist die Varianz der Deklarationsstelle.

// Kotlin
interface Iterable<out T> {
  ...
}

interface Comparable<in T> {
  ...
}

Durch Hinzufügen von "out" oder "in", wenn ein Typparameter auf diese Weise deklariert wird, wird "Extends" / "out" oder "Super" / "in" immer angehängt, auch wenn bei der Verwendung nichts hinzugefügt wird. Es wird sich so verhalten. Wenn Sie beispielsweise versuchen, "T" als Methodenargument in "Iterable " [^ 5] zu verwenden, werden Sie vom Compiler als Kompilierungsfehler angezeigt.

Der Designer der Klasse oder Schnittstelle denkt mehr über den Typ als über den Benutzer nach und sollte in der Lage sein, die Änderung entsprechend festzulegen. Umgekehrt hindert uns das Nachdenken über Denaturierung zum Zeitpunkt der Deklaration auch daran, seltsame Typen zu entwerfen, die Denaturierung vernachlässigen. Es ist auch mühsam, jedes Mal, wenn Sie es verwenden, "Extends" / "out" usw. hinzuzufügen. Die Abweichung von der Deklarationsstelle ist ein Ansatz für die Probleme solcher Abweichungen von der Verwendungsstelle.

Explizite Konvertierung primitiver Typen

In Java ist ↓ feiner Code.

// Java
int x = 42;
double y = x; // OK

Kotlin gibt jedoch einen Kompilierungsfehler aus.

// Kotlin
val x: Int = 42
val y: Double = x //Kompilierungsfehler

Im Allgemeinen muss "A" ein Untertyp von "B" sein, um einer Variablen vom Typ "B" einen Wert vom Typ "A" zuweisen zu können. Kotlins "Int" ist jedoch keine Unterklasse von "Double". Wenn es keine Typableitung gibt und Sie diese zuweisen können, bedeutet dies, dass eine implizite Typkonvertierung durchgeführt wurde. Kotlin führt keine implizite Typkonvertierung durch, selbst wenn sie von "Int" zu "Double" wechselt. Dies mag auf den ersten Blick problematisch erscheinen, wenn Sie an Java gewöhnt sind, aber ich denke, es ist eine gute Spezifikation, da die Typbehandlung nicht richtig funktioniert.

Wenn Sie beispielsweise ↓ in Java ausführen, läuft "short" über und zeigt "-32768" an.

// Java
short a = Short.MAX_VALUE;
a++;
System.out.println(a); // -32768

Was wird dann in ↓ angezeigt?

// Java
short a = Short.MAX_VALUE;
short b = 1;
System.out.println(a + b);

In diesem Fall ist es "32768". Dies liegt daran, dass "a" und "b" implizit in "int" konvertiert werden, bevor die Addition von "a + b" durchgeführt wird. Die implizite Typkonvertierung führt zu einem solchen unerwarteten Verhalten, wenn die Spezifikationen nicht gut verstanden werden. Ich denke, es ist ein besserer Ansatz, eine präzise Methode zur expliziten Typkonvertierung bereitzustellen als die implizite Typkonvertierung.

Kotlin verwendet die Methode "toDouble", um explizit von "Int" in "Double" zu konvertieren.

// Kotlin
val x: Int = 42
val y: Double = x.toDouble() // OK

Beachten Sie auch, dass Kotlin Methoden anstelle von Casts für das explizite Casting in Java verwendet, z. B. "Double" bis "Int". Dies liegt daran, dass "Int" und "Double" keine Typableitungsbeziehung haben.

// Java
double x = 42.0;
int y = (int)x; // OK
// Kotlin
val x: Double = 42.0
val y: Int = x as Int // ClassCastException
// Kotlin
val x: Double = 42.0
val y: Int = x.toInt() // OK

Zusammenfassung

In diesem Beitrag habe ich einige Dinge behandelt, über die Java-Programmierer, die mit Kotlin beginnen, möglicherweise stolpern, weil sie neue Ideen benötigen.

** Nächster Beitrag **: Was Java-Programmierer mit Kotlin nützlich finden

[^ 1]: Es ist nicht immer der Code, der garantiert, dass er niemals "null" ist. Wenn ein Benutzer beispielsweise kein Argument an ein Skript übergibt, das ein oder mehrere Argumente übergeben muss, liegt es in der Verantwortung des Benutzers, fehlzuschlagen. Code sollte sicherstellen, dass er auf kritischen Systemen nicht "null" ist, aber in vielen Fällen sollte er vom Benutzer für Einweg-Skripte garantiert werden. In solchen Fällen sind Sie dafür verantwortlich, wenn Sie "!!" "null" sind.

[^ 2]: Wenn ich jedoch versuche, "Object" zu verwenden, erhalte ich eine Compiler-Warnung, dass es nicht verwendet werden sollte.

[^ 3]: Java hat auch Autoboxing, aber Autoboxing konvertiert nur automatisch Typen wie zwischen "int" und "Integer". Das Int von Kotlin ist vom gleichen Typ, unabhängig davon, ob es intern verpackt ist oder nicht, und es gibt keinen Unterschied im Code.

[^ 4]: Da "Int" jedoch ein Subtyp von "Int?" Ist, um genau zu sein, von einzelnen Typen in "allen anderen Nullen" bis zu einzelnen Typen in "allen anderen Nicht-Nullen". Es gibt eine Linie, die sich bis erstreckt.

[^ 5]: Streng genommen kann dieser Ausdruck verkehrt herum verwendet werden, indem T für den Parameter des kontravarianten Typs des Argumenttyps verwendet wird, so dass die Multiplikation von negativ und negativ positiv wird, sodass dieser Ausdruck genau ist. Da ist gar nichts. Für die genauen Regeln (obwohl dies ein Artikel über C # ist) hier Bitte sehen.

Recommended Posts

Wo Java-Programmierer dazu neigen, über Kotlin zu stolpern
Kotlin-Klasse zum Senden an Java-Entwickler
[Android] Konvertieren Sie Android Java-Code in Kotlin
Kotlins Klasse Teil 2 zum Senden an Java-Entwickler
Konvertieren Sie alle Android-Apps (Java) in Kotlin
Kotlin-Scope-Funktionen zum Senden an Java-Entwickler
Was Java-Programmierer mit Kotlin nützlich finden
Einfach mit regulären Java-Ausdrücken zu stolpern
Memo für die Migration von Java nach Kotlin
Kotlin-Funktionen und Lambdas zum Senden an Java-Entwickler
Einführung in Kotlin in ein bestehendes Java Maven-Projekt
Migrieren Sie von Java zu Server Side Kotlin + Spring-Boot
Erste Schritte mit Kotlin zum Senden an Java-Entwickler
Wie schreibe ich Java String # getBytes in Kotlin?
Was Java-Unerfahrene getan haben, um Kotlin zu studieren
Wo ist mit Java?
[Java] Einführung in Java
Einführung in Java
Beherrschen von Kotlin ~ Konvertieren von Java-Dateien in Kotlin-Dateien Road to Graduation ~ Teil 3
Beherrschen von Kotlin ~ Konvertieren von Java-Dateien in Kotlin-Dateien Road to Graduation ~ Teil 2
Beherrschen von Kotlin ~ Konvertieren von Java-Dateien in Kotlin-Dateien Road to Graduation ~ Teil 1
Ich möchte Bildschirmübergänge mit Kotlin und Java machen!