Klasy

Kotlin

  |   17 min czytania
(Kotlin v1.3)

Deklaracja

Klasy w Kotlin definiuje się za pomocą słowa kluczowego class oraz nazwy. Deklaracja może zawierać także ciało klasy w klamrach oraz nagłówek (podstawowy konstruktor wraz z parametrami). W skład ciała klasy mogą wchodzić: konstruktory, bloki inicjalizujące, funkcje, właściwości, klasy zagnieżdzone i wewnętrzne oraz wyrażenia i deklaracje obiektu. Słowo kluczowe this podobnie jak w innych językach programowania wskazuje obiekt odbiorcy na rzecz którego następuje odwołanie do jego elementów. Może więc zatem odnosić się do np. konstruktora czy właściwości. W celu uzyskania referencji do klasy należy wykorzystać mechanizm refleksji.

class Name {

    //some variables
    var a;
    val b;

    fun someFunction(a: String) {
        print(a) //parameter of function
        print(this.a) //property of class
    }

    //some more body code
}

class Empty //if class has no body than curly braces are no needed

val classReference = Empty::class //reflection - classReference is instance of KClass in this case KClass<Empty>

Konstruktor

Klasy mogą posiadać konstruktor podstawowy (primary constructor) oraz konstruktory dodatkowe (secondary constructor). Konstruktor podstawowy jest częścią nagłówka klasy i definiowany jest słowem kluczowym constructor zaraz po nazwie klasy.

//primary constructor can have some parameters
class Primary constructor(text: String) { }

//constructor keyword can be omitted if doesn't have any adnotations or visibility modifiers
class Primary(text: String) { }

//constructor parameters can be also mutable or read-only
class Primary(var text: String, val amount: Int) { }

//constructor keyword can't be ommited because of adnotations or visibility modifiers
class Primary public @Inject constructor(text: String) { }

Konstruktor podstawowy nie posiada żadnego własnego bloku kodu. Zamiast tego inicjalizacja może mieć miejsce w blokach inicjalizujących (initializer block), które definiuje się słowem kluczowym init. Parametry konstruktora podstawowego mogą być wykorzystane również w przypisywaniu wartości zmiennych poza blokiem inicjalizującym.

class Student(name: String, surname: String) {

    var name = name
    var surname = surname.toUpperCase()

    init {
        //do some init staff
        print("Student ${name} ${surname} has been created")
    }
}

Konstruktory dodatkowe definiowane są w ciele klasy również za pomocą słowa kluczowego constructor, opcjonalnych argumentów oraz ciała. Jeśli klasa ma konstruktor podstawowy wówczas każdy konstruktor dodatkowy musi odnosić się do konstruktora podstawowego. Delegacja do innego konstruktora tej samej klasy odbywa się przy pomocy słowa kluczowego this następującego po definicji argumentów konstruktora.

//class has only secondary constructor
class Secondary {
    constructor(text: String) { 
        // some code 
    }
}

//class has primary and secondary constructor
class PrimarySecondary(text: String) {
    var text: String = text;
    var amount: Int
    
    init {
        //This block executes before every secondardy constructor and can be used for example by primary constructor job
        amount = 0
    }
    constructor(text: String, amount: Int) : this(text) {
        //Secondardy constructor body
        this.amount = amount
     }
    //more secondary constructors could be here
}

//objects can be created only be declared constructors
var a = PrimarySecondary("text") //okay
var b = PrimarySecondary("text", 10) //okay
var c = PrimarySecondary() //compiler error

Jeśli klasa nie posiada żadnego konstruktora podstawowego lub dodatkowego wówczas generowany jest domyślny podstawowy konstruktor bez argumentów z widocznością public.

class NoConstructor {
    //some body
}

//use default primary constructor to create an instance
var obj = NoConstructor()

Refleksja pozwala również na uzyskanie referencji do konstruktora dzięki czemu możliwe jest przekazanie go do funkcji jako parametr.

fun constructorReference(x: () -> NoConstructor) {
    val obj: NoConstructor = x()
}
constructorReference(::NoConstructor)

Właściwości i pola

Klasy w Kotlin w przeciwieństwie do Java nie zawierają pól tylko właściwości, które składają się z pola oraz metod dostępowych.

//in Kotlin property is both field and accessors 
var name: String = "William"

//equivalent in Java
String name = "William"
public String getName() {
    return name;
}
public void setName(String name) {
    this.name = name;
}

Właściwości klasy mogą być deklarowane jako typy zmienne (var) lub tylko do odczytu (val). Co więcej jeśli argumenty konstruktora są zadeklarowane jako var lub val wówczas stają się automatycznie właściwościami klasy.

class Weather(val city: String, var temperature: Int) {} 

//equivalent of
class Weather(city: String, temperature: Int) {
    val city: String = city
    var temperature: Int = temperature
}

Ciało deklaracji poza nazwą, typem oraz inicjalizacją może definiować metody dostępowe get oraz set. Jeśli ciało deklaracji właściwości nie posiada akcesorów get lub set wówczas przyjmuje implementacje domyślną (get dla stałych oraz get i set dla zmiennych). Aby właściwość mogła uzyskać dostęp do swojego pola należy użyć w metodzie dostępowej identyfikatora field. Dostęp do właściwości instancji klasy odbywa się za pomocą kropki (jeśli modyfikator dostępu na to pozwala).

class Person(name: String, age: Int) {

    var name: String = name
        get() = field.capitalize()
        set(value) {
            field = "Mr. " + value
        }
    
    var age: Int = age
        private set

    fun changeAge(age: Int) {
        if(this.age < age) {
            this.age = age
        }
    }
}

var person = Person("william", 15)
print("${person.name} is ${person.age} years old") //William is 15 years old
person.name = "Jim"
person.age = 20 //compiler error - setter is private so property can not be directly changed
person.changeAge(20) //change property by specific method
print("${person.name} is ${person.age} years old") Mr. Jim is 20 years old

var person = Person("William", 15)
print("${person.name} is ${person.age} years old")
person.name = 20 //compiler error - the setter is private so property can not be directly changed
person.changeName("Jim") //name property can be changed like this

Dostęp do akcesorów właściwości można uzyskać poprzez referencje dzięki refleksji.

var name = person::name //it's okay
name = person.name.get() //compiler error

Klasy danych

Kotlin umożliwia stworzenie klasy oznaczonej jako data której przeznaczeniem jest trzymanie danych. Dla wszystkich właściwości klasy zadeklarowanych w konstruktorze podstawowym automatycznie tworzone są domyślne metody equals, hashCode, toString, copy oraz metody dostępowe co eliminuje boilerplate w stosunku do odpowiednika w Java. Data class musi posiadać konstruktor podstawowy z argumentami oznaczonymi jako val lub var. Data class może implementować interfejsy lub dziedziczyć po klasie jednak nie może być oznaczone jako abstract, open, sealed czy inner.

data class Product(val name: String) {
    var price: Float = 0f
}

var product1 = Product("Jack Daniels")
product1.price = 10f //note that price isn't a part of autogenereated equals, hashCode, toString and copy methods
print(product1.toString()) //Product(name=Jack Daniels)
var product2 = product1.copy()
print(product1.equals(product2)) //true - despite the different prices

Klasy zagdnieżdzone i wewnętrzne

Deklaracja klasy może odbywać się w ciele inne klasy. Taka klasa nazywa się klasą zagnieżdzoną (nested class). Dodatkowo klasa zagnieżdzona może być oznaczona słowem kluczowym inner co sprawia, że staje się klasą wewnętrzną (inner class) z dostępem do elementów klasy zewnętrznej (odwołanie może nastąpić z użyciem instrukcji super@KlasaZewnętrzna). Klasa wewnętrzna trzyma referencję do obiektu klasy zewnętrznej.

class Outer {
    var amount: Int = 5
    val ratio: Int = 2
    
    class Nested {
        //nested class doesn't know about Outer class
        fun work() = 20
    }

    inner class Inner {
        //inner class knows about Outer class
        fun work(): Int {
            amount = super@Outer.ratio * amount
            return amount
        }
    }
}

var outerObj = Outer()
var nestedObj = Outer.Nested()
var innerObj = outer.Inner()
print(outerObj.amount) //5
print(nestedObj.work()) //20
print(innerObj.work()) //10
print(outerObj.amount) //10

Klasy zapięczetowane

Klasa zapięczętowana (sealed class) pozwala na reprezentowanie ograniczonej hierarchii klas, której instancja może przyjmować jeden z typów skończonego zbioru (klasa ma ustaloną ilość podklas). Koncept sealed class jest podobny do enum class jednakże klasa oznaczona jako sealed może mieć kilka instancji tej samej podklasy (dla enum jest to jeden obiekt) co pozwala obiektom na przechowywanie stanu. Klasy sealed są dobrym narzędziem we współpracy z wyrażeniem when.

sealed class Status {
    data class Content(val text: String) : Status() {
        //some body
    }
    object None : Status() {
        //some body
    }
    //some members of Status class
}
class Error(code: Int) : Status() {
    //some body
}
//subclasses can be declared outside the sealed class but must be in the same file as sealed class

//get status from network event
val status: Status = Status.Content("content of the page")
when(status) {
    is Status.Content -> print(status.text)
    is Status.None -> print("No content to show")
    is Error -> print("Unexpected error")
}

Klasy inline

Klasy oznaczone jako inline umożliwiają opakowanie typu prymitywnego (wrapper) bez utraty wydajności w stosunku do klas opakowań prymitywów, które nie są inline. W czasie działania obiekt klasy inline będzie kompilowany do typu bazowego i reprezentowany jako pojedyncza właściwość. Deklaracja klasy inline wymaga konstruktora podstawowego z jednym argumentem, zezwala na definiowanie właściwości i funkcji oraz implementowanie interfejsów. Nie może jednak rozszerzać innych klas i być rozszerzalną oraz nie może zawierać bloków init, klasy wewnętrznej (inner class) czy pól (backing fields).

 
inline class InlineClass(val text: String) : Showable {
    val length: Int
        get() = text.length

    override fun show() {
        println("$text has $length length")
    }
}

interface Showable {
    fun show()
}

Klasy oznaczone jako inline są podobne do aliasu typu (typealias), jednakże alias jest zgodny z przypisanym typem podstawowym i innymi aliasami wskazującymi na ten typ (wprowadza nową nazwę na typ), a klasa inline rzeczywiście tworzy nowy typ.

Wyrażenia i deklaracje obiektu

Wyrażenie obiektu (object expression) jest strukturą, która umożliwia tworzenie pojedynczej instancji obiektu bez konieczności tworzenia nowego typu. Deklaruje się go za pomocą wyrażenia wraz ze słowem kluczowym object.

var obj = object {
    var number = 10
    var text = "object expression"
}
print(obj.text) //text

Anonimowa klasa wewnętrzna (anonymous inner class) to klasa, która posiada dokładnie jedną instancje (jest modyfikacją kodu istniejącej klasy) bez jawnej deklaracji nowego podtypu. Object expression może być wykorzystywane w celu stworzenia instancji klasy anonimowej.

//normal declared class
class OnClickListener {
    fun onClick() {
        print("click")
    }
}

//normal object passed with default action
var listener = OnClickListener()
button.setOnClickListener(listener)

//object expression passed with modify action
button.setOnClickListener(object: OnClickListener() {
    override fun onClick() { print("click modified") }
})

Deklaracja obiektu (object declaration) jest implementacją wzorca Singleton w Kotlin. Deklarowana jest za pomocą słowa kluczowego object oraz nazwy i podobnie jak zwyczajna klasa może posiadać zmienne czy też funkcje. Inicjalizacja object declaration jest oznaczona jako bezpieczna wątkowo (thread-safe) i nie może zachodzić lokalnie, a odnoszenie się do właściwości następuje tak jak wywołanie funkcji statycznej czy pola w Java.

object Singleton {
    var text = "object declaration"
    fun someFun() {}
}

//execution
Singleton.someFun()
print(Singleton.text) //object declaration

Obiekt towarzysza (companion object) jest tym samym co object declaration jednakże deklarowany jest wewnątrz klasy za pomocą słowa kluczowego companion oraz opcjonalnie nazwy. Klasa może posiadać tylko jeden companion object, a jeśli nie posiada on nazwy wówczas odwołanie następuje poprzez nazwę klasy i słowo Companion (co przypomina odwołanie do pól i metod statycznych znanych z Java).

class SomeClass1 {
    companion object SingletonCompanion {
        var text = "SingletonCompanion"
        fun someFun() {}
    }
}

class SomeClass2 {
    companion object {
        var text = "Companion"
        fun someFun() {}
    }
}

SomeClass1.SingletonCompanion.someFun() //companion name can be missed
print(SomeClass1.text) //SingletonCompanion
SomeClass2.Companion.someFun() //companion default name is Companion
print(SomeClass2.text) //Companion

Modyfikatory dostępu

Klasy, obiekty, konstruktory, funkcje, właściwości i interfejsy mogą przyjmować jeden z czterech modyfikatorów dostępu: public (deklaracja dostępna wszędzie), internal (wszędzie w tym samym module), protected (w tej samej klasie i podklasach), private (deklaracja dostępna tylko w tej samej klasie). W przypadku braku podania modyfikatora widoczności domyślnie ustawiany jest public. Lokalne deklaracje mają widoczność lokalną, zatem nie mogą przyjmować modyfikatorów dostępu.

class Visibility {
  
    var number: Int //public by default
    private var text: String

    public constructor(number: Int, text: String) {
        this.number = number
        this.text = text
    }

    internal fun internalFunction() { }
    protected fun protectedFunction() { }
}

//execution from the same module but another class
var obj = Visibility(1, "text")
print(obj.number) //1
print(obj.text) //compiler error - it's private
obj.internalFunction() //it's okay because it is the same module
obj.protectedFunction() //compiler error - it's protected so access only in class or in extending class