Informacje o tym ćwiczeniu (w Codelabs)
1. Witamy!
Z tego modułu dowiesz się, jak przekonwertować kod z kodu Java na Kotlin. Dowiesz się też, na czym polegają konwencje językowe Kotlina i jak upewnić się, że kod, który piszesz, jest z nimi zgodny.
To ćwiczenie z programowania jest przeznaczone dla każdego dewelopera, który używa języka Java i rozważa migrację projektu do Kotlina. Rozpoczniemy od kilku kursów Java, które zostaną przekształcone w Kotlin za pomocą IDE. Przyjrzymy się teraz przekonwertowanemu kodowi i zobaczymy, jak możemy go ulepszyć, zwiększając jego idiomatic unikanie typowych problemów.
Czego się nauczysz
Dowiedz się, jak przekonwertować kod Java na Kotlin. Dzięki temu poznasz funkcje i koncepcje języka Kotlin:
- Obsługa wartości null
- Implementowanie singli
- Klasy danych
- Obsługa ciągów tekstowych
- Operator Elvisa
- Niszczenie
- Właściwości i zasoby zapasowe
- Domyślne argumenty i parametry nazwane
- Praca z kolekcjami
- Funkcje rozszerzeń
- Funkcje i parametry najwyższego poziomu
- Słowa kluczowe:
let
,apply
,with
irun
Założenia
Musisz znać język Java.
Czego potrzebujesz
2. Konfiguracja
Tworzenie nowego projektu
Jeśli używasz IntelliJ IDEA, utwórz nowy projekt w Javie z kotlin/JVM.
Jeśli używasz Androida Studio, utwórz nowy projekt bez aktywności.
Kod
Utworzymy obiekt modelu User
i klasę jednotonową Repository
, która działa z obiektami User
i wyświetla listy użytkowników i sformatowane nazwy użytkowników.
Utwórz nowy plik User.java
o nazwie app/java/<nazwa_pakietu> i wklej 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;
}
}
W zależności od typu projektu zaimportuj androidx.annotation.Nullable
, jeśli używasz projektu na Androida lub 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 wartości null, val, var i danych
Nasz IDE może nieźle radzić sobie z automatycznym refaktoryzacją kodu Java do kodu Kotlin, ale czasami wymaga to trochę pomocy. Zaczniemy od przeanalizowania kodu refaktorowego, by dowiedzieć się, jak i dlaczego został on poddany refaktoryzacji w ten sposób.
Otwórz plik User.java
i przekonwertuj go na Kotlin: Pasek menu -> Kod -> Przekonwertuj plik Java na Kotlin.
Jeśli IDE wyświetla komunikat o konieczności korekty po konwersji, naciśnij Tak.
Powinien wyświetlić się ten kod Kotlin:
class User(var firstName: String?, var lastName: String?)
Nazwa usługi User.java
została zmieniona na User.kt
. Pliki Kotlin mają rozszerzenie .kt.
Na zajęciach w języku Java User
mamy 2 usługi: firstName
i lastName
. Każda z nich korzystała z metody getter i seter, co zmienia wartość. Słowo kluczowe Kotlin dla zmiennych zmiennego to var
, więc osoba dokonująca konwersji używa var
w każdej z tych właściwości. Gdyby nasze usługi Java miały tylko metody getter, byłyby stałe i zostały zadeklarowane jako zmienne val
. val
jest podobne do słowa kluczowego final
w Javie.
Jedną z głównych różnic między Kotlin a Java jest to, że Kotlin wyraźnie określa, czy zmienna może zaakceptować wartość null. Aby to zrobić, należy dodać deklarację typu „?
”.
Ponieważ firstName
i lastName
oznaczyliśmy jako wartości null, automatyczne konwertery automatycznie oznaczyły właściwości jako null i String?
. Jeśli oznaczysz członków Java jako „null” (za pomocą org.jetbrains.annotations.NotNull
lub androidx.annotation.NonNull
), użytkownik dokonujący konwersji rozpozna to i zmieni też pola w kotlinie jako puste.
Podstawowy refaktoryzacja jest już wykonana. Możemy jednak napisać to w bardziej identyczny sposób. Zobaczmy, jak to zrobić.
Klasa danych
Klasa User
zawiera tylko dane. Kotlin ma słowo kluczowe dla zajęć o tej roli: data
. Jeśli oznaczysz te zajęcia jako klasy data
, kompilator sam utworzy dla Ciebie klasy getter i setery. Będzie też funkcja equals()
, hashCode()
i toString()
.
Dodajmy słowo kluczowe data
do naszej klasy User
:
data class User(var firstName: String, var lastName: String)
Kotlin, podobnie jak Java, może mieć główny konstruktor i co najmniej jeden konstruktor dodatkowy. Ten w powyższym przykładzie jest głównym konstruktorem klasy użytkownika. Jeśli przekonwertujesz klasę Java, która ma wiele konstruktorów, moduł konwersji automatycznie utworzy też wiele konstruktorów w Kotlinie. Są one definiowane za pomocą słowa kluczowego constructor
.
Jeśli chcesz utworzyć instancję tej klasy, wykonaj następujące czynności:
val user1 = User("Jane", "Doe")
Równość
Kotlin stosuje dwa rodzaje równości:
- Równość strukturalna korzysta z operatora
==
i wywołuje polecenieequals()
, aby określić, czy 2 wystąpienia są równe. - Równość referencyjna korzysta z operatora
===
i sprawdza, czy 2 odwołania wskazują ten sam obiekt.
Właściwości zdefiniowane w podstawowym konstruktorze klasy danych będą używane do kontroli strukturalnej równości.
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 Kotlin możemy przypisywać wartości domyślne do argumentów w wywołaniach funkcji. W przypadku pominięcia argumentu używana jest wartość domyślna. Kotlin to również konstrukcje, więc możemy użyć domyślnych argumentów, aby określić, że domyślna wartość lastName
to null
. W tym celu przypisujemy numer null
do organizacji lastName
.
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")
Parametry funkcji możesz nazwać podczas wywoływania funkcji:
val john = User(firstName = "John", lastName = "Doe")
Załóżmy, że firstName
ma wartość domyślną null
, a lastName
nie. W tym przypadku parametr domyślny jest poprzedzony parametrem bez wartości domyślnej, więc trzeba 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")
5. Inicjowanie obiektu, obiekt towarzyszący i pojedyncze tony
Zanim przejdziesz dalej, upewnij się, że klasa User
jest klasą data
. Przekonwertujmy klasę Repository
na 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)
}
}
Zobaczmy, co zrobiła automatyczna konwersja:
- Dodano blok
init
(Repository.kt#L50) - Pole
static
jest teraz częścią blokucompanion object
(Repozytorium.kt#L33) - Lista
users
ma wartość null, ponieważ obiekt nie został utworzony w momencie deklaracji (Repository.kt#L7). - Metoda
getFormattedUserNames()
jest teraz usługą o nazwieformattedUserNames
(Repository.kt#L11) - iteracja nad listą użytkowników (która początkowo była częścią
getFormattedUserNames(
) ) ma inną składnię niż Java (Repository.kt#L15)
Zanim przejdziemy dalej, nieco uporządkujemy kod. Jak widać, użytkownik dokonujący konwersji przekształcił naszą listę users
w możliwą do zmodyfikowania listę zawierającą obiekty null. Lista może mieć wartość null, ale nie może zawierać użytkowników o wartości null. Wykonajmy więc te czynności:
- Usuń
?
zUser?
w deklaracji typuusers
- Parametr
getUsers
powinien zwrócić wartośćList<User>?
Automatycznie konwertuje również niepotrzebnie podzielone na 2 wiersze deklaracje zmiennych użytkownika i zmiennych zdefiniowanych w bloku init. Każda deklaracja zmiennej znajduje się w jednym wierszu. Kod powinien wyglądać tak:
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)
}
}
Blokowanie Init
W Kotlin główny konstruktor nie może zawierać żadnego kodu, więc kod inicjowania jest umieszczany w blokach init
. Ich 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)
}
}
Znaczna część kodu init
obsługuje właściwości inicjowania. Możesz to zrobić w deklaracji właściwości. Na przykład w klasie w Kotlin klasy Repository
widzimy, że właściwość użytkowników została zainicjowana w deklaracji.
private var users: MutableList<User>? = null
Usługi i metody static
Kotlin
W języku Java używamy słowa kluczowego static
w polach lub funkcjach, aby wskazywać, że element należy do klasy, ale nie do wystąpienia klasy. Dlatego utworzyliśmy pole statyczne INSTANCE
w klasie Repository
. Odpowiednikiem Kotlina jest blok companion object
. Następnie trzeba zadeklarować pola statyczne i funkcje statyczne. Użytkownik, który dokonał konwersji, utworzył i przeniósł pole INSTANCE
.
Obsługa jednotonów
Potrzebujemy tylko jednego wystąpienia klasy Repository
, dlatego użyliśmy wzorca pojedynczego adresowania w Javie. Korzystając z Kotlin, możesz wymusić stosowanie tego wzorca na poziomie kompilacji, zastępując słowo kluczowe class
wartością object
.
Usuń konstruktor prywatny oraz obiekt towarzyszący i zastąp definicję klasy 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)
}
}
Gdy używasz klasy object
, funkcje i właściwości są wywoływane bezpośrednio w obiekcie, na przykład:
val users = Repository.users
Nękanie
Kotlin umożliwia zniszczenie obiektu w wielu zmiennych przy użyciu składni zwanej destrukcją zniszczenia. Tworzymy wiele zmiennych i możemy ich używać niezależnie.
Na przykład klasy danych obsługują zniszczenie, aby móc zniszczyć obiekt User
w pętli for
do (firstName, lastName)
. Dzięki temu możemy pracować bezpośrednio z wartościami firstName
i lastName
. Zaktualizujmy pętlę for
w ten sposób:
for ((firstName, lastName) in users!!) {
val name: String?
if (lastName != null) {
if (firstName != null) {
name = "$firstName $lastName"
} else {
name = lastName
}
} else if (firstName != null) {
name = firstName
} else {
name = "Unknown"
}
userNames.add(name)
}
6. Obsługa wartości null
Podczas konwertowania klasy Repository
na Kotlin lista użytkowników, którzy dokonali konwersji, została uznana za unieważnioną, ponieważ nie została ona zainicjowana do obiektu podczas jego rejestrowania. Przy wszystkich zastosowaniach obiektu users
używany jest niepusty operator asercji !!
. Konwertuje dowolną zmienną na typ inny niż null i tworzy wyjątek, jeśli wartość jest pusta. Korzystanie z narzędzia !!
wiąże się z ryzykiem odrzucenia wyjątków w czasie działania.
Zamiast tego lepiej jest użyć wartości null. Aby to zrobić, użyj jednej z tych metod:
- Przeprowadzam kontrolę o wartości null (
if (users != null) {...}
) - Stosowanie operatora Elvisa
?:
(opisane później w ćwiczeniach z programowania) - Korzystanie z niektórych funkcji standardowych Kotlin (opisanych w dalszej części ćwiczenia z programowania)
W naszym przypadku lista użytkowników nie musi być null, bo inicjuje się zaraz po utworzeniu obiektu, więc możemy za jego pomocą natychmiast utworzyć instancję.
Podczas tworzenia wystąpień typów kolekcji Kotlin udostępnia kilka funkcji pomocniczych, aby kod był bardziej czytelny i elastyczny. Oto nazwa domeny MutableList
dla users
:
private var users: MutableList<User>? = null
Dla uproszczenia możemy użyć funkcji mutableListOf()
, przekazać typ elementu listy, usunąć wywołanie konstruktora ArrayList
z bloku init
i usunąć konkretną deklarację typu właściwości users
.
private val users = mutableListOf<User>()
Zmieniliśmy też zmienną var val, ponieważ użytkownicy będą zawierać stałe odniesienie do listy użytkowników. Pamiętaj, że odniesienie nie może być stałe, ale lista jest stała (możesz dodawać i usuwać elementy).
Dzięki tym zmianom nasza właściwość users
nie jest już pusta i możemy usunąć wszystkie niepotrzebne wystąpienia operatora !!
.
val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
...
}
Poza tym ze względu na to, że zmienna „user” została już zainicjowana, musimy ją usunąć z bloku init
:
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users.add(user1)
users.add(user2)
users.add(user3)
}
Zarówno wartość lastName
, jak i firstName
może wynosić null
, dlatego podczas tworzenia listy sformatowanych nazw użytkowników musimy użyć wartości null. Jeśli chcemy wyświetlać "Unknown"
, jeśli brakuje którejś z tych nazw, możemy usunąć ją z deklaracji ?
bez wartości null.
val name: String
Jeśli lastName
ma wartość NULL, name
jest firstName
lub "Unknown"
:
if (lastName != null) {
if (firstName != null) {
name = "$firstName $lastName"
} else {
name = lastName
}
} else if (firstName != null) {
name = firstName
} else {
name = "Unknown"
}
Możesz to zapisać w sposób bardziej idiotyczny, używając operatora Elvisa ?:
. Operator elvis zwróci wyrażenie po lewej stronie, jeśli nie jest puste (lub nie ma wartości null).
Jeśli ten kod nie jest pusty, zwracany jest kod user.firstName
. Jeśli user.firstName
ma wartość NULL, wyrażenie zwraca wartość po prawej stronie, "Unknown"
:
if (lastName != null) {
...
} else {
name = firstName ?: "Unknown"
}
7. Szablony ciągów i wyrażenie if
Kotlin ułatwia współpracę z String
w szablonach String. Szablony ciągów umożliwiają odwoływanie się do zmiennych w deklaracjach ciągu.
Automatyczna konwersja dokonała połączenia imienia i nazwiska, by odwoływały się do nazwy zmiennej bezpośrednio w ciągu, używając symbolu $
i umieszczając wyrażenie między { }
.
// Java
name = user.getFirstName() + " " + user.getLastName();
// Kotlin
name = "${user.firstName} ${user.lastName}"
W kodzie zastąp ciąg znaków ciągiem:
name = "$firstName $lastName"
W Kotlin if
, when
, for
i while
to wyrażenia – zwracają wartość. IDE pokazuje nawet ostrzeżenie, że przypisanie powinno zostać usunięte z if
:
Zobaczmy sugestię IDE i cofnij przypisanie dla obu instrukcji if
. Ostatni wiersz instrukcji „if” zostanie przypisany. W ten sposób można łatwo zainicjować wartość nazwy w tym bloku:
name = if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName
}
} else {
firstName ?: "Unknown"
}
Następnie pojawi się ostrzeżenie, że można dołączyć deklarację name
. Zastosujmy to też. Ponieważ typ zmiennej nazwy może być wydychany, możemy usunąć deklarację konkretnego typu. Teraz formattedUserNames
wygląda tak:
val formattedUserNames: List<String?>
get() {
val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
val name = if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName
}
} else {
firstName ?: "Unknown"
}
userNames.add(name)
}
return userNames
}
8. Operacje na kolekcjach
Przyjrzyjmy się bliżej getrowi formattedUserNames
i zobaczmy, jak możemy zwiększyć jego idiomatykę. Teraz kod wykonuje te czynności:
- Tworzy nową listę ciągów znaków
- Przeglądanie 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ę przekształceń kolekcji, które przyspieszają i zwiększają możliwości programowania dzięki wykorzystaniu możliwości interfejsu Java Collection API. Jedną z nich jest funkcja map
. Ta funkcja zwraca nową listę zawierającą wyniki zastosowania określonej funkcji przekształcenia do każdego elementu na oryginalnej liście. Dlatego zamiast tworzyć nową listę i szybko przeglądać listę użytkowników, możemy użyć funkcji map
i przeprowadzić działanie logiczne w pętli for
w treści map
. Domyślnie nazwa bieżącego elementu listy używanego w polu map
to it
, ale aby można było odczytać, it
można zastąpić 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
}
}
Aby jeszcze bardziej uprościć to działanie, 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 zasoby zapasowe
Wykryliśmy, że automatyczna konwertacja zastąpiła funkcję getFormattedUserNames()
właściwością formattedUserNames
, która ma niestandardowy moduł getter. Pod maską Kotlin nadal generuje metodę getFormattedUserNames()
, która zwraca List
.
W Javie ujawnimy właściwości klasy za pomocą funkcji getter i seter. Kotlin umożliwia nam rozróżnianie między klasami wyrażonymi polami i funkcjami, które klasa może wykonywać w ramach funkcji wyrażonych za pomocą funkcji. W naszym przypadku klasa Repository
jest bardzo prosta i nie wykonuje żadnych działań, więc ma tylko pola.
Logika, która została wywołana w funkcji Java getFormattedUserNames()
jest teraz wyzwalana podczas pobierania metody formattedUserNames
usługi Kotlin.
Chociaż nie mamy pola odpowiadającego właściwości formattedUserNames
, Kotlin udostępnia automatyczne pole o nazwie field
, do którego mamy dostęp w razie potrzeby od niestandardowych nadawców i osób określających.
Potrzebujemy czasem dodatkowych funkcji, których nie oferuje pole do automatycznego tworzenia kopii zapasowych. Przeanalizujmy to poniżej.
W klasie Repository
znajduje się zmienna lista użytkowników, które są udostępniany przez funkcję getUsers()
wygenerowaną na podstawie kodu Java:
fun getUsers(): List<User>? {
return users
}
Problem polega na tym, że gdy zwracamy wartość users
, każdy klient klasy Repozytorium może modyfikować naszą listę użytkowników – nie jest to dobry pomysł. Rozwiążmy ten problem, korzystając z usługi zapasowej.
Najpierw zmień nazwę elementu users
na _users
. Teraz dodaj stałą właściwość publiczną, która zwraca listę użytkowników. Nazwijmy ją users
:
private val _users = mutableListOf<User>()
val users: List<User>
get() = _users
Po wprowadzeniu tej zmiany prywatna usługa _users
stanie się usługą zapasową dla publicznej usługi users
. Poza klasą Repository
nie można modyfikować listy _users
, ponieważ dostęp do niej mają tylko users
.
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 rozszerzeń
Obecnie klasa Repository
wie, jak obliczyć sformatowaną nazwę użytkownika obiektu User
. Jeśli jednak chcesz wykorzystać tę samą zasadę formatowania w innych zajęciach, musisz ją skopiować i wkleić lub przenieść do klasy User
.
Kotlin umożliwia deklarowanie funkcji i właściwości poza dowolną klasą, obiektem lub interfejsem. Na przykład funkcja mutableListOf()
, która została użyta do utworzenia nowego wystąpienia obiektu List
, jest zdefiniowana bezpośrednio w Collections.kt
w Bibliotece standardowej.
Gdy potrzebujesz obsługi jakiejś funkcji w języku Java, prawdopodobnie tworzysz klasę Util
i deklarujesz ją jako funkcję statyczną. W Kotlin możesz zadeklarować funkcje najwyższego poziomu bez klasy. Kotlin umożliwia też jednak tworzenie funkcji rozszerzeń. Są to funkcje, które rozszerzają określony typ, ale są zadeklarowane poza typem. Mają więc podobne zainteresowania.
Widoczność funkcji i rozszerzeń rozszerzeń można ograniczyć, stosując modyfikatory widoczności. Ograniczają one użycie tylko do klas, które wymagają rozszerzeń, i nie będą zaśmiecać przestrzeni nazw.
W przypadku klasy User
możemy dodać funkcję rozszerzenia do obliczania sformatowanej nazwy lub przechowywać nazwę sformatowaną we właściwości rozszerzenia. Możesz 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
Następnie możemy używać funkcji i właściwości rozszerzenia w taki sposób, jakby były one częścią klasy User
.
Sformatowana nazwa jest właściwością użytkownika, a nie klasy Repository
, dlatego użyjemy 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)
}
}
Biblioteka standardowa Kotlin używa funkcji rozszerzeń do rozszerzania funkcjonalności niektórych interfejsów API Java. Wiele funkcji Iterable
i Collection
implementujemy jako funkcje rozszerzeń. Na przykład funkcja map
użyta w poprzednim kroku to funkcja rozszerzenia w usłudze Iterable
.
11. Funkcje zakresu: zezwól, zastosuj, uruchom, również
W kodzie zajęć Repository
dodaliśmy kilka obiektów użytkownika do listy _users
. Dzięki funkcjom zakresu te wywołania mogą być bardziej idiotyczne.
Aby wykonać kod tylko w kontekście konkretnego obiektu, bez konieczności uzyskania dostępu do obiektu na podstawie jego nazwy, Kotlin utworzył 5 funkcji zakresu: let
, apply
, with
, run
i also
. Wszystkie te funkcje są krótkie i obszerne, mają odbiornik (this
), mogą mieć argument (it
) i mogą zwracać wartość. To, której z nich użyjesz, zależy od tego, co chcesz osiągnąć.
Oto przydatna ściągawka, która to pomoże:
Konfigurujemy obiekt _users
w Repository
, więc za pomocą funkcji apply
możemy zwiększyć kod 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 potrzebne do refaktoryzacji kodu z Javy do Kotlina. Ten współczynnik refaktoryzacji jest niezależny od platformy deweloperskiej, dzięki czemu masz pewność, że kod jest idiomatyczny.
Idiomatyczna Kotlin sprawia, że pisanie kodu jest krótkie i ciekawe. Kotlin oferuje wiele funkcji, dzięki którym Twój kod może być bardziej bezpieczny, zwięzły i czytelny. Możemy na przykład zoptymalizować klasę Repository
, tworząc listę _users
z użytkownikami bezpośrednio w deklaracji, pozbywając się bloku init
:
private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
omówiliśmy szeroką gamę tematów: od obsługi wartości null, singleton, Strings i collection po takie funkcje jak rozszerzenie, funkcje najwyższego poziomu, właściwości i zakresy. Przeszliśmy z dwóch klas Java na dwie klasy Kotlin, które wyglądają teraz tak:
User.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 }
}
Oto kierunek rozwoju funkcji Java oraz ich mapowanie na Kotlin:
Java | Kotlin |
|
|
|
|
|
|
Klasa, która zawiera tylko dane | Klasa |
Zdarzenie w konstruktorze | Inicjacja w bloku |
| pola i funkcje zadeklarowane w elemencie |
Klasa Singleton |
|
Aby dowiedzieć się więcej o Kotlin i sposobie korzystania z niego na swojej platformie, sprawdź te zasoby:
- Kotlin Koans
- Samouczki Kotlin
- Tworzenie aplikacji na Androida razem z Kotlin – bezpłatny kurs
- Kurs dotyczący Kotlina dla programistów
- Kotlin dla deweloperów Java – bezpłatny kurs w trybie kontrolnym