(Veraltet) In Kotlin konvertieren

1. Willkommen!

In diesem Codelab erfahren Sie, wie Sie Ihren Code von Java zu Kotlin konvertieren. Außerdem erfahren Sie, welche Konventionen für die Kotlin-Sprache gelten und wie Sie sicherstellen können, dass der von Ihnen geschriebene Code diesen Konventionen entspricht.

Dieses Codelab eignet sich für alle Entwickler, die Java verwenden und ihr Projekt zu Kotlin migrieren möchten. 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 Fehler vermeiden.

Lerninhalte

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

  • Null-Zulässigkeit verarbeiten
  • Singletons implementieren
  • Datenklassen
  • Umgang mit Strings
  • Elvis-Operator
  • Destrukturierung
  • Properties und Backing-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 No Activity (Keine Aktivität). Wählen Sie Kotlin als Projektsprache aus. Die Mindest-SDK kann einen beliebigen Wert haben. Das Ergebnis wird dadurch nicht beeinflusst.

Der Code

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

Erstellen Sie eine neue Datei mit dem Namen User.java unter „app/java/<yourpackagename>“ 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;
    }

}

Ihre IDE meldet, 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. Null-Zulässigkeit, „val“, „var“ und Datenklassen deklarieren

Unsere IDE kann Java-Code recht gut automatisch in Kotlin-Code konvertieren, aber manchmal ist etwas Hilfe erforderlich. Lassen wir unsere IDE einen ersten Durchlauf der Konvertierung durchführen. 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 -> Convert Java File to Kotlin File (Java-Datei in Kotlin-Datei konvertieren).

Wenn Ihre IDE nach der Konvertierung eine Korrektur vorschlägt, drücken Sie Ja.

e6f96eace5dabe5f.png

Der folgende Kotlin-Code sollte angezeigt werden:

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

User.java wurde in User.kt umbenannt. Kotlin-Dateien haben die Endung „.kt“.

In unserer Java-Klasse User hatten wir zwei Attribute: firstName und lastName. Jede hatte eine Getter- und eine Setter-Methode, wodurch ihr Wert geändert werden konnte. Das Kotlin-Schlüsselwort für veränderliche Variablen ist var. Der Konverter verwendet daher var für jede dieser Eigenschaften. Wenn unsere Java-Attribute nur Getter hätten, wären sie schreibgeschützt und als val-Variablen deklariert worden. val ähnelt dem Keyword final in Java.

Einer der wichtigsten Unterschiede 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“ markiert haben, hat der automatische Konverter die Properties automatisch mit String? als „nullable“ markiert. Wenn Sie Ihre Java-Member mit „non-null“ annotieren (mit org.jetbrains.annotations.NotNull oder androidx.annotation.NonNull), erkennt der Konverter dies und macht die Felder auch in Kotlin zu „non-null“.

Die grundlegende Conversion ist bereits erfolgt. Wir können das aber auch idiomatisch ausdrücken. Sehen wir uns an, wie das geht.

Datenklasse

Unsere User-Klasse enthält nur Daten. In Kotlin gibt es 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 Schlüsselwort data hinzu:

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

In Kotlin kann es wie in Java einen primären Konstruktor und einen oder mehrere sekundäre Konstruktoren geben. Der 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")

Gleichheit

In Kotlin gibt es zwei Arten von Gleichheit:

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

Die im primären Konstruktor der Datenklasse definierten Properties werden für strukturelle Gleichheitsprü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. Der Standardwert wird verwendet, wenn das Argument weggelassen wird. In Kotlin sind Konstruktoren auch Funktionen. Wir können also Standardargumente verwenden, um anzugeben, dass der Standardwert von lastName null ist. Dazu weisen wir null einfach lastName 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 von Funktionen labeln:

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

Nehmen wir als anderes Beispiel an, dass firstName den Standardwert null hat und lastName nicht. Da der Standardparameter in diesem Fall einem Parameter ohne Standardwert vorangestellt wird, 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, daher benötigen wir keine Standardwerte.

5. Objektinitialisierung, Companion-Objekt und Singletons

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 Konvertierung 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 gemacht hat:

  • Die Liste von users ist auf null setzbar, 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 (die ursprünglich Teil von getFormattedUserNames( war) hat eine andere Syntax als die Java-Syntax.
  • Das Feld static ist jetzt Teil des Blocks companion object.
  • Ein init-Block wurde hinzugefügt

Bevor wir fortfahren, bereinigen wir den Code ein wenig. Wenn wir uns den Konstruktor ansehen, stellen wir fest, dass der Konverter unsere users-Liste in eine veränderliche Liste mit nullable-Objekten umgewandelt hat. Die Liste kann zwar tatsächlich null sein, aber wir gehen davon aus, dass sie keine Nullnutzer enthalten kann. Gehen wir also so vor:

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

Init-Block

In Kotlin darf der primäre Konstruktor keinen Code enthalten. Daher wird Initialisierungscode in init-Blöcke eingefügt. Die Funktionalität ist dieselbe.

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 zum Initialisieren von Attributen. Dies kann auch in der Deklaration der Property erfolgen. In der Kotlin-Version unserer Repository-Klasse sehen wir beispielsweise, dass die „users“-Eigenschaft in der Deklaration initialisiert wurde.

private var users: MutableList<User>? = null

static Attribute 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 hierfür ist der companion object-Block. Hier deklarieren Sie auch die statischen Felder und statischen Funktionen. Der Konverter hat den Companion-Objektblock erstellt und das Feld INSTANCE hierher verschoben.

Singletons verarbeiten

Da wir nur eine Instanz der Klasse Repository benötigen, haben wir in Java das Singleton-Muster 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 Companion-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)
    }
}

Wenn Sie die Klasse object verwenden, rufen Sie Funktionen und Eigenschaften direkt für das Objekt auf:

val formattedUserNames = Repository.formattedUserNames

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

6. Null-Zulässigkeit verarbeiten

Beim Konvertieren der Klasse Repository in Kotlin hat der automatische Konverter die Liste der Nutzer auf „nullable“ gesetzt, da sie bei der Deklaration nicht mit einem Objekt initialisiert wurde. Daher muss für alle Verwendungen des users-Objekts der Not-Null-Assertionsoperator !! verwendet werden. users!! und user!! werden im gesamten konvertierten Code angezeigt. Der Operator !! wandelt jede Variable in einen Typ ohne Nullwert um, sodass Sie auf Eigenschaften zugreifen oder Funktionen für sie aufrufen können. Wenn der Variablenwert jedoch tatsächlich null ist, wird eine Ausnahme ausgelöst. Wenn Sie !! verwenden, riskieren Sie, dass zur Laufzeit Ausnahmen ausgelöst werden.

Verwenden Sie stattdessen eine der folgenden Methoden, um Nullwerte zu verarbeiten:

  • NULL-Prüfung durchführen ( if (users != null) {...} )
  • Elvis-Operator ?: verwenden (wird später im Codelab behandelt)
  • Einige der Kotlin-Standardfunktionen verwenden (werden später im Codelab behandelt)

In unserem Fall wissen wir, dass die Liste der Nutzer nicht nullfähig sein muss, da sie direkt nach der Erstellung des Objekts (im init-Block) initialisiert wird. Daher können wir das users-Objekt direkt instanziieren, wenn wir es deklarieren.

Beim Erstellen von Instanzen von Sammlungstypen bietet Kotlin mehrere Hilfsfunktionen, um Ihren Code lesbarer und flexibler zu machen. 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 Listenelementtyp angeben. mutableListOf<User>() erstellt eine leere Liste, die User-Objekte enthalten kann. Da der Datentyp der Variablen jetzt vom Compiler abgeleitet werden kann, entfernen Sie die explizite Typdeklaration der users-Eigenschaft.

private val users = mutableListOf<User>()

Außerdem haben wir var in val geändert, da Nutzer einen schreibgeschützten Verweis auf die Liste der Nutzer enthalten. Die Referenz ist schreibgeschützt. Sie kann also nie auf eine neue Liste verweisen. Die Liste selbst ist jedoch weiterhin 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 init-Block 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 users-Eigenschaft jetzt nicht mehr null und wir können alle unnötigen Vorkommen des !!-Operators entfernen. Beachten Sie, dass in Android Studio weiterhin Kompilierungsfehler angezeigt werden. Fahren Sie jedoch mit den nächsten Schritten des Codelabs fort, um diese 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 als Strings angeben, können Sie den expliziten Typ in der Deklaration entfernen, da er abgeleitet wird.

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

Destrukturierung

In Kotlin kann ein Objekt mithilfe einer Syntax, die als destrukturierende Deklaration bezeichnet wird, in eine Reihe von Variablen zerlegt werden. Wir erstellen mehrere Variablen und können sie unabhängig voneinander verwenden.

data-Klassen unterstützen beispielsweise die Destrukturierung. Daher können wir das User-Objekt in der for-Schleife in (firstName, lastName) zerlegen. 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 „userNames“ haben noch nicht das gewünschte Format. Da sowohl lastName als auch firstName null sein können, müssen wir die Nullable-Eigenschaft berücksichtigen, wenn wir die Liste der formatierten Nutzernamen erstellen. Wir möchten "Unknown" anzeigen, wenn einer der beiden Namen fehlt. Da die Variable name nach dem einmaligen Festlegen 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 Namensvariable festgelegt wird. Es mag für Sie neu sein, dass eine Variable auf einen if- / else-Codeblock gesetzt 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 dazu, den Wert name zu initialisieren.

Wenn lastName null ist, wird name entweder auf firstName oder "Unknown" festgelegt.

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

Elvis-Operator

Dieser Code kann idiomatisch mit dem Elvis-Operator ?: 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 auf der rechten Seite zurück , "Unknown":

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

7. Stringvorlagen

Mit String-Vorlagen in Kotlin ist die Arbeit mit String ganz einfach. Mit Stringvorlagen können Sie in Stringdeklarationen auf Variablen verweisen, indem Sie vor die Variable das $-Symbol setzen. Sie können auch einen Ausdruck in eine String-Deklaration einfügen, indem Sie den Ausdruck in geschweifte Klammern ({ }) setzen und das $-Symbol davor verwenden. Beispiel: ${user.firstName}.

In Ihrem Code wird derzeit die Stringverkettung verwendet, um firstName und lastName zum Nutzernamen zu kombinieren.

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

Ersetzen Sie die Stringverkettung stattdessen durch:

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

Durch die Verwendung von Stringvorlagen kann Ihr Code vereinfacht werden.

Ihre IDE zeigt Warnungen an, wenn es eine idiomatischere Möglichkeit gibt, Ihren Code zu schreiben. Im Code wird eine geschlängelte Unterstreichung angezeigt. Wenn Sie den Mauszeiger darauf bewegen, sehen Sie einen Vorschlag, wie Sie den Code umgestalten können.

Derzeit sollte eine Warnung angezeigt werden, dass die name-Deklaration mit der Zuweisung zusammengefasst werden kann. Wenden wir das an. Da der Typ der Variablen name abgeleitet werden kann, kann die explizite Typdeklaration String entfernt werden. Unsere formattedUserNames sieht jetzt 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 zusätzliche Anpassung vornehmen. In unserer UI-Logik wird "Unknown" angezeigt, wenn Vor- und Nachname fehlen. Null-Objekte werden also nicht unterstützt. Ersetzen Sie daher für den Datentyp von 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 idiomatisch gestalten können. Der Code führt derzeit Folgendes aus:

  • Erstellt eine neue Liste mit Strings.
  • Iteriert die Liste der Nutzer
  • Erstellt den formatierten Namen für jeden Nutzer basierend auf dem Vor- und Nachnamen des Nutzers.
  • Gibt die neu erstellte Liste zurück
    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 die 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 Nutzerliste manuell zu durchlaufen, können wir die Funktion map verwenden und die Logik, die wir in der for-Schleife hatten, in den map-Body verschieben. Standardmäßig ist der Name des aktuellen Listenelements, das in map verwendet wird, it. Zur besseren Lesbarkeit können Sie it jedoch 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 Ganze noch weiter zu vereinfachen, können wir die Variable name komplett 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 Backing-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 Klassenattribute über Getter- und Setter-Funktionen verfügbar machen. Mit Kotlin können wir besser zwischen Eigenschaften einer Klasse, die mit Feldern ausgedrückt werden, und Funktionen, Aktionen, die eine Klasse ausführen kann, unterscheiden. In unserem Fall ist die Klasse Repository sehr einfach und führt keine Aktionen aus. Sie enthält also 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.

Es gibt zwar kein Feld, das explizit der formattedUserNames-Eigenschaft entspricht, aber Kotlin bietet uns ein automatisches Backing-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 Backing Field nicht bietet.

Sehen wir uns ein Beispiel an.

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

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

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

Benennen wir zuerst users in _users um. Markieren Sie den Variablennamen, klicken Sie mit der rechten Maustaste und wählen Sie Refactor > Rename (Umgestalten > Umbenennen) aus. 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 das private Attribut _users zum Backing-Attribut für das öffentliche Attribut users. Außerhalb der Klasse Repository ist die Liste _users nicht änderbar, 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, in der die Liste nicht geändert werden kann. Wenn users über Java aufgerufen wird, wird die java.util.List-Implementierung verwendet, in 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 Attribute der obersten Ebene und Erweiterungsfunktionen

Derzeit weiß die Klasse Repository, wie der formatierte Nutzername für ein User-Objekt berechnet wird. Wenn wir dieselbe Formatierungslogik 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(), die wir zum Erstellen einer neuen Instanz von List verwendet haben, ist beispielsweise bereits in Collections.kt aus der Kotlin-Standardbibliothek definiert.

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

Die Sichtbarkeit von Erweiterungsfunktionen und ‑attributen kann mithilfe von Sichtbarkeitsmodifizierern eingeschränkt werden. Dadurch wird die Verwendung auf Klassen beschränkt, die die Erweiterungen benötigen, und der Namespace wird nicht überladen.

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 ‑attribute dann so verwenden, als wären sie Teil der Klasse User.

Da der formatierte Name eine Property der Klasse User und keine Funktion der Klasse Repository ist, verwenden wir die Erweiterungsproperty. 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 verschiedener Java-APIs zu erweitern. Viele Funktionen von Iterable und Collection werden als Erweiterungsfunktionen implementiert. Die Funktion map, die wir in einem vorherigen Schritt verwendet haben, ist beispielsweise eine Erweiterungsfunktion für Iterable.

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

Im Code der Klasse Repository fügen wir der Liste _users mehrere User-Objekte hinzu. Diese Aufrufe können mithilfe von Kotlin-Bereichsfunktionen idiomatisch gestaltet werden.

Wenn Sie Code nur im Kontext eines bestimmten Objekts ausführen möchten, 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 lesbarer und prägnanter. Alle Bereichsfunktionen haben einen Empfänger (this), können ein Argument (it) haben und können einen Wert zurückgeben.

Hier finden Sie eine praktische Übersicht, die Ihnen hilft, sich zu merken, wann Sie die einzelnen Funktionen verwenden sollten:

6b9283d411fb6e7b.png

Da wir unser _users-Objekt in unserem Repository konfigurieren, können wir den Code mit der apply-Funktion idiomatisch 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 benötigen, um Ihren Code von Java zu Kotlin zu konvertieren. Diese Konvertierung ist unabhängig von Ihrer Entwicklungsplattform und trägt dazu bei, dass der von Ihnen geschriebene Code idiomatisch in Kotlin ist.

Idiomatisches Kotlin ermöglicht es, Code kurz und prägnant zu schreiben. Mit all den Funktionen, die Kotlin bietet, gibt es viele Möglichkeiten, Ihren Code sicherer, prägnanter und lesbarer zu machen. Wir können beispielsweise unsere Repository-Klasse optimieren, indem wir die _users-Liste mit Nutzern direkt in der Deklaration instanziieren und so den init-Block entfernen:

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

Wir haben eine Vielzahl von Themen behandelt, von der Verarbeitung von Nullable-Typen, 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 eine Kurzfassung der Java-Funktionen und ihrer Zuordnung zu Kotlin:

Java

Kotlin

final Objekt

val Objekt

equals()

==

==

===

Klasse, die nur Daten enthält

Klasse data

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: