Kotlin — dalsze podstawy

Adrian Tworkowski
akra-polska
Published in
6 min readJan 11, 2019

--

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.length
val b: String? = text as? String // safe cast - możliwy null
length = b?.length
if (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.

--

--