(Wycofane) Konwertowanie na Kotlin

1. Witamy!

Z tego laboratorium dowiesz się, jak przekonwertować kod z Javy na Kotlin. Dowiesz się też, jakie są konwencje języka Kotlin i jak zadbać o to, aby pisany przez Ciebie kod był z nimi zgodny.

Te warsztaty są przeznaczone dla każdego dewelopera, który korzysta z Javy i rozważa przeniesienie projektu na język Kotlin. Zaczniemy od kilku klas w języku Java, które przekonwertujesz na Kotlin za pomocą środowiska IDE. Następnie przyjrzymy się przekonwertowanemu kodowi i zobaczymy, jak możemy go ulepszyć, aby był bardziej idiomatyczny i pozwalał uniknąć typowych błędów.

Czego się nauczysz

Dowiesz się, jak przekonwertować kod Java na Kotlin. W ten sposób poznasz te funkcje i koncepcje języka Kotlin:

  • Obsługa wartości null
  • Implementowanie singletonów
  • Klasy danych
  • Obsługa ciągów znaków
  • Operator Elvis
  • Destrukturyzacja
  • Właściwości i właściwości pomocnicze
  • Argumenty domyślne i nazwane parametry
  • Praca z kolekcjami
  • Funkcje rozszerzeń
  • Funkcje i parametry najwyższego poziomu
  • let, apply, withrun

Założenia

Musisz znać język Java.

Czego potrzebujesz

2. Przygotowania

Tworzenie nowego projektu

Jeśli używasz IntelliJ IDEA, utwórz nowy projekt w języku Java z Kotlin/JVM.

Jeśli używasz Androida Studio, utwórz nowy projekt, korzystając z szablonu No Activity (Brak aktywności). Jako język projektu wybierz Kotlin. Minimalna wersja pakietu SDK może mieć dowolną wartość, nie wpłynie to na wynik.

Kod

Utworzymy obiekt modelu User i klasę singleton Repository, która współpracuje z obiektami User i udostępnia listy użytkowników oraz sformatowane nazwy użytkowników.

Utwórz nowy plik o nazwie User.java w katalogu app/java/<yourpackagename> i wklej do niego ten kod:

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

}

Zauważysz, że środowisko IDE informuje, że @Nullable nie jest zdefiniowane. Jeśli używasz Androida Studio, zaimportuj androidx.annotation.Nullable, a jeśli IntelliJ – org.jetbrains.annotations.Nullable.

Utwórz nowy plik o nazwie Repository.java i wklej do niego ten kod:

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. Deklarowanie dopuszczalności wartości null, val, var i klas danych

Nasze środowisko IDE całkiem dobrze radzi sobie z automatycznym przekształcaniem kodu Java w kod Kotlin, ale czasami potrzebuje pomocy. Pozwólmy IDE na wstępne przekształcenie. Następnie przeanalizujemy powstały kod, aby zrozumieć, jak i dlaczego został przekonwertowany w ten sposób.

Otwórz plik User.java i przekonwertuj go na plik Kotlin: pasek menu –> Kod –> Przekonwertuj plik Java na plik Kotlin.

Jeśli środowisko IDE po konwersji wyświetli prośbę o korektę, kliknij Yes (Tak).

e6f96eace5dabe5f.png

Powinien wyświetlić się ten kod w języku Kotlin:

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

Pamiętaj, że User.java zostało zmienione na User.kt. Pliki Kotlin mają rozszerzenie .kt.

W klasie Java User mieliśmy 2 właściwości: firstNamelastName. Każda z nich miała metodę pobierającą i ustawiającą, dzięki czemu jej wartość można było zmieniać. Słowem kluczowym w języku Kotlin oznaczającym zmienne modyfikowalne jest var, więc konwerter używa var w przypadku każdej z tych właściwości. Gdyby nasze właściwości Javy miały tylko metody pobierające, byłyby tylko do odczytu i zostałyby zadeklarowane jako zmienne val. val jest podobne do słowa kluczowego final w języku Java.

Jedną z głównych różnic między Kotlinem a Javą jest to, że Kotlin wyraźnie określa, czy zmienna może przyjmować wartość null. W tym celu dodaje do deklaracji typu znak ?.

Ponieważ oznaczyliśmy typy firstNamelastName jako dopuszczające wartość null, automatyczny konwerter automatycznie oznaczył właściwości jako dopuszczające wartość null za pomocą typu String?. Jeśli oznaczysz elementy Java jako niepuste (za pomocą org.jetbrains.annotations.NotNull lub androidx.annotation.NonNull), konwerter rozpozna to i ustawi pola jako niepuste również w Kotlinie.

Podstawowa konwersja została już przeprowadzona. Możemy jednak zapisać to w bardziej idiomatyczny sposób. Zobaczmy, jak to zrobić.

Klasa danych

Nasza klasa User zawiera tylko dane. Kotlin ma słowo kluczowe dla klas z tą rolą: data. Oznaczając tę klasę jako klasę data, kompilator automatycznie utworzy dla nas metody pobierające i ustawiające. Wygeneruje też funkcje equals(), hashCode() i toString().

Dodajmy słowo kluczowe data do klasy User:

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

W języku Kotlin, podobnie jak w języku Java, można mieć konstruktor podstawowy i co najmniej jeden konstruktor dodatkowy. Konstruktor w przykładzie powyżej jest konstruktorem podstawowym klasy User. Jeśli konwertujesz klasę Java, która ma wiele konstruktorów, konwerter automatycznie utworzy w Kotlinie kilka konstruktorów. Są one definiowane za pomocą słowa kluczowego constructor.

Jeśli chcemy utworzyć instancję tej klasy, możemy to zrobić w ten sposób:

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

Równość

Kotlin ma 2 rodzaje równości:

  • Równość strukturalna używa operatora == i wywołuje funkcję equals(), aby określić, czy 2 instancje są równe.
  • Równość referencyjna używa operatora === i sprawdza, czy 2 odwołania wskazują ten sam obiekt.

Właściwości zdefiniowane w konstruktorze podstawowym klasy danych będą używane do sprawdzania równości strukturalnej.

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

4. Argumenty domyślne, argumenty nazwane

W Kotlinie możemy przypisywać wartości domyślne do argumentów w wywołaniach funkcji. Gdy argument zostanie pominięty, używana jest wartość domyślna. W Kotlinie konstruktory są też funkcjami, więc możemy użyć argumentów domyślnych, aby określić, że domyślna wartość lastName to null. W tym celu przypisujemy null do lastName.

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

Kotlin umożliwia oznaczanie argumentów podczas wywoływania funkcji:

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

W innym przypadku załóżmy, że atrybut firstName ma wartość domyślną null, a atrybut lastName nie ma. W tym przypadku, ponieważ parametr domyślny poprzedza parametr bez wartości domyślnej, musisz wywołać funkcję z argumentami nazwanymi:

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

Wartości domyślne to ważne i często używane pojęcie w kodzie Kotlin. W naszym laboratorium kodu chcemy zawsze określać imię i nazwisko w deklaracji obiektu User, więc nie potrzebujemy wartości domyślnych.

5. Inicjowanie obiektów, obiekty towarzyszące i singletony

Zanim przejdziesz dalej, upewnij się, że klasa User jest klasą data. Teraz przekonwertujmy klasę Repository na język Kotlin. Wynik automatycznej konwersji powinien wyglądać tak:

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

Sprawdźmy, co zrobił automatyczny konwerter:

  • Lista users może mieć wartość null, ponieważ obiekt nie został utworzony w momencie deklaracji.
  • Funkcje w Kotlinie, takie jak getUsers(), są deklarowane za pomocą modyfikatora fun.
  • Metoda getFormattedUserNames() jest teraz właściwością o nazwie formattedUserNames
  • Iteracja po liście użytkowników (która początkowo była częścią getFormattedUserNames() ma inną składnię niż w przypadku Javy.
  • Pole static jest teraz częścią bloku companion object
  • Dodano blok init

Zanim przejdziemy dalej, uporządkujmy nieco kod. Jeśli przyjrzymy się konstruktorowi, zauważymy, że konwerter przekształcił naszą users listę w listę modyfikowalną, która zawiera obiekty dopuszczające wartość null. Chociaż lista może być pusta, załóżmy, że nie może zawierać użytkowników o wartości null. Zróbmy więc tak:

  • Usuń ?User? w deklaracji typu users
  • Usuń znak ? w User? dla zwracanego typu getUsers(), aby zwracał List<User>?

Blok inicjujący

W języku Kotlin główny konstruktor nie może zawierać żadnego kodu, więc kod inicjujący umieszcza się w blokach init. Funkcja działa tak samo.

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

Większość init kodu odpowiada za inicjowanie właściwości. Można to też zrobić w deklaracji właściwości. Na przykład w wersji klasy Repository w języku Kotlin widzimy, że właściwość users została zainicjowana w deklaracji.

private var users: MutableList<User>? = null

Właściwości i metody static w języku Kotlin

W języku Java używamy słowa kluczowego static w przypadku pól lub funkcji, aby wskazać, że należą one do klasy, ale nie do jej instancji. Dlatego w naszej klasie Repository utworzyliśmy pole statyczne INSTANCE. Odpowiednikiem tego w języku Kotlin jest blok companion object. Tutaj należy też zadeklarować pola statyczne i funkcje statyczne. Konwerter utworzył blok obiektu towarzyszącego i przeniósł tu pole INSTANCE.

Obsługa pojedynczych elementów

Potrzebujemy tylko jednej instancji klasy Repository, dlatego w języku Java użyliśmy wzorca singleton. W języku Kotlin możesz wymusić ten wzorzec na poziomie kompilatora, zastępując słowo kluczowe class słowem object.

Usuń prywatny konstruktor i zastąp definicję klasy wartością object Repository. Usuń też obiekt towarzyszący.

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

W przypadku klasy object wywołujemy funkcje i właściwości bezpośrednio w obiekcie, np. tak:

val formattedUserNames = Repository.formattedUserNames

Pamiętaj, że jeśli właściwość nie ma modyfikatora widoczności, domyślnie jest publiczna, tak jak w przypadku właściwości formattedUserNames w obiekcie Repository.

6. Obsługa wartości null

Podczas konwertowania klasy Repository na Kotlin automatyczny konwerter ustawił listę użytkowników jako dopuszczającą wartość null, ponieważ podczas deklarowania nie została ona zainicjowana jako obiekt. W związku z tym we wszystkich przypadkach użycia obiektu users należy używać operatora potwierdzenia, że wartość nie jest zerowa – !!. (W przekonwertowanym kodzie zobaczysz znaki users!!user!!). Operator !! przekształca każdą zmienną w typ niepusty, dzięki czemu możesz uzyskiwać dostęp do jej właściwości lub wywoływać na niej funkcje. Jeśli jednak wartość zmiennej jest rzeczywiście zerowa, zostanie zgłoszony wyjątek. Korzystając z !!, ryzykujesz wystąpienie wyjątków w czasie działania programu.

Zamiast tego lepiej obsługiwać dopuszczalność wartości null za pomocą jednej z tych metod:

  • Sprawdzanie wartości null ( if (users != null) {...})
  • Używanie operatora elvis ?: (omówionego w dalszej części tego laboratorium)
  • używanie niektórych funkcji standardowych Kotlina (omówionych w dalszej części tego laboratorium);

W naszym przypadku wiemy, że lista użytkowników nie musi dopuszczać wartości null, ponieważ jest inicjowana zaraz po utworzeniu obiektu (w bloku init). Dzięki temu możemy bezpośrednio utworzyć instancję obiektu users podczas jego deklarowania.

Podczas tworzenia instancji typów kolekcji Kotlin udostępnia kilka funkcji pomocniczych, które zwiększają czytelność i elastyczność kodu. W tym przykładzie używamy MutableList dla users:

private var users: MutableList<User>? = null

Aby uprościć ten proces, możemy użyć funkcji mutableListOf() i podać typ elementu listy. mutableListOf<User>() tworzy pustą listę, która może zawierać obiekty User. Typ danych zmiennej może być teraz wywnioskowany przez kompilator, więc usuń jawną deklarację typu właściwości users.

private val users = mutableListOf<User>()

Zmieniliśmy też var na val, ponieważ użytkownicy będą zawierać odniesienie tylko do odczytu do listy użytkowników. Pamiętaj, że odwołanie jest tylko do odczytu, więc nigdy nie może wskazywać nowej listy, ale sama lista jest nadal modyfikowalna (możesz dodawać lub usuwać elementy).

Zmienna users jest już zainicjowana, więc usuń tę inicjację z bloku init:

users = ArrayList<Any?>()

Blok init powinien wyglądać tak:

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

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

Po wprowadzeniu tych zmian właściwość users nie jest już wartością null, więc możemy usunąć wszystkie niepotrzebne wystąpienia operatora !!. Pamiętaj, że w Android Studio nadal będą widoczne błędy kompilacji, ale wykonaj kolejne czynności z tego laboratorium, aby je rozwiązać.

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

Jeśli dla wartości userNames określisz typ ArrayList jako zawierający Strings, możesz usunąć jawny typ w deklaracji, ponieważ zostanie on wywnioskowany.

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

Destrukturyzacja

Kotlin umożliwia rozkładanie obiektu na kilka zmiennych za pomocą składni zwanej deklaracją rozkładającą. Tworzymy wiele zmiennych i możemy ich używać niezależnie od siebie.

Na przykład data klasy obsługują destrukturyzację, więc możemy destrukturyzować obiekt User w pętli for do (firstName, lastName). Dzięki temu możemy pracować bezpośrednio z wartościami firstNamelastName. Zaktualizuj pętlę for, jak pokazano poniżej. Zastąp wszystkie wystąpienia user.firstName ciągiem firstName, a ciąg user.lastName ciągiem 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 expression

Nazwy na liście userNames nie są jeszcze w odpowiednim formacie. Ponieważ zarówno lastName, jak i firstName mogą mieć wartość null, podczas tworzenia listy sformatowanych nazw użytkowników musimy uwzględnić dopuszczalność wartości null. Jeśli brakuje którejkolwiek z nazw, chcemy wyświetlać znak "Unknown". Ponieważ zmienna name nie zostanie zmieniona po jej jednorazowym ustawieniu, możemy użyć val zamiast var. Najpierw wprowadź tę zmianę.

val name: String

Przyjrzyj się kodowi, który ustawia zmienną name. Może Ci się wydawać, że ustawienie zmiennej na równą blokowi kodu if / else jest czymś nowym. Jest to dozwolone, ponieważ w Kotlinie ifwhen są wyrażeniami – zwracają wartość. Ostatni wiersz instrukcji if zostanie przypisany do name. Ten blok służy tylko do zainicjowania wartości name.

Zasadniczo ta logika oznacza, że jeśli element lastName ma wartość null, element name ma wartość firstName lub "Unknown".

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

Operator Elvis

Ten kod można zapisać w bardziej idiomatyczny sposób, używając operatora elvis ?:. Operator Elvisa zwraca wyrażenie po lewej stronie, jeśli nie ma wartości null, lub wyrażenie po prawej stronie, jeśli lewa strona ma wartość null.

W poniższym kodzie zwracana jest wartość firstName, jeśli nie jest ona wartością null. Jeśli firstName ma wartość null, wyrażenie zwraca wartość po prawej stronie , "Unknown":

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

7. Szablony ciągów znaków

Kotlin ułatwia pracę z String dzięki szablonom ciągów znaków. Szablony ciągów znaków umożliwiają odwoływanie się do zmiennych w deklaracjach ciągów znaków za pomocą symbolu $ przed zmienną. Możesz też umieścić wyrażenie w deklaracji ciągu znaków, umieszczając je w nawiasach klamrowych {} i używając przed nim symbolu $. Przykład: ${user.firstName}.

Twój kod używa obecnie łączenia ciągów znaków, aby połączyć firstNamelastName w nazwę użytkownika.

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

Zamiast tego zastąp konkatenację ciągów znaków tym kodem:

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

Używanie szablonów ciągów znaków może uprościć kod.

IDE będzie wyświetlać ostrzeżenia, jeśli istnieje bardziej idiomatyczny sposób napisania kodu. W kodzie zobaczysz faliste podkreślenie, a gdy najedziesz na nie kursorem, pojawi się sugestia dotycząca refaktoryzacji kodu.

Obecnie powinno się wyświetlać ostrzeżenie, że deklarację name można połączyć z przypisaniem. Zastosujmy to. Ponieważ typ zmiennej name można wywnioskować, możemy usunąć jawną deklarację typu String. Teraz formattedUserNames wygląda tak:

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
    }

Możemy wprowadzić jeszcze jedną zmianę. Nasza logika interfejsu wyświetla "Unknown", jeśli brakuje imienia i nazwiska, więc nie obsługujemy obiektów o wartości null. Dlatego w przypadku typu danych formattedUserNames zamień List<String?> na List<String>.

val formattedUserNames: List<String>

8. Operacje na kolekcjach

Przyjrzyjmy się bliżej formattedUserNames getterowi i zobaczmy, jak możemy go ulepszyć. Obecnie kod wykonuje te czynności:

  • Tworzy nową listę ciągów znaków.
  • Przechodzi przez listę użytkowników
  • Tworzy sformatowaną nazwę każdego użytkownika na podstawie jego imienia i nazwiska.
  • Zwraca nowo utworzoną listę.
    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 udostępnia obszerną listę transformacji kolekcji, które przyspieszają i zwiększają bezpieczeństwo programowania dzięki rozszerzeniu możliwości interfejsu Java Collections API. Jedną z nich jest funkcja map. Ta funkcja zwraca nową listę zawierającą wyniki zastosowania podanej funkcji przekształcania do każdego elementu na liście pierwotnej. Zamiast tworzyć nową listę i ręcznie przeglądać listę użytkowników, możemy użyć funkcji map i przenieść logikę, którą mieliśmy w pętli for, do treści map. Domyślnie nazwa bieżącego elementu listy używana w map to it, ale dla większej czytelności możesz zastąpić it własną nazwą zmiennej. W tym przykładzie nazwijmy go 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
            }
        }

Zwróć uwagę, że używamy operatora Elvisa, aby zwrócić "Unknown", jeśli user.lastName ma wartość null, ponieważ user.lastName jest typu String?, a w przypadku name wymagana jest wartość String.

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

Aby jeszcze bardziej to uprościć, możemy całkowicie usunąć zmienną name:

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. Właściwości i właściwości pomocnicze

Zauważyliśmy, że automatyczny konwerter zastąpił funkcję getFormattedUserNames() właściwością o nazwie formattedUserNames, która ma niestandardowy getter. W tle Kotlin nadal generuje metodę getFormattedUserNames(), która zwraca List.

W języku Java właściwości klasy udostępniamy za pomocą funkcji pobierających i ustawiających. Kotlin pozwala nam lepiej rozróżniać właściwości klasy, wyrażone za pomocą pól, oraz funkcje, czyli działania, które klasa może wykonywać, wyrażone za pomocą funkcji. W naszym przypadku klasa Repository jest bardzo prosta i nie wykonuje żadnych działań, więc zawiera tylko pola.

Logika, która była wywoływana w funkcji Java getFormattedUserNames(), jest teraz wywoływana podczas wywoływania funkcji pobierającej właściwość Kotlin formattedUserNames.

Nie mamy pola odpowiadającego właściwości formattedUserNames, ale Kotlin udostępnia automatyczne pole zapasowe o nazwie field, do którego w razie potrzeby możemy uzyskać dostęp z niestandardowych funkcji pobierających i ustawiających.

Czasami jednak potrzebujemy dodatkowych funkcji, których automatyczne pole zapasowe nie zapewnia.

Przeanalizujmy to na przykładzie.

W klasie Repository mamy modyfikowalną listę użytkowników, która jest udostępniana w funkcji getUsers() wygenerowanej z naszego kodu Java:

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

Nie chcieliśmy, aby wywołujący klasę Repository mogli modyfikować listę użytkowników, dlatego utworzyliśmy funkcję getUsers(), która zwraca obiekt List<User> tylko do odczytu. W przypadku języka Kotlin preferujemy używanie właściwości zamiast funkcji. Dokładniej mówiąc, udostępnimy interfejs List<User> tylko do odczytu, który jest obsługiwany przez interfejs mutableListOf<User>.

Najpierw zmieńmy nazwę users na _users. Zaznacz nazwę zmiennej, kliknij ją prawym przyciskiem myszy i wybierz Refaktoryzacja > Zmień nazwę. Następnie dodaj publiczną usługę tylko do odczytu, która zwraca listę użytkowników. Nazwijmy ją users:

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

W tym momencie możesz usunąć formę płatności getUsers().

Po wprowadzeniu powyższej zmiany prywatna usługa _users stanie się usługą pomocniczą dla publicznej usługi users. Poza klasą Repository lista _users nie jest modyfikowalna, ponieważ użytkownicy klasy mogą uzyskać do niej dostęp tylko za pomocą users.

Gdy funkcja users jest wywoływana z kodu Kotlin, używana jest implementacja List z biblioteki standardowej Kotlin, w której lista nie jest modyfikowalna. Jeśli funkcja users jest wywoływana z Javy, używana jest implementacja java.util.List, w której lista jest modyfikowalna i dostępne są operacje takie jak add() i remove().

Pełny kod:

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. Funkcje i właściwości najwyższego poziomu oraz rozszerzenia

Obecnie klasa Repository wie, jak obliczyć sformatowaną nazwę użytkownika dla obiektu User. Jeśli jednak chcemy ponownie użyć tej samej logiki formatowania w innych klasach, musimy ją skopiować i wkleić lub przenieść do klasy User.

Kotlin umożliwia deklarowanie funkcji i właściwości poza klasą, obiektem lub interfejsem. Na przykład funkcja mutableListOf(), której użyliśmy do utworzenia nowej instancji List, jest już zdefiniowana w Collections.kt z biblioteki standardowej języka Kotlin.

W języku Java, gdy potrzebujesz jakiejś funkcji narzędziowej, najprawdopodobniej utworzysz klasę Util i zadeklarujesz tę funkcję jako funkcję statyczną. W Kotlinie możesz deklarować funkcje najwyższego poziomu bez konieczności tworzenia klasy. Kotlin umożliwia jednak też tworzenie funkcji rozszerzeń. Są to funkcje, które rozszerzają określony typ, ale są zadeklarowane poza nim.

Widoczność funkcji i właściwości rozszerzenia można ograniczyć za pomocą modyfikatorów widoczności. Ograniczają one użycie tylko do klas, które potrzebują rozszerzeń, i nie zanieczyszczają przestrzeni nazw.

W przypadku klasy User możemy dodać funkcję rozszerzenia, która oblicza sformatowaną nazwę, lub przechowywać sformatowaną nazwę we właściwości rozszerzenia. Można go dodać poza klasą Repository, w tym samym pliku:

// 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

Możemy wtedy używać funkcji i właściwości rozszerzenia tak, jakby były częścią klasy User.

Sformatowana nazwa jest właściwością klasy User, a nie funkcją klasy Repository, więc użyjemy właściwości rozszerzenia. Nasz plik Repository wygląda teraz tak:

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

Biblioteka standardowa Kotlina używa funkcji rozszerzeń do rozszerzania funkcjonalności kilku interfejsów API Javy. Wiele funkcji w IterableCollection jest zaimplementowanych jako funkcje rozszerzeń. Na przykład funkcja map, której użyliśmy w poprzednim kroku, jest funkcją rozszerzenia w Iterable.

11. Funkcje zakresu: let, apply, with, run, also

W kodzie zajęć Repository dodajemy kilka obiektów User do listy _users. Dzięki funkcjom zakresu Kotlin te wywołania mogą być bardziej idiomatyczne.

Aby wykonać kod tylko w kontekście konkretnego obiektu bez konieczności uzyskiwania do niego dostępu na podstawie jego nazwy, Kotlin oferuje 5 funkcji zakresu: let, apply, with, runalso. Dzięki tym funkcjom kod jest bardziej czytelny i zwięzły. Wszystkie funkcje zakresu mają odbiorcę (this), mogą mieć argument (it) i mogą zwracać wartość.

Oto przydatna ściągawka, która pomoże Ci zapamiętać, kiedy używać poszczególnych funkcji:

6b9283d411fb6e7b.png

Ponieważ konfigurujemy obiekt _users w obiekcie Repository, możemy uprościć kod, używając funkcji apply:

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. Podsumowanie

W tym ćwiczeniu omówiliśmy podstawy, które pomogą Ci zacząć przekształcać kod z Javy na Kotlin. Ta konwersja jest niezależna od platformy programistycznej i pomaga zapewnić, że pisany przez Ciebie kod jest zgodny z zasadami języka Kotlin.

Idiomatic Kotlin sprawia, że pisanie kodu jest krótkie i przyjemne. Dzięki wszystkim funkcjom, jakie oferuje Kotlin, istnieje wiele sposobów na zwiększenie bezpieczeństwa, zwięzłości i czytelności kodu. Możemy na przykład zoptymalizować klasę Repository, tworząc instancję listy _users z użytkownikami bezpośrednio w deklaracji, co pozwoli nam pozbyć się bloku init:

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

Omówiliśmy szeroki zakres tematów, od obsługi dopuszczalności wartości null, singletonów, ciągów znaków i kolekcji po funkcje rozszerzające, funkcje najwyższego poziomu, właściwości i funkcje zakresu. Z 2 klas Javy przeszliśmy na 2 klasy Kotlin, które wyglądają teraz tak:

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

Oto podsumowanie funkcji Java i ich odpowiedników w Kotlinie:

Java

Kotlin

final obiekt

val obiekt

equals()

==

==

===

Klasa, która zawiera tylko dane

data zajęcia

Inicjowanie w konstruktorze

Inicjowanie w bloku init

static pola i funkcje

pola i funkcje zadeklarowane w companion object,

Klasa singleton

object

Więcej informacji o języku Kotlin i sposobie jego używania na Twojej platformie znajdziesz w tych materiałach: