Typy

Kotlin

  |   16 min czytania

Obiektowość

W Kotlin wszystko jest obiektem, innymi słowy możliwe jest wywołanie funkcji oraz dostęp do pól dla dowolnej zmiennej. Klasy prymitów tzn. numeryczne, logiczne czy tekstowe nie są tu wyjątkiem - mogą wyglądać jak prymitywy jednak są klasami. W związku z tym zmienne opakowanych prymitywów (Int, Float, Double, String, Boolean itp) nie posiadają wartości domyślnej tak jak ma to miejsce w Java. Każda klasa w Kotlin dziedziczy klasę Any (podobnie jak w klasa Object w Java).

Zmienne i stałe

Właściwości w trybie tylko do odczytu (read only) deklarowane są słowem kluczowym val, zmienne (mutable) słowem kluczowym var, natomiast stałe oznaczane są jako const (muszą być typem prymitywnym oraz zadeklarowane na najwyższym poziomie struktury pliku lub jako właściwość klasy). Właściwości mogą zostać zdefiniowane poprzez wskazanie typu oraz przypisanie wartości lub tylko przypisując wartość (wówczas zachodzi wnioskowanie typu na podstawie wartości).

var variable: Int = 1 //assignment Int
var variableInferred = 2 //type is inferred
val variableDeferred: Int //definition as Int
variableDeferred = 3 //deferred assignment - must be Int
var someObject = SomeClass(1, "Name") //instance of class - no 'new' keyword

//top-level or member class declaration
const val constant: Float = 3.14f

Deklaracja następuje poprzez zdefiniowanie typu. Warto zauważyć, że ze względu na brak przyjmowania domyślnej wartości przez tak zwane typy proste (które są klasami), jeśli są używane musi zostać przypisana do nich wartość (może być null).

var a: String
println(a) //compiler error - variable must be initialized
var b //compiler error - variable must have a type or be initialized

Zmienne oznaczone jako lateinit var umożliwiają pominięcie przypisania wartości zmiennej (dla typów nie prymitywnych) w momencie definicji w celu opóźnienia inicjalizacji np. przez wstrzykiwanie zależności. Dzięki czemu zmienna nie musi być oznaczona jako nullable co pozwala na pominięcie sprawdzania null w kodzie. Jednakże odwołanie się do takiej zmiennej, która nie została zainicjalizowana skutkuje wyrzuceniem wyjątku.

lateinit var obj: SomeClass //type must be declared
print(obj) //throws UninitializedPropertyAccessException

obj = SomeClass() //setup obj from external source
print(obj) //now it's okay

Numeryczne

Podobnie jak w większości języków Kotlin dostarcza podstawowe typy numeryczne tj: Int, Double, Long, Float, Short, Byte. Do ich zapisu służą odpowiednie literały.

var decimals = 100 //decimals (Int, Short, Byte)
var binary = 0b1100100 //binary
var hexadecimal = 0x64 //hexadecimal
var longNumber = 100L //Long
var doubleNumber = 10.0 //Double
var doubleScientific = 10.0e5 //Double in scientific notation
var floatNumber = 100F //Float

Aby zwiększyć czytelność zapisu zmiennych numerycznych można wykorzystać znak underscore.

var million = 1_000_000
var rgb = 0xFF_00_55

Typy liczb całkowitych mogą być unsigned. Deklaracja typu i przypisywanie wartości przedstawia się następująco.

var a = 1u
var b: UInt = 1u

W Kotlin nie możliwa jest niejawna konwersja niższych typów do wyższych. W takim przypadku należy wykorzystać jawną konwersje.

var shortNumber: Short = 1
var intNumber: Int = shortNumber.toInt() //valid cast
intNumber = shortNumber //invalid cast - compile error

var longNumber = 1L + intNumber //Long + Int -> Long
var longNumber2 = intNumber + 1L //Int + Long -> Long
var validInt = longNumber.toInt() + longNumber2.toInt() //valid cast
var invalidInt = longNumber.toInt() + longNumber2 //invalid cast - compile error

Znakowe

W przeciwieństwie do Java typ znakowy Char nie może być traktowany bezpośrednio jako liczba.

var character = 'a'
character = '1'
var tab = '\t' //special tab character
var special = '\uAABB' //special character preceded by \u

Logiczne

Typ logiczny Boolean przyjmuje jedną z dwóch wartości true lub false, a w wyrażeniach logicznych można wykorzystać operatory koniukcji ||, alternatywy && oraz negacji !. W przeciwieństwie do Java nie można do niego przypisać wartości numerycznej.

var bool: Boolean
bool = true
bool = 1 //compiler error
var result = bool && false //false

Tekstowe

Typ tekstowy jest reprezentowany przez klasę String, której instancje są niezmienne. Obiekt typu String składa się z elementów znakowych do których dostęp może być uzyskany poprzez odwołanie do indeksu tablicy.

var text = "Hello"
var letter = text[0] //this is H

Aby wpisać znak nowej linii można wykorzystać znak specjalny lub tzw, raw strings (zaczyna się i kończy potrójnym cudzysłowiem """).

var escaped = "Some line\nAnother line"
var raw = """
    Some line
    Another line
"""

Konkatenacja odbywa się za pomocą operatora + i zachodzi ona także dla połączenia zmiennych tekstowych ze zmiennymi innego typu (konkatenacja z innymi typami wymaga, aby pierwszy człon był typu String). W większości przypadków lepszym rozwiązaniem będzie jednak użycie string templates, który pozwala na użycie wartości zmiennych w tekście.

var text = "text"
var concat = text + 1 //result text1
concat = 1 + text //compiler error

var number = 5
var template = "number = $number and text variable length = ${text.length}"

Tablice

Tablice są reprezentowane przez klasę Array. Dostęp do elementów odbywa się przy pomocy indeksów tablicy lub metod get oraz set. Kotlin udostępnia również dedykowane tablicę dla typów prostych i są to m.in. IntArray, ShortArray, DoubleArray itp.

var array = arrayOf(1, 2, 3)
var arrayInt: IntArray = intArrayOf(1, 2, 3)
var element = array[1]
element = arrayInt.get(1)

Operatory

Sprawdzanie równości obiektów w Kotlin odbywa się za pomocą następujących operatorów: równości strukturalnej == (negacja !=) lub metody equals, równości referencyjnej === (negacja !==), natomiast porównania zachodzą przy wykorzystaniu <, >, <=, =>. Równość strukturalna sprawdza czy wartości pól obiektów są równe, a równość referencyjna sprawdza czy obiekty wskazują na ten sam adres (dla prymitywów jest to równoważne z równością strukturalną).

var a = 1
var b = 2
var result = a == b //false
result = a.equals(b) //false - translated as a == b
result = a != b //true
result = a < b //true
result = a === b //false - translated into a == b

var rect1 = Rectangle(1, 2)
var rect2 = Rectangle(1, 2)
result = rect1 === rect2 //false
rect1 = rect2
result = rec1 === rec2 //true

Sprawdzanie typu i rzutowanie

Operator is oraz !is w negacji pozwalają sprawdzić czy zmienna jest (lub nie) danego typu. Jeśli zmienna jest instancją sprawdzanego typu wówczas w ciele spełnionego warunku zmienna ta jest automatycznie rzutowana do typu sprawdzanego (smart cast).

if(a is String) {
    var length = a.length //a is treated as String here
}

Co więcej, jeśli w liście warunków warunkiem poprzedzającym jest sprawdzanie typu, wówczas w kolejnych warunkach zmienna jest także automatycznie rzutowana do sprawdzanego typu (przy zachowaniu poprawności wyrażeń logicznych).

if(a is String && a.length > 0) { 
    //do something with not empty String
}

if(a !is String || a.length == 0) {
    //do something with no String or empty String
}

Rzutowanie można wykonać ręcznie wywołując operator as. Jednakże w przypadku niemożliwości wykonania rzutowania operacja ta wywoła wyjątek ClassCastException (unsafe cast). Aby zapobiec takiemu przypadkowi należy skorzystać z bezpiecznego operatora rzutowania as? (safe nullable cast), które w sytuacji niepowodzenia przypisze wartość null.

var text = "text"
var number = 1
var a: String = text as String //cast correct
var b: Int = text as Int //ClassCastException exception thrown
var c: Int? = text as? Int //cast unsuccess - c is null but no exception throwed

Enum

Deklaracja typu wyliczeniowego odbywa się za pomocą słowa kluczowego enum, a każda stała enum jest obiektem.

enum class Size {
    S, M, L, XL
}

enum class Color(val number: Int) {
    S(1), M(2), L(3), XL(4)
}

var size = Size.M
var color = Color.RED
print(color) //RED
print(color.number) //1
print(Color.GREEN.number) //2

Typy generyczne

Typy generyczne (generics) pozwalają na opóźnienie w dostarczeniu specyfikacji typów dla budowanych klas do momentu ich wywyołania w trakcie działania programu. Umożliwiają więc wykorzystanie klas i metod dla pewnej rodziny typów ograniczonych przez zadany typ generyczny T, dzięki czemu nie trzeba pisać wielu funkcji dla parametrów różnego typu. Domyślnie typ generyczny jest niezmienny i zapisuje się go w nawiasach kątowych <> po nazwie klasy do której się odnosi lub przed nazwą w deklaracji funkcji.

class Invariant<T> //class definition
var genString: Invariant<String> = Invariant<String>() //ok types are equals
var genInt: Invariant<Int> = Invariant<Long>() //compiler error - Invariant<Int> expected instead of Invariant<Long>

fun <T> genericFunction(x: T) {} //function definition
var a = genericFunction<String>("generic") //execution

W kontekście typów generycznych należy wspomnieć o pojęciu producenta (producer) oraz konsumenta (consumer). Obiekt producenta pozwala na odczytywanie obiektu zadanego typu T, a obiekt konsumenta na zapisywanie do niego. Instrukcja wariancji pozwalają na użycie typów generycznych w sposób zmienny. Modyfikator out sprawia, że klasa Covariant staje się kowariantna i jest producentem dla typu T (ale nie jest konsumentem) co pozwala na wykorzystanie deklaracji typu generycznego przez klasy bazowe, natomiast modyfikator in sprawia, że klasa Contravariant staje się kontrawariantna i jest konsumentem dla typu T (ale nie jest producentem) dzięki czemu deklaracja typu generycznego może zostać wykorzystana przez typy pochodne.

class Covariant<out T>
class Contravariant<in T>	

var covA: Covariant<Number> = Covariant<Int>() //ok, Number is super class for Int
var covB: Covariant<Int> = Covariant<Number> //compiler error - type mismatch 
var contraA: Covariant<Number> = Covariant<Int>() //compiler error - type mismatch 	
var contraB: Generic<Int> = Generic<Number> //ok, Int is subtype of Number class

Kolekcje

Przykładem użycia typów generycznych są kolekcje (Collections). Kotlin wyróżnia kolekcje zmienne (immutable) i niezmienne (mutable), które są kowariantne, dzięki czemu eliminuje błędy z ich użyciem. Do typów kolekcji zaliczamy listy (List), zbiory (Set) oraz mapy (Map), a w przypadku zmiennych kolekcji są to odpowiednio MutableList, MutableSet oraz MutableMap. Kotlin nie dostarcza dedykowanych konstruktorów dla kolekcji. Aby je zainicializować należy wykorzystać metody ze standardowej biblioteki takie jak listOf, mutableListOf, setOf, mutableSetOf czy mapOf.

//empty
var emptyList: List<Int> = emptyList()
var emptySet = setOf<Int>() //type paremeter inferred from context

//immutable
var immutableList: List<Int> = listOf(1,2,3)
var immutableSet: Set<Int> = setOf(4,5,6)
var immutableMap: Map<Int, String>  = mapOf(1 to "a", 2 to "b", 3 to "c")

//mutable
var mutableList: MutableList<Int> = mutableListOf(3,2,1)
var mutableSet: MutableSet<Int> = mutableSetOf(6,5,4)
var mutableMap: MutableMap<Int, String> = mutableMapOf(3 to "c", 2 to "b", 1 to "c")

//cast
var immutableFromMutable: List<Int> = mutableList //ok by 
var mutableFromImmutable: MutableList<Int> = list.toMutableList() //must be directly casted by method

//covariant
var covNumbers: List<Number> = immutableList //ok because Number is supertype for Int
var covIntegers: List<Int> = covNumbers //compiler error - Int is not supertype of Number
var covMutableIntegers: MutableList<Int> = immutableList //compiler error - List<Int> is expected

Kolekcje dostarczają zestaw wielu gotowych podstawowych funkcji takich jak m.in. get, first, last, copy, containts, filter, groupBy, slice itp. Implementacja kolekcji zmiennych pozwala na modyfikowanie struktury kolekcji za pomocą metod m.in. set, add, remove, clear, sort, shuffle, reverse itp.

immutableList.first() //1
immutableList.get(1) //2
immutableList.slice(0..1) //[1,2]
immutableList.filter { it % 2 == 0} //[1, 3]

println(mutableList) //[3,2,1]
mutableList.add(4) //[3,2,1,4]
mutableList.set(1, 5) //[3,5,1,4]
mutableList.removeAt(2) //[3,5,4]
mutableList.remove(4) //[3,5]
mutableList.clear() //[]

Alias

Kotlin dostarcza mechanizmu aliasów dla istniejącego typów co deklaruje się słowem kluczowym typealias. Ma on przede wszystkim zastosowanie w typach generycznych lub tam gdzie nazwa typu jest zbyt długa.

typealias ShortName = VeryLongNameOfSomeCustomClass
var someObject: ShortName = ShortName()
var result = someObject is VeryLongNameOfSomeCustomClass //true