Kotlin — dalsze podstawy
W poprzednim artykule przedstawiono podstawy języka Kotlin. Niniejszy z kolei ma na celu zaprezentowanie pozostałych, równie kluczowych cech tego języka.
Dziedziczenie
Klasy i metody w Kotlinie nie dają domyślnie możliwości ich rozszerzenia. Traktowane są jako final. Aby umożliwić dziedziczenie należy wyraźnie oznaczyć klasę/metodę słowem open. Z kolei nadpisującą metodę należy zaznaczyć przez override inaczej środowisko poinformuje nas o błędzie.
open class Currency {
open fun toPln() = 0f
}
class Usd : Currency() {
override fun toPln(): Float {
return 3.75f
}
}
Interfejsy
Słowo open w interfejsie można pominąć natomiast override nadal jest wymagane. Podczas implementacji kilku interfejsów należy je oddzielić przecinkiem. Analogiczny przykład z wykorzystaniem interfejsu:
interface Currency {
fun toPln(): Float
}
class Usd : Currency {
override fun toPln() = 3.75f
}
Rzutowanie
W Kotlinie rzutuje się typy za pomocą as, a sprawdza je przy użyciu is. Rzutowanie poprzez as samo w sobie nie jest bezpieczne dlatego istnieje safe cast, który zapisuje się jako as?. W przypadku gdy typ nie jest kompatybilny zwracany jest null, co przy zwykłym rzutowaniu kończy się na exception. Zmienna zweryfikowana z wykorzystaniem is zostaje automatycznie traktowana jako dany typ w dalszej części kodu dzięki smart castom. Poniżej zastosowanie wymienionych rozwiązań.
val text...val a: String = text as String // unsafe cast - możliwe exception
var length = a.lengthval b: String? = text as? String // safe cast - możliwy null
length = b?.lengthif (text is String) { // smart cast
length = text.length // text jest traktowany jako String
}
length = text.length // błąd, text nie jest tutaj typu String
Tworzenie obiektów
W Kotlinie nie wykorzystuje się słowa new do utworzenia instancji klasy. Tworzenie obiektu wygląda jak w przykładzie poniżej:
val a = Object()
Konstruktory
Klasy w Kotlinie muszą posiadać jeden główny konstruktor. Jeśli nie jest on podany to wykorzystywany zostaje domyślny, pusty konstruktor.
class Ball() {} // Ball() i Ball są równoznaczne
W konstruktorze głównym parametry podaje się w następujący sposób:
class Ball(color: Int) {
val color = Color.valueOf(color);
}
Kotlin dodatkowo umożliwia deklarację pól w konstruktorze. Jeśli podane w konstruktorze głównym zostaną wartości oznaczone jako val i var wtedy staną się one atrybutami tej klasy. Dla przykładu:
class User(val name: String, var age: Int = 18) {}...val user = User("Adam Nowak")
user.age = 21
user.name = "Jan Nowak" // błąd, name jest tylko do odczytu (val)
Dodatkowo poza głównym konstruktorem może zostać podanych wiele podrzędnych konstruktorów, które muszą wywołać główny. W tych konstruktorach nie ma już możliwości wykorzystania wartości val i var.
class User(val name: String, var age: Int = 18) { constructor(name: String, age: Int, height: Int)
: this(name, age) {
validateHeight(height)
} constructor(name: String, age: Int, height: Float)
: this(name, age) {
validateHeight((height * 100f).toInt())
}
...
Typy klas
data class
Typ stworzony głównie do przechowywania danych lub modeli encji. Kontruktor główny musi posiadać co najmniej jeden parametr. Dodatkowo każdy z parametrów ma być oznaczony jako val lub var. Nie ma możliwości nadpisania data class. Metody takie jak equals(), hashCode(), toString() i copy() są domyślnie zaimplementowane na podstawie parametrów konstruktora głównego.
data class User(val name: String, var age: Int, var height: Int)
object
Typ służący jako singleton, czyli zezwalający na utworzenie tylko jednego obiektu danej klasy. Instancja klasy jest tworzona w sposób bezpieczny wielowątkowo co uniemożliwia przypadkowe utworzenie się kolejnych. Nie ma jednak możliwości zdefiniowania jakiegokolwiek konstruktora. Do instancji odwoływać się trzeba za pomocą nazwy klasy.
object AlertManager {
fun showMessage(message: String) {
println(message)
}
}...AlertManager.showMessage("Alert!")
// Java - AlertManager.INSTANCE.showMessage("Alert");
companion object
Pozwala na deklarowanie statycznych zmiennych i metod klas, wewnątrz których się znajdują. Poniżej przykład użycia:
class Settings {
companion object {
val DEFAULT_AUDIO_ENABLED = true
}
}...val enabledByDefault = Settings.DEFAULT_AUDIO_ENABLED
nested
Zagnieżdzona klasa statyczna. Działaniem przypomina te z podobnych języków. Instancja tej klasy jest niezależna od klasy, w której się znajduje.
class Calculator() {
class Multiplier {
fun multiply(a: Float, b: Float) = a * b
}
}...val multiplier = Calculator.Multiplier()val result = multiplier.multiply(10f, 15f)
inner class
Klasa wewnętrzna. Analogiczna do tych z podobnych języków. Instancję tej klasy tworzy się na podstawie obiektu klasy zewnętrznej, do którego dostaje ona dostęp. Daje to jej możliwość wykorzystania atrybutów i metod obiektu zewnętrznej klasy.
class Calculator(val value: Int) {
inner class Multiplier {
fun multiply(a: Float) = value * a
}
}...val calculator = Calculator(10)
val multiplier = calculator.Multiplier()val result = multiplier.multiply(15f)
enum class
Wyliczeniowy typ z określonymi wartościami. Działaniem analogiczny do tego z podobnych języków.
enum class Status { SUCCESS, ERROR }
Wyrażenia standardowe
when
Działaniem podobny do switch. Z wyjątkiem tego, że zwraca wartość, a parametr jest opcjonalny. Przekazując parametr można wykorzystać when jako wyrażenie. Typ zwracany określany jest na podstawie wszystkich gałęzi when. W przykładzie poniżej jest nim Int:
fun printLength(a: Any) {
when {
a is String -> println(a.length)
else -> println(0)
}
}fun getLength(a: Any) = when(a) { // Int
is String -> a.length
else -> 0
}
let/also
Wyrażenia let i also pozwalają na operacje na obiektach z czego let zwraca wartość ostatniej instrukcji, a also zwraca obiekt na którym zostało wywołane wyrażenie. Umożliwia to wiązanie wielu instrukcji. Odniesienie się do obiektu w bloku wykonuje się poprzez it.
var s = "text"s = s.let { // let zwraca wartość ostatniej instrukcji, tutaj "xt"
println(it) // "text"
it.substring(2)
}
s = s.also { // also zwraca input - tutaj "xt"
println(it.toUpperCase()) // "XT"
}
with/apply
Wyrażenia with i apply są analogiczne do poprzednich. Różnica polega na tym, że metody w bloku wywoływane są na przekazywanym obiekcie. Do obiektu można odwołać się poprzez this.
var s = "text"s = with(s) { // zwraca "xt"
println(this) // "text"
substring(2)
}
s = s.apply { // zwraca "xt"
println(toUpperCase()) // "XT"
}
run/repeat
Wyrażenia run i repeat pozwalają na wykonanie bloku instrukcji jednorazowo w przypadku run i wielokrotnie w przypadku repeat. Dodatkowo run ma możliwość zwrócenia wartości poprzez podanie wyniku w ostatniej instrukcji.
val a = run { // zwraca 15
println(-1) // "-1"
15
}
repeat(5) {
println(it) // "0", "1", "2", "3", "4"
}
takeIf/takeUnless
Wyrażenia takeIf i takeUnless przyjmują metodę zwracającą Boolean. W zależności od jej wyniku zwracany zostaje obiekt, na którym zostały wywołane te wyrażenia lub null. Obiekt zwrócony zostaje dla true w przypadku takeIf lub dla false w razie takeUnless. W przeciwnym razie zwracany jest null.
var s = "text"val a: String? = s.takeIf { s.length > 2 } // "text"
val b: String? = s.takeUnless { s.length <= 2 } // "text"
val c: String? = s.takeIf { s.length <= 2 } // null
Widoczność
Kotlin zawiera 4 modyfikatory widoczności: private, protected, internal i public. Zmienne i klasy domyślnie traktowane są jako public.
private — widoczność tylko w danej klasie,
protected — widoczność w klasie i w jej podklasach,
internal — widoczność tylko w obrębie modułu,
public — pełna widoczność.
Getter/Setter
Podczas deklarowania zmiennych można podać własne settery i gettery. Zapisuje się je na końcu deklaracji zmiennej jako metody set() i get(). Każda z metod jest opcjonalna. Istnieje zatem możliwość nadpisania obydwu. Typ zmiennej również tutaj może wynikać z kontekstu.
val a get() = 15
var b: String = "0: default"
set(value) { field = "${Date()}: $value" }
Lazy loading
Kotlin pozwala na prostą deklarację lazy loadingu. Wartość zmiennej obliczana jest wtedy dopiero przy pierwszej próbie użycia. Każdy kolejny dostęp do zmiennej wykorzystuje obliczoną już wartość.
val a by lazy {
println("lazy")
2 + 2
}...println("a1: $a") // wyświetli kolejno "lazy" i "a1: 4"
println("a2: $a") // wyświetli jedynie "a2: 4"
Reactive programming
Kotlin zawiera funkcje do modyfikacji kolekcji znane przykładowo z RxJava. Do wykorzystania istnieją metody takie jak filter, map, flatMap, distinct czy zip. Poniżej przedstawiono przykłady ich wykorzystania:
val array = arrayOf(1, 2, 3, 4, 5, 6, 7, 8)val even = array.filter { it % 2 == 0 } // [2, 4, 6, 8]
val negative = array.map { it * -1 }
// [-1, -2, -3, -4, -5, -6, -7, -8]val oddNegative = array.filter { it % 2 != 0 }.map { it * -1 }
// [-1, -3, -5, -7]val duplicated = arrayOf(1, 1, 1, 2, 3, 3, 4, 5, 5, 5)
val distinct = duplicated.distinct() // [1, 2, 3, 4, 5]
Podsumowanie
Powyższy materiał uzupełnia kluczowe podstawy języka Kotlin zaprezentowane w poprzednim artykule. Bardziej zaawansowane treści zostały pominięte ze względu na węższe zastosowanie. Niewykluczone jednak, że zostaną zaprezentowane w kolejnym artykule.