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).
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
).
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.
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.
Aby zwiększyć czytelność zapisu zmiennych numerycznych można wykorzystać znak underscore
.
Typy liczb całkowitych mogą być unsigned
. Deklaracja typu i przypisywanie wartości przedstawia się następująco.
W Kotlin nie możliwa jest niejawna konwersja niższych typów do wyższych. W takim przypadku należy wykorzystać jawną konwersje.
Znakowe
W przeciwieństwie do Java typ znakowy Char
nie może być traktowany bezpośrednio jako liczba.
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.
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.
Aby wpisać znak nowej linii można wykorzystać znak specjalny lub tzw, raw strings
(zaczyna się i kończy potrójnym cudzysłowiem """
).
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.
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.
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ą).
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
).
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).
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.
Enum
Deklaracja typu wyliczeniowego odbywa się za pomocą słowa kluczowego enum
, a każda stała enum jest obiektem.
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.
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.
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
.
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.
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.