Refaktorierung auf Kotlin

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 und run Keywords

Annahmen

Sie sollten bereits mit Java vertraut sein.

Voraussetzungen

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;
    }
}

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 und equals() 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

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 firstNamenull“ 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")

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 eines companion 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 namens formattedUserNames (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 ? in User? innerhalb der users-Deklarationen.
  • getUsers sollte List<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

staticProperties 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)
}

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"
}

Mit Kotlin arbeitest du mit Strings 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
   }

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"
                }
            }
        }

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)
    }
}

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.

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)
    }
 }

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

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

Mehr über Kotlin und die Verwendung auf Ihrer Plattform erfahren Sie in den folgenden Ressourcen: