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
- undrun
-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.
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 undequals()
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 Modifikatorfun
deklariert. - Die Methode
getFormattedUserNames()
ist jetzt ein Attribut namensformattedUserNames
. - 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 einescompanion 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
?
inUser?
in derusers
-Typdeklaration. - Entfernen Sie das
?
inUser?
für den RückgabetypgetUsers()
, damitList<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 String
s 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:
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 |
|
|
|
|
|
|
Klasse, die nur Daten enthält |
|
Initialisierung im Konstruktor | Initialisierung im Block |
| Felder und Funktionen, die in einem |
Singleton-Klasse |
|
Weitere Informationen zu Kotlin und zur Verwendung auf Ihrer Plattform finden Sie in den folgenden Ressourcen:
- Kotlin-Koans
- Kotlin-Anleitungen
- Grundlagen von Android und Kotlin
- Kotlin Bootcamp für Programmierer
- Kotlin für Java-Entwickler – kostenloser Kurs im Audit-Modus