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
irun
,
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.
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: firstName
i lastName
. 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 firstName
i lastName
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()
i 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ą modyfikatorafun
- Metoda
getFormattedUserNames()
jest teraz właściwością o nazwieformattedUserNames
- 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ą blokucompanion 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ń
?
wUser?
w deklaracji typuusers
- Usuń
?
wUser?
dla typu zwrotugetUsers()
, aby zwróciłoList<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!!
i 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 firstName
i lastName
. 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 if
i when
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ć firstName
i lastName
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 Iterable
i Collection
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
, run
i also
. 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:
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 | Obiekt |
|
|
|
|
Klasa, która tylko przechowuje dane | Zajęcia: |
Inicjowanie w konstruktorze | Inicjowanie w bloku |
| pola i funkcje zadeklarowane w |
Klasa singleton |
|
Aby dowiedzieć się więcej o Kotlinie i o tym, jak używać go na swojej platformie, zapoznaj się z tymi materiałami:
- Kotlin Koans
- Samouczki dotyczące Kotlina
- Podstawy Kotlina na Androida
- Kotlin Bootcamp for Programmers
- Kotlin dla programistów Java – bezpłatny kurs w trybie przeglądania