(Veraltet) In Kotlin konvertieren

1. Willkommen!

In diesem Codelab erfahren Sie, wie Sie Ihren Code von Java in Kotlin konvertieren. Außerdem erfahren Sie, was die Konventionen der Kotlin-Programmiersprache sind und wie Sie dafür sorgen können, dass der von Ihnen geschriebene Code diesen entspricht.

Dieses Codelab eignet sich für alle Java-Entwickler, die erwägen, ihr Projekt zu Kotlin zu migrieren. Wir beginnen mit einigen Java-Klassen, die Sie mit der IDE in Kotlin konvertieren. Anschließend sehen wir uns den konvertierten Code an und überlegen, wie wir ihn verbessern können, indem wir ihn idiomatischer gestalten und häufige Fallstricke vermeiden.

Lerninhalte

Sie lernen, wie Sie Java in Kotlin konvertieren. Dabei lernen Sie die folgenden Kotlin-Sprachfunktionen und -Konzepte kennen:

  • Umgang mit Null-Zulässigkeit
  • Singletons implementieren
  • Datenklassen
  • Umgang mit Strings
  • Elvis-Operator
  • Destrukturierung
  • Properties und Back-End-Properties
  • Standardargumente und benannte Parameter
  • Mit Sammlungen arbeiten
  • Erweiterungsfunktionen
  • Funktionen und Parameter der obersten Ebene
  • let-, apply-, with- und run-Keywords

Annahmen

Sie sollten bereits mit Java vertraut sein.

Voraussetzungen

2. Einrichtung

Neues Projekt erstellen

Wenn Sie IntelliJ IDEA verwenden, erstellen Sie ein neues Java-Projekt mit Kotlin/JVM.

Wenn Sie Android Studio verwenden, erstellen Sie ein neues Projekt mit der Vorlage Keine Aktivität. Wählen Sie Kotlin als Projektsprache aus. Der Wert für das Mindest-SDK kann beliebig sein. Er hat keinen Einfluss auf das Ergebnis.

Der Code

Wir erstellen ein User-Modellobjekt und eine Repository-Singleton-Klasse, die mit User-Objekten funktioniert und Listen von Nutzern und formatierte Nutzernamen bereitstellt.

Erstellen Sie unter „app/java/<IhrPaketname>“ eine neue Datei mit dem Namen User.java und fügen Sie den folgenden Code ein:

public class User {

    @Nullable
    private String firstName;
    @Nullable
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

}

In der IDE wird angezeigt, dass @Nullable nicht definiert ist. Importieren Sie also androidx.annotation.Nullable, wenn Sie Android Studio verwenden, oder org.jetbrains.annotations.Nullable, wenn Sie IntelliJ verwenden.

Erstellen Sie eine neue Datei mit dem Namen Repository.java und fügen Sie den folgenden Code ein:

import java.util.ArrayList;
import java.util.List;

public class Repository {

    private static Repository INSTANCE = null;

    private List<User> users = null;

    public static Repository getInstance() {
        if (INSTANCE == null) {
            synchronized (Repository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Repository();
                }
            }
        }
        return INSTANCE;
    }

    // keeping the constructor private to enforce the usage of getInstance
    private Repository() {

        User user1 = new User("Jane", "");
        User user2 = new User("John", null);
        User user3 = new User("Anne", "Doe");

        users = new ArrayList();
        users.add(user1);
        users.add(user2);
        users.add(user3);
    }

    public List<User> getUsers() {
        return users;
    }

    public List<String> getFormattedUserNames() {
        List<String> userNames = new ArrayList<>(users.size());
        for (User user : users) {
            String name;

            if (user.getLastName() != null) {
                if (user.getFirstName() != null) {
                    name = user.getFirstName() + " " + user.getLastName();
                } else {
                    name = user.getLastName();
                }
            } else if (user.getFirstName() != null) {
                name = user.getFirstName();
            } else {
                name = "Unknown";
            }
            userNames.add(name);
        }
        return userNames;
    }
}

3. Gültigkeit, „val“, „var“ und Datenklassen deklarieren

Unsere IDE kann Java-Code ziemlich gut automatisch in Kotlin-Code konvertieren, manchmal braucht sie aber ein wenig Hilfe. Lassen Sie uns die IDE die Umwandlung vornehmen. Anschließend sehen wir uns den resultierenden Code an, um zu verstehen, wie und warum er so konvertiert wurde.

Rufen Sie die Datei User.java auf und konvertieren Sie sie in Kotlin: Menüleiste -> Code -> Java-Datei in Kotlin-Datei konvertieren.

Wenn Sie nach der Umwandlung in der IDE aufgefordert werden, eine Korrektur vorzunehmen, drücken Sie Ja.

e6f96eace5dabe5f.png

Sie sollten den folgenden Kotlin-Code sehen:

class User(var firstName: String?, var lastName: String?)

Beachten Sie, dass User.java in User.kt umbenannt wurde. Kotlin-Dateien haben die Erweiterung „.kt“.

In unserer Java-Klasse User gab es zwei Eigenschaften: firstName und lastName. Jedes hatte eine Getter- und eine Setter-Methode, wodurch der Wert veränderbar war. Das Schlüsselwort für mutable Variablen in Kotlin ist var. Daher verwendet der Konverter var für jede dieser Eigenschaften. Wenn unsere Java-Properties nur Getter hätten, wären sie schreibgeschützt und würden als val-Variablen deklariert. val ähnelt dem Java-Keyword final.

Einer der Hauptunterschiede zwischen Kotlin und Java besteht darin, dass in Kotlin explizit angegeben wird, ob eine Variable einen Nullwert akzeptieren kann. Dazu wird der Typdeklaration ein ? angehängt.

Da wir firstName und lastName als nullable gekennzeichnet haben, hat der automatische Konverter die Properties automatisch mit String? als nullable gekennzeichnet. Wenn Sie Ihre Java-Mitglieder als nicht null annotieren (mit org.jetbrains.annotations.NotNull oder androidx.annotation.NonNull), erkennt der Konverter dies und setzt die Felder auch in Kotlin auf „nicht null“.

Die grundlegende Umstellung ist bereits abgeschlossen. Wir können das aber auch idiomatischer formulieren. Sehen wir uns an, wie das geht.

Datenklasse

Die User-Klasse enthält nur Daten. Kotlin hat ein Keyword für Klassen mit dieser Rolle: data. Wenn wir diese Klasse als data-Klasse kennzeichnen, erstellt der Compiler automatisch Getter und Setter für uns. Außerdem werden die Funktionen equals(), hashCode() und toString() abgeleitet.

Fügen wir der Klasse User das Keyword data hinzu:

data class User(var firstName: String?, var lastName: String?)

Kotlin kann wie Java einen primären und einen oder mehrere sekundäre Konstruktoren haben. Der Konstruktor im obigen Beispiel ist der primäre Konstruktor der Klasse User. Wenn Sie eine Java-Klasse mit mehreren Konstruktoren konvertieren, erstellt der Konverter automatisch auch mehrere Konstruktoren in Kotlin. Sie werden mit dem Keyword constructor definiert.

Wenn wir eine Instanz dieser Klasse erstellen möchten, können wir das so tun:

val user1 = User("Jane", "Doe")

Gleichberechtigung

In Kotlin gibt es zwei Arten von Gleichheit:

  • Bei der strukturellen Gleichheit wird der Operator == verwendet und equals() aufgerufen, um zu ermitteln, ob zwei Instanzen gleich sind.
  • Bei der referenziellen Gleichheit wird der Operator === verwendet, um zu prüfen, ob zwei Verweise auf dasselbe Objekt verweisen.

Die im primären Konstruktor der Datenklasse definierten Properties werden für strukturelle Gleichheitsüberprüfungen verwendet.

val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false

4. Standardargumente, benannte Argumente

In Kotlin können wir Argumenten in Funktionsaufrufen Standardwerte zuweisen. Wenn das Argument weggelassen wird, wird der Standardwert verwendet. In Kotlin sind Konstruktoren auch Funktionen. Daher können wir mithilfe von Standardargumenten angeben, dass der Standardwert von lastName null ist. Dazu weisen wir lastName einfach null zu.

data class User(var firstName: String?, var lastName: String? = null)

// usage
val jane = User("Jane") // same as User("Jane", null)
val joe = User("Joe", "Doe")

In Kotlin können Sie Ihre Argumente beim Aufrufen Ihrer Funktionen beschriften:

val john = User(firstName = "John", lastName = "Doe") 

Angenommen, firstName hat null als Standardwert, lastName hingegen nicht. Da der Standardparameter in diesem Fall einem Parameter ohne Standardwert vorangestellt wäre, müssen Sie die Funktion mit benannten Argumenten aufrufen:

data class User(var firstName: String? = null, var lastName: String?)

// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")

Standardwerte sind ein wichtiges und häufig verwendetes Konzept in Kotlin-Code. In unserem Codelab möchten wir immer den Vor- und Nachnamen in einer User-Objektdeklaration angeben, sodass wir keine Standardwerte benötigen.

5. Objektinitialisierung, Companion-Objekt und Singleton

Bevor Sie mit dem Codelab fortfahren, prüfen Sie, ob Ihre User-Klasse eine data-Klasse ist. Konvertieren wir nun die Klasse Repository in Kotlin. Das Ergebnis der automatischen Umwandlung sollte so aussehen:

import java.util.*

class Repository private constructor() {
    private var users: MutableList<User?>? = null
    fun getUsers(): List<User?>? {
        return users
    }

    val formattedUserNames: List<String?>
        get() {
            val userNames: MutableList<String?> =
                ArrayList(users!!.size)
            for (user in users) {
                var name: String
                name = if (user!!.lastName != null) {
                    if (user!!.firstName != null) {
                        user!!.firstName + " " + user!!.lastName
                    } else {
                        user!!.lastName
                    }
                } else if (user!!.firstName != null) {
                    user!!.firstName
                } else {
                    "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

    companion object {
        private var INSTANCE: Repository? = null
        val instance: Repository?
            get() {
                if (INSTANCE == null) {
                    synchronized(Repository::class.java) {
                        if (INSTANCE == null) {
                            INSTANCE =
                                Repository()
                        }
                    }
                }
                return INSTANCE
            }
    }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Sehen wir uns an, was der automatische Konverter getan hat:

  • Die Liste von users ist nullable, da das Objekt bei der Deklaration nicht instanziiert wurde.
  • Funktionen in Kotlin wie getUsers() werden mit dem Modifikator fun deklariert.
  • Die Methode getFormattedUserNames() ist jetzt ein Attribut namens formattedUserNames.
  • Die Iteration über die Liste der Nutzer (ursprünglich Teil von getFormattedUserNames() hat eine andere Syntax als die Java-Syntax.
  • Das Feld static ist jetzt Teil eines companion object-Blocks
  • Ein init-Block wurde hinzugefügt

Bevor wir fortfahren, sollten wir den Code etwas optimieren. Wenn wir uns den Konstruktor ansehen, stellen wir fest, dass der Konverter unsere users-Liste in eine veränderliche Liste umgewandelt hat, die Objekte vom Typ „Nullable“ enthält. Die Liste kann zwar null sein, aber nehmen wir an, dass sie keine Nutzer mit null enthalten kann. Gehen wir so vor:

  • Entfernen Sie das ? in User? in der users-Typdeklaration.
  • Entfernen Sie das ? in User? für den Rückgabetyp getUsers(), damit List<User>? zurückgegeben wird.

Init-Block

In Kotlin darf der primäre Konstruktor keinen Code enthalten. Daher wird der Initialisierungscode in init-Blöcken platziert. Die Funktionalität bleibt gleich.

class Repository private constructor() {
    ...
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Ein Großteil des init-Codes dient der Initialisierung von Eigenschaften. Das kann auch in der Deklaration der Property erfolgen. In der Kotlin-Version unserer Repository-Klasse sehen wir beispielsweise, dass die Eigenschaft „Nutzer“ in der Deklaration initialisiert wurde.

private var users: MutableList<User>? = null

static Eigenschaften und Methoden von Kotlin

In Java verwenden wir das Keyword static für Felder oder Funktionen, um anzugeben, dass sie zu einer Klasse, aber nicht zu einer Instanz der Klasse gehören. Aus diesem Grund haben wir das statische Feld INSTANCE in unserer Klasse Repository erstellt. Das Kotlin-Äquivalent dazu ist der companion object-Block. Hier deklarieren Sie auch die statischen Felder und Funktionen. Der Konverter hat den Block für das Companion-Objekt erstellt und das Feld INSTANCE dorthin verschoben.

Singletons behandeln

Da wir nur eine Instanz der Klasse Repository benötigen, haben wir das Singleton-Muster in Java verwendet. In Kotlin können Sie dieses Muster auf Compilerebene erzwingen, indem Sie das Schlüsselwort class durch object ersetzen.

Entfernen Sie den privaten Konstruktor und ersetzen Sie die Klassendefinition durch object Repository. Entfernen Sie auch das zugehörige Objekt.

object Repository {

    private var users: MutableList<User>? = null
    fun getUsers(): List<User>? {
       return users
    }

    val formattedUserNames: List<String>
        get() {
            val userNames: MutableList<String> =
                ArrayList(users!!.size)
        for (user in users) {
            var name: String
            name = if (user!!.lastName != null) {
                if (user!!.firstName != null) {
                    user!!.firstName + " " + user!!.lastName
                } else {
                    user!!.lastName
                }
            } else if (user!!.firstName != null) {
                user!!.firstName
            } else {
                "Unknown"
            }
            userNames.add(name)
       }
       return userNames
   }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Bei der Verwendung der Klasse object werden Funktionen und Eigenschaften einfach direkt auf dem Objekt aufgerufen, so:

val formattedUserNames = Repository.formattedUserNames

Wenn für eine Property kein Sichtbarkeitsmodifikator festgelegt ist, ist sie standardmäßig öffentlich, wie im Fall der Property formattedUserNames im Objekt Repository.

6. Umgang mit Null-Zulässigkeit

Beim Konvertieren der Klasse Repository in Kotlin hat der automatische Konverter die Liste der Nutzer als nullable deklariert, da sie bei der Deklarierung nicht mit einem Objekt initialisiert wurde. Daher muss für alle Verwendungen des users-Objekts der Operator „Nicht null“ !! verwendet werden. Im konvertierten Code werden users!! und user!! verwendet. Der Operator !! wandelt jede Variable in einen nicht-null-Typ um, damit Sie auf Eigenschaften zugreifen oder Funktionen darauf aufrufen können. Wenn der Variablenwert jedoch tatsächlich null ist, wird eine Ausnahme ausgelöst. Wenn Sie !! verwenden, besteht das Risiko, dass bei der Laufzeit Ausnahmen geworfen werden.

Verwenden Sie stattdessen eine der folgenden Methoden, um Nullbarkeit zu behandeln:

  • Null-Prüfung ( if (users != null) {...} )
  • Mit dem Elvis-Operator ?: (wird später in diesem Codelab behandelt)
  • Einige der Kotlin-Standardfunktionen verwenden (wird später im Codelab behandelt)

In unserem Fall wissen wir, dass die Liste der Nutzer nicht nullable sein muss, da sie direkt nach dem Erstellen des Objekts (im Block init) initialisiert wird. So können wir das users-Objekt direkt bei der Deklaration instanziieren.

Beim Erstellen von Instanzen von Sammlungstypen bietet Kotlin mehrere Hilfsfunktionen, um Ihren Code übersichtlicher und flexibler zu gestalten. Hier verwenden wir ein MutableList für users:

private var users: MutableList<User>? = null

Der Einfachheit halber können wir die Funktion mutableListOf() verwenden und den Typ des Listenelements angeben. Mit mutableListOf<User>() wird eine leere Liste erstellt, die User Objekte aufnehmen kann. Da der Datentyp der Variablen jetzt vom Compiler abgeleitet werden kann, entfernen Sie die explizite Typdeklaration der users-Property.

private val users = mutableListOf<User>()

Außerdem haben wir var in val geändert, da Nutzer einen schreibgeschützten Verweis auf die Nutzerliste enthalten. Die Referenz ist schreibgeschützt und kann daher niemals auf eine neue Liste verweisen. Die Liste selbst ist jedoch veränderbar (Sie können Elemente hinzufügen oder entfernen).

Da die Variable users bereits initialisiert ist, entfernen Sie diese Initialisierung aus dem Block init:

users = ArrayList<Any?>()

Der Block init sollte dann so aussehen:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    users.add(user1)
    users.add(user2)
    users.add(user3)
}

Durch diese Änderungen ist die Property users jetzt nicht mehr null und wir können alle unnötigen Vorkommen des Operators !! entfernen. In Android Studio werden weiterhin Kompilierungsfehler angezeigt. Fahren Sie jedoch mit den nächsten Schritten der Codelabs fort, um sie zu beheben.

val userNames: MutableList<String?> = ArrayList(users.size)
for (user in users) {
    var name: String
    name = if (user.lastName != null) {
        if (user.firstName != null) {
            user.firstName + " " + user.lastName
        } else {
            user.lastName
        }
    } else if (user.firstName != null) {
        user.firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

Wenn Sie für den Wert userNames den Typ ArrayList angeben, der Strings enthält, können Sie den expliziten Typ in der Deklaration entfernen, da er abgeleitet wird.

val userNames = ArrayList<String>(users.size)

Destrukturierung

In Kotlin können Sie ein Objekt mithilfe einer Syntax, die als Destrukturierungsdeklaration bezeichnet wird, in eine Reihe von Variablen zerlegen. Wir erstellen mehrere Variablen und können sie unabhängig voneinander verwenden.

data-Klassen unterstützen beispielsweise die Destrukturierung, sodass wir das User-Objekt in der for-Schleife in (firstName, lastName) destrukturieren können. So können wir direkt mit den Werten firstName und lastName arbeiten. Aktualisieren Sie die for-Schleife wie unten dargestellt. Ersetzen Sie alle Instanzen von user.firstName durch firstName und user.lastName durch lastName.

for ((firstName, lastName) in users) {
    var name: String
    name = if (lastName != null) {
        if (firstName != null) {
            firstName + " " + lastName
        } else {
            lastName
        }
    } else if (firstName != null) {
        firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

if-Ausdruck

Die Namen in der Liste der Nutzernamen sind noch nicht im gewünschten Format. Da sowohl lastName als auch firstName null sein können, müssen wir beim Erstellen der Liste der formatierten Nutzernamen Nullwerte berücksichtigen. Wenn einer der Namen fehlt, soll "Unknown" angezeigt werden. Da die Variable name nach der Ersteinrichtung nicht mehr geändert wird, können wir val anstelle von var verwenden. Nehmen Sie diese Änderung zuerst vor.

val name: String

Sehen Sie sich den Code an, mit dem die Variable „name“ festgelegt wird. Vielleicht ist es neu für Sie, dass eine Variable auf einen if-/else-Codeblock festgelegt wird. Das ist zulässig, da if und when in Kotlin Ausdrücke sind, die einen Wert zurückgeben. Die letzte Zeile der if-Anweisung wird name zugewiesen. Dieser Block dient nur zum Initialisieren des Werts name.

Im Wesentlichen bedeutet diese Logik, dass name entweder auf firstName oder "Unknown" gesetzt wird, wenn lastName null ist.

name = if (lastName != null) {
    if (firstName != null) {
        firstName + " " + lastName
    } else {
        lastName
    }
} else if (firstName != null) {
    firstName
} else {
    "Unknown"
}

Elvis-Operator

Dieser Code kann mit dem Elvis-Operator ?: idiomatischer geschrieben werden. Der elvis-Operator gibt den Ausdruck auf der linken Seite zurück, wenn er nicht null ist, oder den Ausdruck auf der rechten Seite, wenn die linke Seite null ist.

Im folgenden Code wird also firstName zurückgegeben, wenn es nicht null ist. Wenn firstName null ist, gibt der Ausdruck den Wert rechts davon, "Unknown", zurück:

name = if (lastName != null) {
    ...
} else {
    firstName ?: "Unknown"
}

7. Stringvorlagen

In Kotlin können Sie mithilfe von String-Vorlagen ganz einfach mit Strings arbeiten. Mit Stringvorlagen können Sie in Stringdeklarationen auf Variablen verweisen, indem Sie vor der Variablen das Dollarzeichen $ verwenden. Sie können einen Ausdruck auch in eine Stringdeklaration einfügen, indem Sie ihn in { } setzen und davor das $-Symbol verwenden. Beispiel: ${user.firstName}.

In Ihrem Code wird derzeit die Stringkonkatenierung verwendet, um firstName und lastName zum Nutzernamen zusammenzuführen.

if (firstName != null) {
    firstName + " " + lastName
}

Ersetzen Sie stattdessen die Stringkonkatenierung durch:

if (firstName != null) {
    "$firstName $lastName"
}

Mit String-Vorlagen können Sie Ihren Code vereinfachen.

Ihre IDE zeigt Ihnen Warnungen an, wenn es eine idiomatischere Möglichkeit gibt, Ihren Code zu schreiben. Sie sehen im Code eine gestrichelte Unterstreichung. Wenn Sie den Mauszeiger darauf bewegen, wird ein Vorschlag zur Refaktorisierung des Codes angezeigt.

Derzeit sollte eine Warnung angezeigt werden, dass die name-Deklaration mit der Zuweisung zusammengeführt werden kann. Probieren wir das aus. Da der Typ der Variablen name abgeleitet werden kann, können wir die explizite Typdeklaration String entfernen. Jetzt sieht unsere formattedUserNames so aus:

val formattedUserNames: List<String?>
    get() {
        val userNames = ArrayList<String>(users.size)
        for ((firstName, lastName) in users) {
            val name = if (lastName != null) {
                if (firstName != null) {
                    "$firstName $lastName"
                } else {
                    lastName
                }
            } else {
                firstName ?: "Unknown"
            }
            userNames.add(name)
        }
        return userNames
    }

Wir können noch eine weitere Anpassung vornehmen. Wenn der Vorname und der Nachname fehlen, wird in unserer UI-Logik "Unknown" angezeigt. Nullobjekte werden nicht unterstützt. Ersetzen Sie also für den Datentyp formattedUserNames List<String?> durch List<String>.

val formattedUserNames: List<String>

8. Vorgänge für Sammlungen

Sehen wir uns den formattedUserNames-Getter genauer an und überlegen, wie wir ihn idiomatischer gestalten können. Derzeit führt der Code Folgendes aus:

  • Erstellt eine neue Liste von Strings.
  • Durchläuft die Liste der Nutzer
  • Erstellt den formatierten Namen für jeden Nutzer anhand seines Vor- und Nachnamens.
  • Die neu erstellte Liste wird zurückgegeben.
    val formattedUserNames: List<String>
        get() {
            val userNames = ArrayList<String>(users.size)
            for ((firstName, lastName) in users) {
                val name = if (lastName != null) {
                    if (firstName != null) {
                        "$firstName $lastName"
                    } else {
                        lastName
                    }
                } else {
                    firstName ?: "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

Kotlin bietet eine umfangreiche Liste von Sammlungstransformationen, die die Entwicklung durch Erweiterung der Funktionen der Java Collections API schneller und sicherer machen. Eine davon ist die Funktion map. Diese Funktion gibt eine neue Liste zurück, die die Ergebnisse der Anwendung der angegebenen Transformationsfunktion auf jedes Element in der ursprünglichen Liste enthält. Anstatt also eine neue Liste zu erstellen und die Liste der Nutzer manuell durchzugehen, können wir die Funktion map verwenden und die Logik aus der for-Schleife in den map-Body verschieben. Standardmäßig lautet der Name des aktuellen Listenelements in map it. Sie können it jedoch aus Gründen der Lesbarkeit durch einen eigenen Variablennamen ersetzen. In unserem Fall nennen wir es user:

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                val name = if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
                name
            }
        }

Beachten Sie, dass wir den Elvis-Operator verwenden, um "Unknown" zurückzugeben, wenn user.lastName null ist, da user.lastName vom Typ String? ist und für name ein String erforderlich ist.

...
else {
    user.lastName ?: "Unknown"
}
...

Um das noch einfacher zu machen, können wir die Variable name vollständig entfernen:

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

9. Properties und Back-End-Properties

Wir haben gesehen, dass der automatische Konverter die Funktion getFormattedUserNames() durch ein Attribut namens formattedUserNames mit einem benutzerdefinierten Getter ersetzt hat. Intern generiert Kotlin weiterhin eine getFormattedUserNames()-Methode, die eine List zurückgibt.

In Java würden wir unsere Klasseneigenschaften über Getter- und Setter-Funktionen freigeben. Mit Kotlin können wir besser zwischen den Eigenschaften einer Klasse, die mit Feldern ausgedrückt werden, und den Funktionen, also den Aktionen, die eine Klasse ausführen kann, die mit Funktionen ausgedrückt werden, unterscheiden. In unserem Fall ist die Repository-Klasse sehr einfach und führt keine Aktionen aus. Sie enthält daher nur Felder.

Die Logik, die in der Java-Funktion getFormattedUserNames() ausgelöst wurde, wird jetzt beim Aufrufen des Getters der Kotlin-Property formattedUserNames ausgelöst.

Wir haben zwar kein explizites Feld, das der Eigenschaft formattedUserNames entspricht, aber Kotlin bietet uns ein automatisches Back-End-Feld namens field, auf das wir bei Bedarf über benutzerdefinierte Getter und Setter zugreifen können.

Manchmal benötigen wir jedoch zusätzliche Funktionen, die das automatische Sicherungsfeld nicht bietet.

Sehen wir uns ein Beispiel an.

In unserer Repository-Klasse haben wir eine veränderbare Liste von Nutzern, die in der Funktion getUsers() freigegeben wird, die aus unserem Java-Code generiert wurde:

fun getUsers(): List<User>? {
    return users
}

Da wir nicht wollten, dass die Aufrufer der Repository-Klasse die Nutzerliste ändern, haben wir die Funktion getUsers() erstellt, die eine schreibgeschützte List<User> zurückgibt. In Kotlin empfehlen wir in solchen Fällen eher die Verwendung von Properties als von Funktionen. Genauer gesagt würden wir eine schreibgeschützte List<User> freigeben, die von einer mutableListOf<User> unterstützt wird.

Benennen wir zuerst users in _users um. Markieren Sie den Variablennamen und klicken Sie mit der rechten Maustaste auf Umstrukturieren > Umbenennen. Fügen Sie dann eine öffentliche, schreibgeschützte Property hinzu, die eine Liste von Nutzern zurückgibt. Nennen wir sie users:

private val _users = mutableListOf<User>()
val users: List<User>
    get() = _users

An diesem Punkt können Sie die Methode getUsers() löschen.

Durch die oben genannte Änderung wird die private Property _users zur unterstützenden Property für die öffentliche Property users. Außerhalb der Repository-Klasse kann die _users-Liste nicht geändert werden, da Nutzer der Klasse nur über users auf die Liste zugreifen können.

Wenn users aus Kotlin-Code aufgerufen wird, wird die List-Implementierung aus der Kotlin-Standardbibliothek verwendet, bei der die Liste nicht geändert werden kann. Wenn users von Java aufgerufen wird, wird die java.util.List-Implementierung verwendet, bei der die Liste geändert werden kann und Vorgänge wie add() und remove() verfügbar sind.

Vollständiger Code:

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

10. Funktionen und Properties der obersten Ebene und Erweiterungen

Derzeit weiß die Repository-Klasse, wie der formatierte Nutzername für ein User-Objekt berechnet wird. Wenn wir dieselbe Formatierungslogik jedoch in anderen Klassen wiederverwenden möchten, müssen wir sie entweder kopieren und einfügen oder in die Klasse User verschieben.

In Kotlin können Funktionen und Eigenschaften außerhalb von Klassen, Objekten oder Schnittstellen deklariert werden. Die Funktion mutableListOf(), mit der wir eine neue Instanz einer List erstellt haben, ist beispielsweise bereits in Collections.kt aus der Kotlin-Standardbibliothek definiert.

Wenn Sie in Java eine Dienstprogrammfunktion benötigen, erstellen Sie höchstwahrscheinlich eine Util-Klasse und deklarieren diese Funktion als statische Funktion. In Kotlin können Sie Funktionen der obersten Ebene deklarieren, ohne eine Klasse zu haben. In Kotlin können Sie jedoch auch Erweiterungsfunktionen erstellen. Dies sind Funktionen, die einen bestimmten Typ erweitern, aber außerhalb des Typs deklariert werden.

Mithilfe von Sichtbarkeitsmodifikatoren lässt sich die Sichtbarkeit von Erweiterungsfunktionen und -eigenschaften einschränken. Dadurch wird die Verwendung auf Klassen beschränkt, die die Erweiterungen benötigen, und der Namespace wird nicht verunreinigt.

Für die Klasse User können wir entweder eine Erweiterungsfunktion hinzufügen, die den formatierten Namen berechnet, oder den formatierten Namen in einer Erweiterungseigenschaft speichern. Sie kann außerhalb der Repository-Klasse in derselben Datei hinzugefügt werden:

// extension function
fun User.getFormattedName(): String {
    return if (lastName != null) {
        if (firstName != null) {
            "$firstName $lastName"
        } else {
            lastName ?: "Unknown"
        }
    } else {
        firstName ?: "Unknown"
    }
}

// extension property
val User.userFormattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

// usage:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedName

Wir können die Erweiterungsfunktionen und ‑eigenschaften dann so verwenden, als wären sie Teil der Klasse User.

Da der formatierte Name ein Attribut der Klasse User und keine Funktion der Klasse Repository ist, verwenden wir die Erweiterungseigenschaft. Unsere Repository-Datei sieht jetzt so aus:

val User.formattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
      get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user -> user.formattedName }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

Die Kotlin-Standardbibliothek verwendet Erweiterungsfunktionen, um die Funktionalität mehrerer Java-APIs zu erweitern. Viele der Funktionen von Iterable und Collection sind als Erweiterungsfunktionen implementiert. Die im vorherigen Schritt verwendete Funktion map ist beispielsweise eine Erweiterungsfunktion für Iterable.

11. Scope-Funktionen: let, apply, with, run, also

In unserem Repository-Klassencode fügen wir der Liste _users mehrere User-Objekte hinzu. Mithilfe von Kotlin-Bereichsfunktionen können diese Aufrufe idiomatischer gestaltet werden.

Um Code nur im Kontext eines bestimmten Objekts auszuführen, ohne auf das Objekt anhand seines Namens zugreifen zu müssen, bietet Kotlin fünf Bereichsfunktionen: let, apply, with, run und also. Diese Funktionen machen Ihren Code leichter lesbar und prägnanter. Alle Bereichsfunktionen haben einen Empfänger (this), können ein Argument (it) haben und einen Wert zurückgeben.

In dieser Übersicht finden Sie eine praktische Auflistung, die Ihnen hilft, sich daran zu erinnern, wann Sie welche Funktion verwenden sollten:

6b9283d411fb6e7b.png

Da wir unser _users-Objekt in unserem Repository konfigurieren, können wir den Code mithilfe der Funktion apply idiomatischer gestalten:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")
   
    _users.apply {
       // this == _users
       add(user1)
       add(user2)
       add(user3)
    }
 }

12. Zusammenfassung

In diesem Codelab haben wir die Grundlagen behandelt, die Sie für die Umwandlung Ihres Codes von Java in Kotlin benötigen. Diese Konvertierung ist unabhängig von Ihrer Entwicklungsplattform und trägt dazu bei, dass der von Ihnen geschriebene Code idiomatisch ist.

Idiomatische Kotlin-Codezeilen sind kurz und prägnant. Mit den vielen Funktionen von Kotlin gibt es viele Möglichkeiten, Ihren Code sicherer, prägnanter und leserlicher zu gestalten. So können wir beispielsweise unsere Repository-Klasse optimieren, indem wir die _users-Liste mit Nutzern direkt in der Deklaration instanziieren und den init-Block entfernen:

private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))

Wir haben eine große Bandbreite von Themen behandelt, vom Umgang mit Nullbarkeit, Singletons, Strings und Sammlungen bis hin zu Themen wie Erweiterungsfunktionen, Funktionen der obersten Ebene, Eigenschaften und Bereichsfunktionen. Wir haben zwei Java-Klassen durch zwei Kotlin-Klassen ersetzt, die jetzt so aussehen:

User.kt

data class User(var firstName: String?, var lastName: String?)

Repository.kt

val User.formattedName: String
    get() {
       return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() = _users.map { user -> user.formattedName }
}

Hier ist eine Zusammenfassung der Java-Funktionen und ihrer Zuordnung zu Kotlin:

Java

Kotlin

final Objekt

val Objekt

equals()

==

==

===

Klasse, die nur Daten enthält

data-Kurs

Initialisierung im Konstruktor

Initialisierung im Block init

static-Felder und ‑Funktionen

Felder und Funktionen, die in einem companion object deklariert sind

Singleton-Klasse

object

Weitere Informationen zu Kotlin und zur Verwendung auf Ihrer Plattform finden Sie in den folgenden Ressourcen: