Informationen zu diesem Codelab
1. Herzlich willkommen!
In diesem Codelab lernen Sie, wie Sie Code in Java in Kotlin konvertieren. Außerdem erfährst du, was die Kotlin-Sprachkonventionen sind und wie du dafür sorgst, dass der Code, den du schreibst, diesen folgt.
Dieses Codelab eignet sich für alle Entwickler, die Java verwenden und überlegen, ihr Projekt zu Kotlin zu migrieren. Wir beginnen mit einigen Java-Klassen, die Sie mithilfe der IDE in Kotlin konvertieren. Dann sehen wir uns den konvertierten Code an und sehen, wie wir ihn verbessern können, indem wir ihn idiomatischer machen und häufige Fehler vermeiden.
Lerninhalte
Sie erfahren, wie Sie Java in Kotlin konvertieren. Dabei lernen Sie die folgenden Kotlin-Sprachfunktionen und -konzepte kennen:
- Umgang mit Nullfähigkeit
- Singleton-Implementierungen
- Datenklassen
- Strings verarbeiten
- Elvis-Operator
- Zerstörende
- Eigenschaften und Sicherungseigenschaften
- 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 du Android Studio verwendest, erstelle ein neues Projekt ohne Aktivitäten.
Der Code
Wir erstellen ein User
-Modellobjekt und eine Repository
-Einzeltonklasse, die mit User
-Objekten funktioniert und Listen mit Nutzern und formatierte Nutzernamen enthält.
Erstelle eine neue Datei mit dem Namen User.java
unter app/java/<deinePaketname> und füge 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;
}
}
Importieren Sie je nach Projekttyp androidx.annotation.Nullable
, wenn Sie ein Android-Projekt verwenden, oder org.jetbrains.annotations.Nullable
.
Erstelle eine neue Datei mit dem Namen Repository.java
und füge 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. Deklarieren von Null-, Val- und Var- und Datenklassen
Unsere IDE kann den Java-Code sehr gut in Kotlin-Code refaktorieren, benötigt aber manchmal etwas Hilfe. Wir tun dies zuerst und sehen uns dann den refaktorierten Code an, um zu verstehen, wie und warum der Ansatz auf diese Weise refaktoriert wurde.
Gehen Sie zu der Datei User.java
und konvertieren Sie sie in Kotlin: Menüleiste -> Code -> Java-Datei in Kotlin-Datei konvertieren.
Wenn in Ihrer IDE nach der Umwandlung eine Korrektur angefordert wird, drücken Sie auf Ja.
Sie sollten den folgenden Kotlin-Code sehen:
class User(var firstName: String?, var lastName: String?)
Hinweis: User.java
wurde in User.kt
umbenannt. Kotlin-Dateien haben die Erweiterung „.kt“.
In unserer Java-User
-Klasse hatten wir zwei Properties: firstName
und lastName
. Jeder hatte einen Getter und eine Setter-Methode, wodurch sein Wert änderbar war. Das Kotlin-Keyword für veränderbare Variablen ist var
. Der Konverter verwendet daher für jede dieser Properties var
. Wenn unsere Java-Properties nur Getter hätten, wären sie unveränderlich und hätten als val
-Variablen deklariert. val
ähnelt dem Keyword final
in Java.
Einer der Hauptunterschiede zwischen Kotlin und Java besteht darin, dass Kotlin explizit angibt, ob eine Variable einen Nullwert annehmen kann. Dazu wird „?
“ an die Typdeklaration angehängt.
Weil wir firstName
und lastName
als NULL-fähig gekennzeichnet haben, hat die automatische Konvertierung die Properties mit String?
automatisch als NULL-fähig gekennzeichnet. Wenn Sie Ihre Java-Mitglieder mit org.jetbrains.annotations.NotNull
oder androidx.annotation.NonNull
als null kennzeichnen, wird dies vom Konverter erkannt und die Felder werden in Kotlin ebenfalls als null markiert.
Die grundlegende Refaktorierung wurde bereits durchgeführt. Aber wir können das auf idiomatischere Weise formulieren. Sehen wir uns an, wie das geht.
Datenklasse
Unsere User
-Klasse enthält nur Daten. Kotlin enthält ein Keyword für Klassen mit dieser Rolle: data
. Wenn Sie diese Klasse als data
-Klasse markieren, erstellt der Compiler automatisch Getter und Setter für uns. Darüber werden auch die Funktionen equals()
, hashCode()
und toString()
abgeleitet.
Fügen Sie unserer User
-Klasse das Keyword data
hinzu:
data class User(var firstName: String, var lastName: String)
Kotlin kann wie ein Java einen primären und einen oder mehrere sekundäre Konstruktoren haben. Die im Beispiel oben ist der primäre Konstruktor der Nutzerklasse. Wenn Sie eine Java-Klasse mit mehreren Konstruktoren konvertieren, werden auch in Kotlin automatisch mehrere Konstruktoren erstellt. Sie werden mit dem Keyword constructor
definiert.
Wenn wir eine Instanz dieser Klasse erstellen möchten, können wir so vorgehen:
val user1 = User("Jane", "Doe")
Gleichheit
Kotlin enthält zwei Arten von Gleichheit:
- Bei der strukturellen Gleichheit wird der Operator
==
verwendet undequals()
aufgerufen, um festzustellen, ob zwei Instanzen identisch sind. - Bei der Referenzgleichheit wird der Operator
===
verwendet und geprüft, ob zwei Verweise 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. Wenn das Argument weggelassen wird, wird der Standardwert verwendet. In Kotlin sind Konstruktoren auch Funktionen. Daher können wir mit Standardargumenten angeben, dass der Standardwert von lastName
null
ist. Dazu ordnen 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("John", "Doe")
Funktionsparameter können beim Aufrufen von Funktionen benannt werden:
val john = User(firstName = "John", lastName = "Doe")
Als weiteren Anwendungsfall nehmen wir an, dass bei firstName
„null
“ als Standardwert und „lastName
“ nicht enthalten ist. Da der Parameter vor einem Parameter ohne Standardwert steht, 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")
5. Objektinitialisierung, Companion-Objekt und Singleton
Bevor Sie fortfahren, muss Ihre User
-Klasse eine data
-Klasse sein. Lass die Klasse Repository
zur Kotlin-Klasse konvertieren. Das automatische Conversion-Ergebnis 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)
}
}
So sieht der automatische Konverter aus:
- Ein
init
-Block wurde hinzugefügt (Repository.kt#L50) - Das Feld
static
ist jetzt Teil einescompanion object
-Blocks (Repository.kt#L33) - Die Liste von
users
ist null, da das Objekt zum Zeitpunkt der Deklaration (Repository.kt#L7) nicht instanziiert wurde. - Die Methode
getFormattedUserNames()
ist jetzt eine Property namensformattedUserNames
(Repository.kt#L11) - Die Iteration über die Liste der Nutzer, die ursprünglich Teil von
getFormattedUserNames(
war, hat eine andere Syntax als die Java-Instanz (Repository.kt#L15).
Bevor wir fortfahren, müssen wir den Code etwas bereinigen. Wir haben festgestellt, dass der Konvertierer unsere users
-Liste zu einer änderbaren Liste mit NULL-fähigen Objekten gemacht hat. Obwohl die Liste in der Tat null sein kann, sagen wir, sie darf keine Nutzer enthalten. Führen wir nun die folgenden Schritte aus:
- Entfernen Sie
?
inUser?
innerhalb derusers
-Deklarationen. getUsers
sollteList<User>?
zurückgeben
Die automatische Konvertierung wird außerdem unnötig in zwei Zeilen für die Variablendeklarationen der Variablen des Nutzers und der Variablen ausgeführt, die im Initialisierungsblock definiert sind. Fügen Sie jeder Variablendeklaration eine Zeile hinzu. So sollte der Code aussehen:
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)
}
}
Grundblock
In Kotlin kann der primäre Konstruktor keinen Code enthalten. Der Initialisierungscode wird in init
-Blöcken platziert. Die Funktionsweise 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)
}
}
Der Großteil des init
-Codes wird für die Initialisierung von Properties verwendet. Dieser Vorgang kann auch in der Deklaration der Property durchgeführt werden. In der Kotlin-Version unserer Repository
-Klasse sehen wir beispielsweise, dass die „user“-Property in der Deklaration initialisiert wurde.
private var users: MutableList<User>? = null
static
Properties und Methoden von Kotlin
In Java verwenden wir das static
-Keyword für Felder oder Funktionen, um anzugeben, dass sie zu einer Klasse gehören, aber nicht zu einer Instanz der Klasse. Aus diesem Grund haben wir das statische Feld INSTANCE
in der Repository
-Klasse erstellt. Die Kotlin-Entsprechung ist der companion object
-Block. Hier würden Sie auch die statischen Felder und statischen Funktionen deklarieren. Der Nutzer mit Conversion hat das Feld INSTANCE
hier erstellt und verschoben.
Umgang mit Singleton-Steinen
Da wir nur eine Instanz der Klasse Repository
benötigen, haben wir das Singleton-Muster in Java verwendet. Mit Kotlin können Sie dieses Muster auf Compiler-Ebene erzwingen. Dazu ersetzen Sie das Keyword class
durch object
.
Entfernen Sie den privaten Konstruktor und das Companion-Objekt und ersetzen Sie die Klassendefinition durch object Repository
.
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 Verwendung der Klasse object
rufen wir Funktionen und Eigenschaften direkt im Objekt auf:
val users = Repository.users
Zerstörende
Mit Kotlin können Sie ein Objekt in eine Reihe von Variablen zerlegen. Dazu wird die Syntax Destrukturierungsdeklaration verwendet. Wir erstellen mehrere Variablen und können diese unabhängig verwenden.
Beispielsweise unterstützen Datenklassen die Zerstörung, sodass wir das User
-Objekt in der for
-Schleife in (firstName, lastName)
zerstören können. So können wir direkt mit den Werten firstName
und lastName
arbeiten. Die for
-Schleife lässt sich so aktualisieren:
for ((firstName, lastName) in users!!) {
val name: String?
if (lastName != null) {
if (firstName != null) {
name = "$firstName $lastName"
} else {
name = lastName
}
} else if (firstName != null) {
name = firstName
} else {
name = "Unknown"
}
userNames.add(name)
}
6. Umgang mit Nullfähigkeit
Beim Konvertieren der Repository
-Klasse in Kotlin wurde die Liste der Nutzer auf null gesetzt, da sie bei der Erklärung des Objekts nicht initialisiert wurde. Für alle Verwendungszwecke des Objekts users
wird der Assertion-Operator !!
verwendet. Jede Variable wird in einen Typ konvertiert, der nicht null ist. Eine Ausnahme wird ausgelöst, wenn der Wert NULL ist. Wenn Sie !!
verwenden, besteht die Gefahr, dass während der Laufzeit Ausnahmen ausgelöst werden.
Stattdessen sollten Sie die Null-Zulässigkeit auf eine der folgenden Arten verarbeiten:
- Nullprüfung (
if (users != null) {...}
) - Mit dem elvis-Operator
?:
(später im Codelab behandelt) - Einige der Kotlin-Standardfunktionen verwenden (werden später im Codelab behandelt)
Wir wissen, dass die Nutzerliste nicht null sein kann, weil sie direkt nach Erstellung des Objekts initialisiert wird. Es kann also direkt direkt beim Deklarieren des Objekts instanziiert werden.
Zum Erstellen von Instanzen von Sammlungstypen bietet Kotlin mehrere Hilfsfunktionen, mit denen der Code lesbarer und flexibler wird. Hier verwenden wir MutableList
für users
:
private var users: MutableList<User>? = null
Der Einfachheit halber können wir die mutableListOf()
-Funktion verwenden, den Listenelementtyp angeben, den ArrayList
-Konstruktor-Aufruf aus dem init
-Block entfernen und die explizite Typdeklaration der users
-Property entfernen.
private val users = mutableListOf<User>()
Außerdem haben wir die Variablen in „Val“ geändert, da Nutzer einen unveränderlichen Verweis auf die Liste der Nutzer enthalten. Hinweis: Die Referenz ist unveränderlich, die Liste selbst kann jedoch geändert werden. Sie können Elemente hinzufügen oder entfernen.
Durch diese Änderungen ist unsere users
-Property jetzt nicht null. Wir können alle unnötigen !!
-Operatoren entfernen.
val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
...
}
Da die Variable „user“ bereits initialisiert ist, müssen wir die Initialisierung aus dem Block init
entfernen:
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users.add(user1)
users.add(user2)
users.add(user3)
}
Da sowohl lastName
als auch firstName
null
sein können, müssen wir beim Erstellen der Liste der formatierten Nutzernamen die Null-Werte verarbeiten. Da "Unknown"
angezeigt werden soll, wenn einer der Namen fehlt, können wir den Namen als null festlegen, indem wir ?
aus der Typdeklaration entfernen.
val name: String
Wenn lastName
null ist, ist name
entweder die firstName
oder die "Unknown"
:
if (lastName != null) {
if (firstName != null) {
name = "$firstName $lastName"
} else {
name = lastName
}
} else if (firstName != null) {
name = firstName
} else {
name = "Unknown"
}
Diese 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 der linke Teil null ist.
Daher wird im folgenden Code user.firstName
zurückgegeben, wenn er nicht null ist. Wenn user.firstName
null ist, gibt der Ausdruck den Wert rechts zurück, "Unknown"
:
if (lastName != null) {
...
} else {
name = firstName ?: "Unknown"
}
7. Stringvorlagen und if-Ausdruck
Mit Kotlin arbeitest du mit String
s Stringseinfachheraus. Mit Stringvorlagen können Sie in Stringdeklarationen auf Variablen verweisen.
Der automatische Konverter hat die Verkettung des Vor- und Nachnamens aktualisiert, sodass er mithilfe des Symbols $
direkt auf den Variablennamen verweist. Dabei wird der Ausdruck zwischen { }
gesetzt.
// Java
name = user.getFirstName() + " " + user.getLastName();
// Kotlin
name = "${user.firstName} ${user.lastName}"
Ersetzen Sie im Code die Stringverkettung durch:
name = "$firstName $lastName"
In Kotlin sind if
, when
, for
und while
Ausdrücke. Sie geben einen Wert zurück. In deiner IDE wird sogar eine Warnung angezeigt, dass die Zuweisung aus der if
entfernt werden sollte:
Folgen Sie dem Vorschlag der IDE und heben Sie die Zuweisung für beide if
-Anweisungen auf. Die letzte Zeile der if-Anweisung wird zugewiesen. Im Gegensatz dazu ist aus diesem Block nur der Initialisierung des Namenswerts erkennbar:
name = if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName
}
} else {
firstName ?: "Unknown"
}
Als Nächstes erhalten wir eine Warnung, dass die Deklaration name
der Aufgabe hinzugefügt werden kann. Dies gilt auch für sie. Weil der Typ der Namensvariablen abgerufen werden kann, kann die explizite Typdeklaration entfernt werden. Jetzt sieht unser formattedUserNames
so aus:
val formattedUserNames: List<String?>
get() {
val userNames: MutableList<String?> = ArrayList(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
}
8. Vorgänge für Sammlungen
Sehen wir uns den formattedUserNames
-Getter einmal genauer an, um zu erfahren, wie wir ihn idiomatischer gestalten können. Derzeit führt der Code folgende Schritte aus:
- Erstellt eine neue Liste von Strings
- Durch die Liste der Nutzer
- Erstellt basierend auf dem Vor- und Nachnamen des Nutzers den formatierten Namen für jeden Nutzer.
- 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 umfassende Liste von Sammlungstransformationen, die durch die Erweiterung der Funktionen der Java Collections API die Entwicklung schneller und sicherer macht. Eine davon ist die Funktion map
. Diese Funktion gibt eine neue Liste mit den Ergebnissen der Anwendung der angegebenen Transformationsfunktion auf jedes Element in der ursprünglichen Liste zurück. Anstatt eine neue Liste zu erstellen und die Liste der Nutzer manuell durchzugehen, können wir die Funktion map
verwenden und die Logik in der for
-Schleife im map
-Text verschieben. Standardmäßig lautet der Name des aktuellen in map
verwendeten Listenelements it
. Sie können jedoch it
durch Ihren eigenen Variablennamen ersetzen, um die Lesbarkeit zu verbessern. In unserem Fall nennen wir sie 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
}
}
Außerdem lässt sich 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. Eigenschaften und Sicherungseigenschaften
Der automatische Konverter hat die Funktion getFormattedUserNames()
durch die Property formattedUserNames
mit einem benutzerdefinierten Getter ersetzt. Im Hintergrund generiert Kotlin eine getFormattedUserNames()
-Methode, die ein List
zurückgibt.
In Java stellen wir unsere Klasseneigenschaften über Getter- und Setter-Funktionen bereit. Mit Kotlin können wir die Eigenschaften einer Klasse (mit Feldern und Funktionen) unterscheiden. In unserem Fall ist die Klasse Repository
sehr einfach und es werden keine Aktionen ausgeführt. Daher enthält sie nur Felder.
Die Logik, die in der Java-Funktion getFormattedUserNames()
ausgelöst wurde, wird jetzt ausgelöst, wenn der Getter der Kotlin-Property formattedUserNames
aufgerufen wird.
Es gibt zwar nicht ausdrücklich ein Feld, das der formattedUserNames
-Property entspricht, aber Kotlin bietet uns ein automatisches Sicherungsfeld mit der Bezeichnung field
, auf das wir bei Bedarf von benutzerdefinierten Getter- und -Settern zugreifen können.
Manchmal wünschen wir uns aber einige zusätzliche Funktionen, die das automatische Back-up-Feld nicht bietet. Hier ein Beispiel:
Innerhalb unserer Repository
-Klasse gibt es eine änderbare Liste von Nutzern, die in der Funktion getUsers()
ermittelt werden, die durch den Java-Code generiert wurde:
fun getUsers(): List<User>? {
return users
}
Das Problem besteht darin, dass durch die Rückgabe von users
jeder Nutzer der Repository-Klasse die Liste der Nutzer ändern kann. Dies ist keine gute Idee. Lass uns das Problem mit einer unterstützenden Property beheben.
Zuerst benennen wir users
in _users
um. Fügen Sie nun eine öffentliche unveränderliche Property hinzu, die eine Liste von Nutzern zurückgibt. Nennen wir dies users
:
private val _users = mutableListOf<User>()
val users: List<User>
get() = _users
Durch diese Änderung wird die private Property _users
zur Sicherungseigenschaft für die öffentliche users
-Property. 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.
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 Eigenschaften der obersten Ebene und Erweiterung
Zurzeit weiß die Repository
-Klasse, wie der formatierte Nutzername für ein User
-Objekt berechnet wird. Wenn wir jedoch dieselbe Formatierungslogik für andere Klassen wiederverwenden möchten, müssen wir diese entweder kopieren und in die User
-Klasse verschieben.
Mit Kotlin können Sie Funktionen und Eigenschaften auch außerhalb von Klassen, Objekten oder Schnittstellen deklarieren. Beispielsweise ist die Funktion mutableListOf()
, die wir zum Erstellen einer neuen List
-Instanz verwendet haben, direkt in Collections.kt
aus der Standardbibliothek definiert.
In Java würden Sie wahrscheinlich immer dann die Util
-Klasse erstellen, wenn Sie bestimmte Dienstprogrammfunktionen benötigen, und diese als statische Funktion deklarieren. In Kotlin können Sie Top-Level-Funktionen deklarieren, ohne eine Klasse zu haben. Kotlin bietet jedoch auch die Möglichkeit, Erweiterungsfunktionen zu erstellen. Das sind Funktionen, die einen bestimmten Typ erweitern, aber außerhalb des Typs deklariert sind. Daher haben sie einen bestimmten Typ.
Die Sichtbarkeit von Erweiterungsfunktionen und -eigenschaften kann mithilfe von Sichtbarkeitsmodifikatoren eingeschränkt werden. Dadurch wird die Nutzung auf Klassen beschränkt, die die Erweiterungen benötigen. Außerdem wird der Namespace nicht belastet.
Für die User
-Klasse können wir entweder eine Erweiterungsfunktion hinzufügen, die den formatierten Namen berechnet, oder den formatierten Namen in einer Erweiterungseigenschaft beibehalten. 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
Anschließend können wir die Funktionen und Eigenschaften der Erweiterung so verwenden, als wären sie Teil der User
-Klasse.
Da der formatierte Name eine Eigenschaft des Nutzers und keine Funktionalität der Klasse Repository
ist, muss die Erweiterungseigenschaft verwendet werden. Die 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 Funktionen auf Iterable
und Collection
sind als Erweiterungsfunktionen implementiert. Beispielsweise ist die map
-Funktion, die wir in einem vorherigen Schritt verwendet haben, eine Erweiterungsfunktion bei Iterable
.
11. Bereichsfunktionen: „Let“, „apply“, „with“, „run“
In unserem Repository
-Kurscode werden mehrere Nutzerobjekte zur _users
-Liste hinzugefügt. Diese Aufrufe können mithilfe von Bereichsfunktionen idiomatischer gemacht werden.
Von Kotlin wurden fünf Funktionen erstellt: let
, apply
, with
, run
und also
. Kurz und leistungsstark: Alle Funktionen haben einen Empfänger (this
), können ein Argument (it
) haben und einen Wert zurückgeben. Sie entscheiden, welche davon Sie verwenden möchten.
Hier ist ein praktisches Spickzettel für Sie:
Da wir das _users
-Objekt in unserem Repository
konfigurieren, können wir den Code mit der apply
-Funktion 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 wurden die Grundlagen für die Refaktorierung Ihres Codes von Java in Kotlin behandelt. Diese Refaktorierung ist unabhängig von Ihrer Entwicklungsplattform und trägt dazu bei, dass der von Ihnen geschriebene Code idiomatisch ist.
Mit idiomatischem Kotlin ist das Schreiben von Code kurz und prägnant. Hilfreich ist auch, wie Sie Ihren Code mit Kotlin noch sicherer und präziser machen können. Wir können beispielsweise die Klasse Repository
optimieren, indem wir die Liste _users
direkt mit Nutzern in der Deklaration instanziieren und so den Block init
entfernen:
private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
Dabei wurde ein breites Spektrum an Themen abgedeckt – von der Verarbeitung der Nullwerte, Singleton-Strings, Strings und Sammlungen bis hin zu Themen wie Erweiterungsfunktionen, Top-Level-Funktionen, Properties und Bereichsfunktionen. Es sind zwei Java-Klassen vorhanden und zwei Kotlin-Klassen befinden:
Nutzer.kt
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 sehen Sie einen TL-DR-Code für die Java-Funktionen und ihre Zuordnung zu Kotlin:
Java | Kotlin |
|
|
|
|
|
|
Klasse, die nur Daten enthält | Klasse |
Initialisierung im Konstruktor | Initialisierung im Block |
| Felder und Funktionen, die in einem |
Singleton-Klasse |
|
Mehr über Kotlin und die Verwendung auf Ihrer Plattform erfahren Sie in den folgenden Ressourcen:
- Kotlin-Koans
- Kotlin-Anleitungen
- Android-Apps mit Kotlin entwickeln – kostenloser Kurs
- Kotlin-Bootcamp für Programmierer
- Kotlin for Java Developers (kostenloser Kurs im Audit-Modus)