(Wycofane) Konwertowanie na Kotlin

1. Witamy!

Z tego Codelab dowiesz się, jak przekonwertować kod z Javy na Kotlin. Dowiesz się też, czym są konwencje języka Kotlin i jak pisać kod, który ich przestrzega.

Ten moduł jest odpowiedni dla każdego programisty, który używa Javy i rozważa przeniesienie projektu na Kotlin. Zaczniemy od kilku klas Java, które za pomocą IDE przekształcisz w Kotlin. Następnie przyjrzymy się przekonwertowanemu kodowi i sprawdzimy, jak go ulepszyć, aby był bardziej idiomatyczny i nie zawierał typowych błędów.

Czego się nauczysz

Dowiedz się, jak przekonwertować kod Java na Kotlin. W trakcie kursu poznasz te funkcje i koncepcje języka Kotlin:

  • Obsługa dopuszczalności wartości null
  • Implementowanie obiektów singleton
  • Klasy danych
  • Obsługa ciągów znaków
  • Operator Elvis
  • restrukturyzacji;
  • Właściwości i właściwości pomocnicze
  • Argumenty domyślne i parametry nazwane
  • Praca z kolekcjami
  • Funkcje rozszerzeń
  • Funkcje i parametry najwyższego poziomu
  • słowa kluczowe let, apply, with i run,

Założenia

Musisz już znać język Java.

Czego potrzebujesz

2. Przygotowania

Tworzenie nowego projektu

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

Jeśli używasz Android Studio, utwórz nowy projekt za pomocą szablonu Brak aktywności. Jako język projektu wybierz Kotlin. Minimalna wersja pakietu SDK może mieć dowolną wartość, która nie będzie miała wpływu na wynik.

Kod

Utworzymy obiekt modelu User i klasę singletona Repository, która współpracuje z obiektmi User i wyświetla listy użytkowników oraz sformatowane nazwy użytkowników.

W katalogu app/java/<nazwa_pakiet> utwórz nowy plik o nazwie User.java 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;
    }

}

IDE informuje, że @Nullable nie jest zdefiniowany. Jeśli używasz Android Studio, zaimportuj androidx.annotation.Nullable, a jeśli IntelliJ – org.jetbrains.annotations.Nullable.

Utwórz nowy plik o nazwie Repository.java i wklej 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 możliwości występowania wartości null, val, var i klas danych

Nasz IDE może automatycznie konwertować kod Java na kod Kotlin, ale czasami potrzebuje trochę pomocy. Pozwólmy IDE na wstępne przetworzenie. Następnie sprawdzimy uzyskany kod, aby zrozumieć, jak i dlaczego został on przekształcony.

Otwórz plik User.java i konwertuj go na Kotlin: pasek menu -> Kod -> Konwertuj plik Java na plik Kotlin.

Jeśli po konwersji IDE poprosi o wprowadzenie poprawek, naciśnij Tak.

e6f96eace5dabe5f.png

Powinien wyświetlić się ten kod Kotlina:

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

Uwaga: User.java zostało przemianowane na User.kt. Pliki Kotlina mają rozszerzenie .kt.

W klasie Java User mieliśmy 2 właściwości: firstNamelastName. Każdy z nich miał metodę pobierania i ustawiania, dzięki czemu jego wartość była zmienna. Słowo kluczowe Kotlina dla zmiennych zmiennych to var, więc konwerter używa var dla każdej z tych właściwości. Jeśli nasze właściwości w Javie miałyby tylko metody getter, byłyby one tylko do odczytu i zostałyby zadeklarowane jako zmienne val. val jest podobne do słowa kluczowego final w Javi.

Jedną z kluczowych różnic między Kotlinem a Java jest to, że Kotlin wyraźnie określa, czy zmienna może przyjmować wartość null. Dokonuje tego przez dodanie do deklaracji typu ?.

Ponieważ oznaczyliśmy właściwości firstNamelastName jako dopuszczające wartość null, konwerter automatyczny automatycznie oznaczył je jako dopuszczające wartość null z wartością String?. Jeśli opatrzysz elementy Java jako niepustych (za pomocą org.jetbrains.annotations.NotNull lub androidx.annotation.NonNull), konwerter rozpozna to i uczyni pola niepustymi także w Kotlinie.

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

Klasa danych

Klasa User zawiera tylko dane. Kotlin ma słowo kluczowe dla klas o tej roli: data. Dzięki oznaczeniu tej klasy jako klasy data kompilator automatycznie utworzy dla nas metody dostępu i ustaw. Wyznaczy też funkcje equals(), hashCode()toString().

Dodajmy słowo kluczowe data do klasy User:

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

Kotlin, podobnie jak Java, może mieć konstruktor główny i co najmniej jeden konstruktor dodatkowy. Konstruktor w tym przykładzie jest głównym konstruktorem klasy User. Jeśli konwertujesz klasę Java, która ma wiele konstruktorów, konwerter automatycznie utworzy też wiele konstruktorów w Kotlinie. 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ównouprawnienie

Kotlin ma 2 typy równości:

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

Właściwości zdefiniowane w głównym konstruktorze 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 przypisać wartości domyślne do argumentów w wywołaniach funkcji. Gdy argument jest 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. Aby to zrobić, przypisz 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") 

Inny przypadek użycia: wartość domyślna atrybutu firstName to null, a atrybutu lastName – nie. W tym przypadku parametr domyślny poprzedza parametr bez wartości domyślnej, więc musisz wywołać funkcję za pomocą argumentów nazwanych:

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żna i często używana koncepcja w kodzie Kotlina. W tym laboratorium chcemy, aby w deklaracji obiektu User zawsze podawano imię i nazwisko, dlatego nie potrzebujemy wartości domyślnych.

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

Zanim przejdziesz dalej, sprawdź, czy klasa User jest klasą data. Teraz przekształcimy klasę Repository na Kotlin. Wynik konwersji automatycznej 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)
    }
}

Zobaczmy, co zrobił konwerter automatyczny:

  • Lista users może być pusta, ponieważ obiekt nie został uruchomiony 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 na liście użytkowników (która początkowo była częścią funkcji getFormattedUserNames() ma inną składnię niż w Java.
  • Pole static jest teraz częścią bloku companion object
  • Dodano blok init

Zanim przejdziemy dalej, trochę uporządkujmy kod. Jeśli spojrzymy na konstruktor, zauważymy, że konwerter zamienił naszą listę users w listę zmienną, która zawiera obiekty z wartością dozwoloną null. Lista może być pusta, ale załóżmy, że nie może zawierać pustych użytkowników. Zróbmy to:

  • Usuń ?User? w deklaracji typu users
  • Usuń ?User? dla typu zwrotu getUsers(), aby zwróciło List<User>?

Blok początkowy

W Kotlinie główny konstruktor nie może zawierać żadnego kodu, więc kod inicjujący jest umieszczany w blokach init. Funkcje są takie same.

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ść kodu init obsługuje 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 widać, że w deklaracji inicjowana jest właściwość users.

private var users: MutableList<User>? = null

static właściwości i metody w Kotlinie,

W języku Java używamy słowa kluczowego static w przypadku pól lub funkcji, aby wskazać, że należą one do klasy, a nie do jej wystąpienia. Dlatego w klasie Repository utworzyliśmy pole statyczne INSTANCE. Jego odpowiednikiem w Kotlinie jest blok companion object. Tutaj deklarujesz też pola i funkcje statyczne. Konwerter utworzył blok obiektu towarzyszącego i przeniósł do niego pole INSTANCE.

Praca z elementami pojedynczymi

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

Usuń konstruktor prywatny 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)
    }
}

Gdy używasz klasy object, po prostu wywołujesz funkcje i właściwości bezpośrednio na obiekcie, np. w ten sposób:

val formattedUserNames = Repository.formattedUserNames

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

6. Obsługa dopuszczalności wartości null

Podczas konwertowania klasy Repository na Kotlin automatyczny konwerter uznał listę użytkowników za opcjonalną, ponieważ podczas deklarowania nie została ona zainicjowana jako obiekt. W rezultacie we wszystkich przypadkach użycia obiektu users należy używać operatora założenia niezerowego !!. (w konwertowanym kodzie zobaczysz wartości users!!user!!). Operator !! konwertuje dowolną zmienną na typ niepusty, dzięki czemu możesz uzyskać dostęp do właściwości lub wywołać funkcje. Jeśli jednak wartość zmiennej jest rzeczywiście pusta, zostanie rzucony wyjątek. Korzystając z funkcji !!, ryzykujesz wyrzucanie wyjątków w czasie wykonywania.

Zamiast tego preferuj obsługę wartości null za pomocą jednej z tych metod:

  • Sprawdzanie wartości null ( if (users != null) {...})
  • Używanie operatora elvis ?: (omówionego później w ćwiczeniach z programowania)
  • używanie niektórych standardowych funkcji Kotlina (omówionych później w tym samouczku),

W naszym przypadku wiemy, że lista użytkowników nie musi być typu nullable, ponieważ jest inicjowana zaraz po utworzeniu obiektu (w bloku init). Dzięki temu możemy bezpośrednio utworzyć instancję obiektu users, gdy go zadeklarujemy.

Podczas tworzenia instancji typów kolekcji Kotlin udostępnia kilka funkcji pomocniczych, które sprawiają, że kod jest bardziej czytelny i elastyczny. Tutaj używamy elementu MutableList w celu users:

private var users: MutableList<User>? = null

Dla uproszczenia możemy użyć funkcji mutableListOf() i podać typ elementu listy. mutableListOf<User>() tworzy pustą listę, która może zawierać obiekty User. Ponieważ kompilator może teraz określić typ danych zmiennej, usuń jawną deklarację typu właściwości users.

private val users = mutableListOf<User>()

Zmieniliśmy też wartość var na val, ponieważ użytkownicy będą zawierać odwołanie 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ć i usuwać elementy).

Ponieważ zmienna users jest już zainicjowana, usuń tę inicjalizację 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)
}

Dzięki tym zmianom właściwość users jest teraz niezerową, więc możemy usunąć wszystkie niepotrzebne wystąpienia operatora !!. Pamiętaj, że w Android Studio nadal będą się pojawiać błędy kompilacji, ale aby je naprawić, wykonaj kilka kolejnych kroków w programie Codelab.

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 w przypadku wartości userNames określisz typ ArrayList jako Strings, możesz usunąć jawny typ w deklaracji, ponieważ zostanie on wywnioskowany.

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

Dezorganizacja

Kotlin umożliwia destrukturyzację obiektu na kilka zmiennych za pomocą składni zwanej deklaracją destrukturyzacji. Tworzymy wiele zmiennych i możemy ich używać niezależnie.

Na przykład klasy data obsługują destrukturyzację, więc możemy zdestrukturyzować obiekt User w pętli for w obiekt (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 ciągu 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 pożądanym formacie. Ponieważ zarówno lastName, jak i firstName mogą być null, musimy uwzględnić możliwość braku wartości podczas tworzenia listy sformatowanych nazw użytkowników. Jeśli brakuje jednej z nazwy, chcemy wyświetlić "Unknown". Ponieważ zmienna name nie będzie się zmieniać po jej ustawieniu, możemy użyć val zamiast var. Najpierw wprowadź tę zmianę.

val name: String

Sprawdź kod, który ustawia zmienną name. Możesz zauważyć, że zmienna jest ustawiona tak, aby była równa blokowi kodu if / else. Jest to dozwolone, ponieważ w Kotlinie ifwhen są wyrażeniami – zwracają wartość. Ostatni wiersz instrukcji if zostanie przypisany do zmiennej name. Jedynym celem tego bloku jest zainicjowanie wartości name.

Zasada przedstawiona tutaj mówi, że jeśli lastName jest null, 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 napisać w bardziej idiomatyczny sposób, używając operatora elvis ?:. Operator elvis zwróci wyrażenie po lewej stronie, jeśli nie jest puste, lub wyrażenie po prawej stronie, jeśli lewy argument jest pusty.

Dlatego w poniższym kodzie zwracana jest wartość firstName, jeśli nie jest ona 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ągu 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 poprzedzielając je znakiem $. Przykład: ${user.firstName}.

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

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

Zamiast tego zastąp konkatenację ciągu znaków tym ciągiem:

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

Korzystanie z szablonów ciągów znaków może uprościć kod.

Jeśli istnieje bardziej idiomatyczny sposób napisania kodu, IDE wyświetli ostrzeżenia. W kodzie zobaczysz ukośną podkreślnię. Gdy na nią najedziesz kursorem, zobaczysz sugestię dotyczącą tego, jak przerobić kod.

Obecnie powinno pojawić się ostrzeżenie, że deklaracja name może zostać połączona z przypisaniem. Zastosujmy to. Typ zmiennej name można określić na podstawie innych zmiennych, więc możemy usunąć jawną deklarację typu String. Teraz nasz 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ą poprawkę. W przypadku braku imienia i nazwiska w interfejsie wyświetla się "Unknown", ponieważ nie obsługujemy obiektów null. W przypadku typu danych formattedUserNames zamień List<String?> na List<String>.

val formattedUserNames: List<String>

8. Operacje na kolekcjach

Przyjrzyjmy się bliżej metodzie formattedUserNames i sprawdźmy, jak można ją uczynić bardziej idiomatyczną. 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 ubezpieczają proces programowania, zwiększając możliwości interfejsu Java Collections API. Jednym z nich jest funkcja map. Ta funkcja zwraca nową listę zawierającą wyniki zastosowania danej funkcji przekształcenia do każdego elementu na pierwotnej liście. Zamiast tworzyć nową listę i ręcznie przeszukiwać listy użytkowników, możemy użyć funkcji map i przesunąć logikę z pętli for do ciała instrukcji map. Domyślnie nazwa bieżącego elementu listy używanego w funkcji map to it, ale ze względu na czytelność możesz zastąpić it własną nazwą zmiennej. W naszym przypadku 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ć wartość "Unknown", jeśli user.lastName jest równa null, ponieważ user.lastName ma typ String?, a w przypadku name wymagany jest typ String.

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

Aby jeszcze bardziej uprościć ten przykład, 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 niestandardową metodę gettera. W tle Kotlin nadal generuje metodę getFormattedUserNames(), która zwraca List.

W Javie właściwości klasy udostępniamy za pomocą funkcji getter i setter. Kotlin pozwala lepiej rozróżniać właściwości klasy wyrażane za pomocą pól oraz funkcje, czyli działania, które może wykonywać klasa, wyrażane 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 została wywołana w funkcji Java getFormattedUserNames(), jest teraz wywoływana podczas wywołania funkcji gettera właściwości Kotlin formattedUserNames.

Chociaż nie mamy pola odpowiadającego właściwości formattedUserNames, Kotlin udostępnia nam automatyczne pole pomocnicze o nazwie field, do którego w razie potrzeby możemy uzyskać dostęp z niestandardowych metod getter i setter.

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

Przyjrzyjmy się temu na przykładzie.

W klasie Repository mamy zmienną listę użytkowników, która jest wyświetlana w funkcji getUsers() wygenerowanej z naszego kodu Java:

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

Nie chcieliśmy, aby wywołujący klasę Repository modyfikowali listę użytkowników, dlatego utworzyliśmy funkcję getUsers(), która zwraca tylko do odczytu List<User>. W przypadku Kotlina zalecamy używanie właściwości zamiast funkcji. Dokładniej rzecz biorąc, udostępniamy tylko do odczytu List<User>, który jest obsługiwany przez mutableListOf<User>.

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

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

Teraz możesz usunąć metodę getUsers().

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

Gdy funkcja users jest wywoływana z kodu Kotlina, używana jest implementacja List z standardowej biblioteki Kotlina, w której lista nie jest modyfikowalna. Jeśli funkcja users jest wywoływana z Java, używana jest implementacja java.util.List, w której lista jest modyfikowalna, a 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 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 standardowej biblioteki Kotlina.

W języku Java, gdy potrzebujesz jakiejś funkcji pomocniczej, tworzysz klasę Util i deklarujesz tę funkcję jako funkcję statyczną. W Kotlinie możesz deklarować funkcje najwyższego poziomu bez tworzenia klasy. Kotlin umożliwia też tworzenie funkcji rozszerzeń. Są to funkcje, które rozszerzają określony typ, ale są zadeklarowane poza tym typem.

Widoczność funkcji i właściwości rozszerzenia można ograniczyć, używając 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 możemy przechowywać sformatowaną nazwę w właściwości rozszerzenia. Można je dodać poza zajęciami 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. 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)
    }
}

Standardowa biblioteka Kotlina używa funkcji rozszerzeń, aby rozszerzać funkcjonalność kilku interfejsów API Javy. Wiele funkcji w IterableCollection jest implementowanych jako funkcje rozszerzeń. Na przykład funkcja map użyta w poprzednim kroku jest funkcją rozszerzenia funkcji Iterable.

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

W kodzie klasy Repository dodajemy do listy _users kilka obiektów User. Za pomocą funkcji zakresu w Kotlinie można uczynić te wywołania bardziej idiomatycznymi.

Aby wykonać kod tylko w kontekście określonego obiektu, bez konieczności uzyskiwania dostępu do obiektu 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ą odbiornik (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ż w funkcji Repository konfigurujemy obiekt _users, możemy użyć funkcji apply, aby kod był bardziej idiomatyczny:

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 pozwolą Ci rozpocząć konwertowanie kodu z Java na Kotlin. Ta konwersja jest niezależna od platformy programistycznej i pomaga zapewnić, aby napisany przez Ciebie kod był zgodny z językiem Kotlin.

Dzięki idiomatycznemu Kotlinowi kod jest krótki i zwięzły. Dzięki wszystkim funkcjom Kotlina masz wiele sposobów na to, aby Twój kod był bezpieczniejszy, bardziej zwięzły i łatwiejszy do odczytania. Możemy na przykład zoptymalizować klasę Repository, tworząc instancję listy _users z użytkownikami bezpośrednio w deklaracji, dzięki czemu pozbędziemy się bloku init:

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

Omówiliśmy wiele tematów, od obsługi pustości, obiektów singleton, ciągów znaków i zbiorów po funkcje rozszerzeń, funkcje najwyższego poziomu, właściwości i funkcje zakresu. Zamiast 2 klas Java mamy teraz 2 klasy Kotlin, które wyglądają 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 krótkie podsumowanie funkcji Javy i ich mapowania na Kotlin:

Java

Kotlin

Obiekt final

Obiekt val

equals()

==

==

===

Klasa, która tylko przechowuje dane

Zajęcia: data

Inicjowanie w konstruktorze

Inicjowanie w bloku init

static pola i funkcje

pola i funkcje zadeklarowane w companion object

Klasa singleton

object

Aby dowiedzieć się więcej o Kotlinie i o tym, jak używać go na swojej platformie, zapoznaj się z tymi materiałami: