Z tego laboratorium dowiesz się, jak przekonwertować kod z Javy na Kotlin. Dowiesz się też, jakie są konwencje języka Kotlin i jak zadbać o to, aby pisany przez Ciebie kod był z nimi zgodny.
Te warsztaty są przeznaczone dla wszystkich programistów, którzy używają języka Java i rozważają przeniesienie projektu na język Kotlin. Zaczniemy od kilku klas w języku Java, które przekonwertujesz na Kotlin za pomocą środowiska IDE. Następnie przyjrzymy się przekonwertowanemu kodowi i zobaczymy, jak możemy go ulepszyć, aby był bardziej idiomatyczny i pozwalał uniknąć typowych błędów.
Czego się nauczysz
Dowiesz się, jak przekonwertować kod Java na Kotlin. W ten sposób poznasz te funkcje i koncepcje języka Kotlin:
- Obsługa wartości null
- Implementowanie singletonów
- Klasy danych
- Obsługa ciągów znaków
- Operator Elvis
- Destrukturyzacja
- Właściwości i właściwości pomocnicze
- Argumenty domyślne i parametry nazwane
- Praca z kolekcjami
- Funkcje rozszerzeń
- Funkcje i parametry najwyższego poziomu
let,apply,withirun
Założenia
Musisz znać język Java.
Czego potrzebujesz
Tworzenie nowego projektu
Jeśli używasz IntelliJ IDEA, utwórz nowy projekt w języku Java z Kotlin/JVM.
Jeśli korzystasz z Androida Studio, utwórz nowy projekt bez aktywności.
Kod
Utworzymy obiekt modelu User i klasę singleton Repository, która współpracuje z obiektami User i udostępnia listy użytkowników oraz sformatowane nazwy użytkowników.
Utwórz nowy plik o nazwie User.java w katalogu app/java/<yourpackagename> i wklej do niego ten kod:
public class User {
@Nullable
private String firstName;
@Nullable
private String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}W zależności od typu projektu zaimportuj androidx.annotation.Nullable, jeśli używasz projektu na Androida, lub org.jetbrains.annotations.Nullable w innych przypadkach.
Utwórz nowy plik o nazwie Repository.java i wklej do niego ten kod:
import java.util.ArrayList;
import java.util.List;
public class Repository {
private static Repository INSTANCE = null;
private List<User> users = null;
public static Repository getInstance() {
if (INSTANCE == null) {
synchronized (Repository.class) {
if (INSTANCE == null) {
INSTANCE = new Repository();
}
}
}
return INSTANCE;
}
// keeping the constructor private to enforce the usage of getInstance
private Repository() {
User user1 = new User("Jane", "");
User user2 = new User("John", null);
User user3 = new User("Anne", "Doe");
users = new ArrayList();
users.add(user1);
users.add(user2);
users.add(user3);
}
public List<User> getUsers() {
return users;
}
public List<String> getFormattedUserNames() {
List<String> userNames = new ArrayList<>(users.size());
for (User user : users) {
String name;
if (user.getLastName() != null) {
if (user.getFirstName() != null) {
name = user.getFirstName() + " " + user.getLastName();
} else {
name = user.getLastName();
}
} else if (user.getFirstName() != null) {
name = user.getFirstName();
} else {
name = "Unknown";
}
userNames.add(name);
}
return userNames;
}
}Nasze środowisko IDE całkiem dobrze radzi sobie z automatycznym refaktoryzowaniem kodu Java na kod Kotlin, ale czasami potrzebuje pomocy. Najpierw to zrobimy, a potem przeanalizujemy przekształcony kod, aby zrozumieć, jak i dlaczego został on przekształcony w ten sposób.
Otwórz plik User.java i przekonwertuj go na plik Kotlin: pasek menu –> Kod –> Przekonwertuj plik Java na plik Kotlin.
Jeśli IDE wyświetli prośbę o korektę po konwersji, kliknij Yes (Tak).

Powinien się wyświetlić ten kod w języku Kotlin:
class User(var firstName: String?, var lastName: String?)Pamiętaj, że User.java zostało zmienione na User.kt. Pliki Kotlin mają rozszerzenie .kt.
W klasie Java User mieliśmy 2 właściwości: firstName i lastName. Każda z nich miała metodę pobierającą i ustawiającą, dzięki czemu jej wartość można było zmieniać. Słowem kluczowym w języku Kotlin oznaczającym zmienne modyfikowalne jest var, więc konwerter używa go w przypadku każdej z tych właściwości.var Gdyby nasze właściwości Javy miały tylko metody pobierające, byłyby niezmienne i zostałyby zadeklarowane jako zmienne val. val jest podobne do słowa kluczowego final w języku Java.
Jedną z głównych różnic między Kotlinem a Javą jest to, że Kotlin wyraźnie określa, czy zmienna może przyjmować wartość null. W tym celu do deklaracji typu dodaje znak „?”.
Ponieważ oznaczyliśmy typy firstName i lastName jako dopuszczające wartość null, automatyczny konwerter automatycznie oznaczył właściwości jako dopuszczające wartość null za pomocą typu String?. Jeśli oznaczysz elementy Java jako niepuste (za pomocą adnotacji org.jetbrains.annotations.NotNull lub androidx.annotation.NonNull), konwerter rozpozna to i ustawi pola jako niepuste również w Kotlinie.
Podstawowe refaktoryzowanie zostało już wykonane. Możemy jednak zapisać to w bardziej idiomatyczny sposób. Zobaczmy, jak to zrobić.
Klasa danych
Nasza klasa User zawiera tylko dane. Kotlin ma słowo kluczowe dla klas z tą rolą: data. Oznaczając tę klasę jako klasę data, kompilator automatycznie utworzy dla nas metody pobierające i ustawiające. Wygeneruje też funkcje equals(), hashCode() i toString().
Dodajmy słowo kluczowe data do klasy User:
data class User(var firstName: String, var lastName: String)W języku Kotlin, podobnie jak w języku Java, można mieć konstruktor podstawowy i co najmniej jeden konstruktor dodatkowy. W przykładzie powyżej jest to konstruktor podstawowy klasy User. Jeśli konwertujesz klasę Java, która ma wiele konstruktorów, konwerter automatycznie utworzy w Kotlinie wiele konstruktorów. Są one definiowane za pomocą słowa kluczowego constructor.
Jeśli chcemy utworzyć instancję tej klasy, możemy to zrobić w ten sposób:
val user1 = User("Jane", "Doe")Equality
Kotlin ma 2 rodzaje równości:
- Równość strukturalna używa operatora
==i wywołujeequals(), aby określić, czy 2 instancje są równe. - Równość referencyjna używa operatora
===i sprawdza, czy 2 odwołania wskazują ten sam obiekt.
Właściwości zdefiniowane w konstruktorze głównym 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 // falseW Kotlinie możemy przypisywać wartości domyślne do argumentów w wywołaniach funkcji. Jeśli argument zostanie pominięty, używana jest wartość domyślna. W Kotlinie konstruktory są też funkcjami, więc możemy użyć argumentów domyślnych, aby określić, że domyślna wartość lastName to null. W tym celu przypisujemy null do lastName.
data class User(var firstName: String?, var lastName: String? = null)
// usage
val jane = User("Jane") // same as User("Jane", null)
val joe = User("John", "Doe")Podczas wywoływania funkcji można nadawać nazwy parametrom funkcji:
val john = User(firstName = "John", lastName = "Doe") W innym przypadku załóżmy, że atrybut firstName ma wartość domyślną null, a atrybut lastName nie ma. W tym przypadku, ponieważ parametr domyślny poprzedza parametr bez wartości domyślnej, musisz wywołać funkcję z argumentami nazwanymi:
data class User(var firstName: String? = null, var lastName: String?)
// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")Zanim przejdziesz dalej, sprawdź, czy Twoja 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)
}
}Sprawdźmy, co zrobił automatyczny konwerter:
- Dodano blok
init(Repository.kt#L50) - Pole
staticjest teraz częścią blokucompanion object(Repository.kt#L33) - Lista
usersmoże mieć wartość null, ponieważ obiekt nie został utworzony w momencie deklaracji (Repository.kt#L7). - Metoda
getFormattedUserNames()jest teraz właściwością o nazwieformattedUserNames(Repository.kt#L11) - Iteracja po liście użytkowników (która początkowo była częścią
getFormattedUserNames() ma inną składnię niż w przypadku języka Java (Repository.kt#L15).
Zanim przejdziemy dalej, uporządkujmy nieco kod. Widzimy, że konwerter przekształcił naszą users listę w listę modyfikowalną, która zawiera obiekty dopuszczające wartość null. Lista może być pusta, ale załóżmy, że nie może zawierać użytkowników o wartości null. Wykonaj te czynności:
- Usuń
?wUser?w deklaracji typuusers. - Użytkownik
getUserspowinien zwrócićList<User>?
Automatyczny konwerter niepotrzebnie podzielił na 2 wiersze deklaracje zmiennych użytkownika i zmiennych zdefiniowanych w bloku init. Umieśćmy każdą deklarację zmiennej w osobnym wierszu. Oto jak powinien wyglądać nasz kod:
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)
}
}Blok inicjujący
W języku Kotlin główny konstruktor nie może zawierać żadnego kodu, więc kod inicjujący umieszcza się w blokach init. Funkcja działa tak samo.
class Repository private constructor() {
...
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users = ArrayList<Any?>()
users.add(user1)
users.add(user2)
users.add(user3)
}
}Większość init kodu odpowiada za inicjowanie właściwości. Można to też zrobić w deklaracji właściwości. Na przykład w wersji klasy Repository w języku Kotlin widzimy, że właściwość users została zainicjowana w deklaracji.
private var users: MutableList<User>? = nullstatic Właściwości i metody w języku Kotlin
W języku Java używamy słowa kluczowego static w przypadku pól lub funkcji, aby wskazać, że należą one do klasy, ale nie do jej instancji. Dlatego w naszej klasie Repository utworzyliśmy pole statyczne INSTANCE. Odpowiednikiem tego w języku Kotlin jest blok companion object. Tutaj należy też zadeklarować pola statyczne i funkcje statyczne. Konwerter utworzył i przeniósł tutaj pole INSTANCE.
Obsługa pojedynczych elementów
Potrzebujemy tylko jednej instancji klasy Repository, dlatego w języku Java użyliśmy wzorca singleton. W języku Kotlin możesz wymusić ten wzorzec na poziomie kompilatora, zastępując słowo kluczowe class słowem object.
Usuń prywatny konstruktor i obiekt towarzyszący, a definicję klasy zastąp wartością 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)
}
}W przypadku klasy object wywołujemy funkcje i właściwości bezpośrednio w obiekcie, np.:
val users = Repository.usersDestrukturyzacja
Kotlin umożliwia rozkładanie obiektu na kilka zmiennych za pomocą składni zwanej deklaracją rozkładającą. Tworzymy wiele zmiennych i możemy ich używać niezależnie od siebie.
Na przykład klasy danych obsługują dekompozycję, dzięki czemu możemy zdekomponować obiekt User w pętli for na (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)
}
Podczas konwertowania klasy Repository na Kotlin automatyczny konwerter ustawił listę użytkowników jako dopuszczającą wartość null, ponieważ nie została ona zainicjowana jako obiekt w momencie deklaracji. We wszystkich przypadkach użycia obiektu users używany jest operator potwierdzenia, że wartość nie jest zerowa – !!. Konwertuje dowolną zmienną na typ inny niż null i zgłasza wyjątek, jeśli wartość jest równa null. Korzystając z !!, ryzykujesz wystąpienie wyjątków w czasie działania programu.
Zamiast tego lepiej obsługiwać dopuszczalność wartości null za pomocą jednej z tych metod:
- Sprawdzanie wartości null (
if (users != null) {...}) - Używanie operatora elvis
?:(omówionego w dalszej części tego laboratorium) - używanie niektórych standardowych funkcji języka Kotlin (omówionych w dalszej części tego laboratorium);
W naszym przypadku wiemy, że lista użytkowników nie musi dopuszczać wartości null, ponieważ jest inicjowana natychmiast po utworzeniu obiektu, więc możemy bezpośrednio utworzyć instancję obiektu podczas jego deklarowania.
Podczas tworzenia instancji typów kolekcji Kotlin udostępnia kilka funkcji pomocniczych, które zwiększają czytelność i elastyczność kodu. W tym przypadku używamy MutableList dla users:
private var users: MutableList<User>? = nullAby uprościć kod, możemy użyć funkcji mutableListOf(), podać typ elementu listy, usunąć wywołanie konstruktora ArrayList z bloku init i usunąć jawną deklarację typu właściwości users.
private val users = mutableListOf<User>()Zmieniliśmy też var na val, ponieważ zmienna users będzie zawierać niezmienny odnośnik do listy użytkowników. Pamiętaj, że odwołanie jest niezmienne, ale sama lista jest zmienna (możesz dodawać i usuwać elementy).
Po wprowadzeniu tych zmian właściwość users nie jest już wartością null, więc możemy usunąć wszystkie niepotrzebne wystąpienia operatora !!.
val userNames: MutableList<String?> = ArrayList(users.size)for ((firstName, lastName) in users) {
...
}Poza tym zmienna users jest już zainicjowana, więc musimy usunąć inicjację 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)
}Ponieważ zarówno lastName, jak i firstName mogą mieć wartość null, podczas tworzenia listy sformatowanych nazw użytkowników musimy uwzględnić możliwość wystąpienia wartości null. Chcemy wyświetlać wartość "Unknown", jeśli brakuje którejkolwiek z nazw, więc możemy sprawić, że nazwa nie będzie miała wartości null, usuwając ? z deklaracji typu.
val name: StringJeśli lastName ma wartość null, name jest równe 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żna to zapisać w bardziej idiomatyczny sposób, używając operatora elvis ?:. Operator Elvisa zwraca wyrażenie po lewej stronie, jeśli nie ma wartości null, lub wyrażenie po prawej stronie, jeśli lewa strona ma wartość null.
W poniższym kodzie zwracana jest wartość user.firstName, jeśli nie jest ona wartością null. Jeśli user.firstName ma wartość null, wyrażenie zwraca wartość po prawej stronie , "Unknown":
if (lastName != null) {
...
} else {
name = firstName ?: "Unknown"
}Kotlin ułatwia pracę z String dzięki szablonom ciągów znaków. Szablony ciągów znaków umożliwiają odwoływanie się do zmiennych w deklaracjach ciągów znaków.
Automatyczny konwerter zaktualizował połączenie imienia i nazwiska, aby odwoływać się bezpośrednio do nazwy zmiennej w ciągu znaków za pomocą symbolu $ i umieścił wyrażenie między znakami { } .
// Java
name = user.getFirstName() + " " + user.getLastName();
// Kotlin
name = "${user.firstName} ${user.lastName}"W kodzie zastąp łączenie ciągów znaków tym kodem:
name = "$firstName $lastName"W języku Kotlin if, when, for i while to wyrażenia, które zwracają wartość. IDE wyświetla nawet ostrzeżenie, że przypisanie powinno zostać przeniesione poza if:

Zastosujmy się do sugestii IDE i wyłączmy przypisanie w przypadku obu instrukcji if. Zostanie przypisany ostatni wiersz instrukcji if. Dzięki temu wyraźniej widać, że jedynym celem tego bloku jest zainicjowanie wartości nazwy:
name = if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName
}
} else {
firstName ?: "Unknown"
}Następnie pojawi się ostrzeżenie, że deklarację name można połączyć z przypisaniem. Zastosujmy to również. Typ zmiennej name można wywnioskować, więc możemy usunąć jawną deklarację 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
}Przyjrzyjmy się bliżej formattedUserNames getterowi i zobaczmy, jak możemy go ulepszyć. Obecnie kod wykonuje te czynności:
- Tworzy nową listę ciągów znaków.
- Iteruje po liście 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 ułatwiają programowanie dzięki rozszerzeniu możliwości interfejsu Java Collections API. Jedną z nich jest funkcja map. Ta funkcja zwraca nową listę zawierającą wyniki zastosowania podanej funkcji przekształcania do każdego elementu na liście pierwotnej. Zamiast tworzyć nową listę i ręcznie przeglądać listę użytkowników, możemy użyć funkcji map i przenieść logikę, którą mieliśmy w pętli for, do treści map. Domyślnie nazwa bieżącego elementu listy używana w map to it, ale dla większej czytelności możesz zastąpić it własną nazwą zmiennej. W tym przykładzie nazwijmy go user:
val formattedUserNames: List<String>
get() {
return users.map { user ->
val name = if (user.lastName != null) {
if (user.firstName != null) {
"${user.firstName} ${user.lastName}"
} else {
user.lastName ?: "Unknown"
}
} else {
user.firstName ?: "Unknown"
}
name
}
}Aby jeszcze bardziej uprościć ten proces, 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"
}
}
}Zauważyliśmy, że automatyczny konwerter zastąpił funkcję getFormattedUserNames() właściwością o nazwie formattedUserNames, która ma niestandardowy getter. W tle Kotlin nadal generuje metodę getFormattedUserNames(), która zwraca List.
W języku Java właściwości klasy udostępniamy za pomocą funkcji getter i setter. Kotlin pozwala nam lepiej rozróżniać właściwości klasy, wyrażone za pomocą pól, oraz funkcje, czyli działania, które klasa może wykonywać, wyrażone za pomocą funkcji. W naszym przypadku klasa Repository jest bardzo prosta i nie wykonuje żadnych działań, więc zawiera tylko pola.
Logika, która była wywoływana w funkcji Java getFormattedUserNames(), jest teraz wywoływana podczas wywoływania funkcji pobierającej właściwość Kotlin formattedUserNames.
Chociaż nie mamy pola odpowiadającego właściwości formattedUserNames, Kotlin udostępnia nam automatyczne pole zapasowe o nazwie field , do którego w razie potrzeby możemy uzyskać dostęp z niestandardowych funkcji pobierających i ustawiających.
Czasami jednak potrzebujemy dodatkowych funkcji, których automatyczne pole zapasowe nie zapewnia. Przyjrzyjmy się przykładowi poniżej.
W klasie Repository mamy modyfikowalną listę użytkowników, która jest udostępniana w funkcji getUsers() wygenerowanej z naszego kodu w Javie:
fun getUsers(): List<User>? {
return users
}Problem polega na tym, że zwracając users każdy odbiorca klasy Repository może modyfikować naszą listę użytkowników, co nie jest dobrym pomysłem. Naprawmy to, używając usługi pomocniczej.
Najpierw zmieńmy nazwę users na _users. Teraz dodaj publiczną właściwość niezmienną, która zwraca listę użytkowników. Nazwijmy ją users:
private val _users = mutableListOf<User>()
val users: List<User>
get() = _usersPo tej zmianie prywatna usługa _users stanie się usługą pomocniczą dla publicznej usługi users. Poza klasą Repository lista _users nie jest modyfikowalna, ponieważ użytkownicy klasy mogą uzyskać do niej dostęp tylko za pomocą users.
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)
}
}Obecnie klasa Repository wie, jak obliczyć sformatowaną nazwę użytkownika dla obiektu User. Jeśli jednak chcemy ponownie użyć tej samej logiki formatowania w innych klasach, musimy ją skopiować i wkleić lub przenieść do klasy User.
Kotlin umożliwia deklarowanie funkcji i właściwości poza klasą, obiektem lub interfejsem. Na przykład funkcja mutableListOf(), której użyliśmy do utworzenia nowej instancji List, jest zdefiniowana bezpośrednio w Collections.kt w bibliotece standardowej.
W języku Java, gdy potrzebujesz jakiejś funkcji narzędziowej, najprawdopodobniej utworzysz klasę Util i zadeklarujesz tę funkcję jako funkcję statyczną. W Kotlinie możesz deklarować funkcje najwyższego poziomu bez konieczności tworzenia klasy. Kotlin umożliwia jednak również tworzenie funkcji rozszerzających. Są to funkcje, które rozszerzają określony typ, ale są zadeklarowane poza nim. Dlatego są one powiązane z tym typem.
Widoczność funkcji i właściwości rozszerzenia można ograniczyć za pomocą modyfikatorów widoczności. Ograniczają one użycie tylko do klas, które potrzebują rozszerzeń, i nie zaśmiecają przestrzeni nazw.
W przypadku klasy User możemy dodać funkcję rozszerzenia, która oblicza sformatowaną nazwę, lub przechowywać sformatowaną nazwę we właściwości rozszerzenia. Można go dodać poza klasą Repository w tym samym pliku:
// extension function
fun User.getFormattedName(): String {
return if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
}
// extension property
val User.userFormattedName: String
get() {
return if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
}
// usage:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedNameMoż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ą użytkownika, a nie funkcją klasy Repository, więc użyjemy właściwości rozszerzenia. Nasz plik Repository wygląda teraz tak:
val User.formattedName: String
get() {
return if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
}
object Repository {
private val _users = mutableListOf<User>()
val users: List<User>
get() = _users
val formattedUserNames: List<String>
get() {
return _users.map { user -> user.formattedName }
}
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
_users.add(user1)
_users.add(user2)
_users.add(user3)
}
}Biblioteka standardowa Kotlina używa funkcji rozszerzeń, aby rozszerzyć funkcjonalność kilku interfejsów API Javy. Wiele funkcji w Iterable i Collection jest zaimplementowanych jako funkcje rozszerzeń. Na przykład funkcja map, której użyliśmy w poprzednim kroku, jest funkcją rozszerzenia w Iterable.
W naszym kodzie klasy Repository dodajemy kilka obiektów użytkownika do listy _users. Dzięki funkcjom zakresu wywołania te mogą być bardziej idiomatyczne.
Aby wykonywać kod tylko w kontekście konkretnego obiektu bez konieczności uzyskiwania do niego dostępu na podstawie jego nazwy, w języku Kotlin utworzono 5 funkcji zakresu: let, apply, with, run i also. Krótkie i skuteczne funkcje mają odbiorcę (this), mogą mieć argument (it) i mogą zwracać wartość. Wybór zależy od tego, co chcesz osiągnąć.
Oto przydatna ściąga, która pomoże Ci to zapamiętać:

Ponieważ konfigurujemy obiekt _users w obiekcie Repository, możemy uprościć kod, używając funkcji apply:
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
_users.apply {
// this == _users
add(user1)
add(user2)
add(user3)
}
}W tym ćwiczeniu omówiliśmy podstawy, które pomogą Ci zacząć refaktoryzację kodu z Javy na Kotlin. Refaktoryzacja jest niezależna od platformy deweloperskiej i pomaga zapewnić, że pisany kod jest idiomatyczny.
Idiomatyczny Kotlin sprawia, że pisanie kodu jest krótkie i przyjemne. Dzięki wszystkim funkcjom, jakie oferuje Kotlin, istnieje wiele sposobów na zwiększenie bezpieczeństwa, zwięzłości i czytelności kodu. Możemy na przykład zoptymalizować naszą klasę Repository, tworząc instancję listy _users z użytkownikami bezpośrednio w deklaracji, co pozwoli nam pozbyć się bloku init:
private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))Omówiliśmy szeroki zakres tematów, od obsługi wartości null, singletonów, ciągów znaków i kolekcji po funkcje rozszerzające, funkcje najwyższego poziomu, właściwości i funkcje zakresu. Z 2 klas Javy przeszliśmy na 2 klasy Kotlin, które teraz wyglądają 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 podsumowanie funkcji Java i ich odpowiedników w Kotlinie:
Java | Kotlin |
|
|
|
|
|
|
Klasa, która zawiera tylko dane |
|
Inicjowanie w konstruktorze | Inicjowanie w bloku |
| pola i funkcje zadeklarowane w |
Klasa singleton |
|
Więcej informacji o Kotlinie i sposobie korzystania z niego na Twojej platformie znajdziesz w tych materiałach:
- Kotlin Koans
- Samouczki dotyczące języka Kotlin
- Tworzenie aplikacji na Androida w Kotlinie – bezpłatny kurs
- Kotlin Bootcamp for Programmers
- Kotlin dla programistów Java – bezpłatny kurs w trybie audytu