Kotlin Bootcamp for Programmers 4: Object-oriented programming

Te warsztaty są częścią kursu Kotlin Bootcamp for Programmers. Najwięcej korzyści przyniesie Ci ukończenie wszystkich ćwiczeń w kolejności. W zależności od swojej wiedzy możesz pominąć niektóre sekcje. Ten kurs jest przeznaczony dla programistów, którzy znają język obiektowy i chcą nauczyć się Kotlin.

Wprowadzenie

W tym module dowiesz się, jak utworzyć program w języku Kotlin oraz jak korzystać z klas i obiektów w tym języku. Większość tych treści będzie Ci znana, jeśli znasz inny język obiektowy, ale Kotlin ma pewne istotne różnice, które zmniejszają ilość kodu, jaki musisz napisać. Dowiesz się też o klasach abstrakcyjnych i przekazywaniu interfejsów.

Lekcje w tym kursie nie są powiązane z jedną przykładową aplikacją. Zostały zaprojektowane tak, aby poszerzać Twoją wiedzę, ale jednocześnie były od siebie częściowo niezależne. Dzięki temu możesz przeglądać sekcje, które już znasz. Aby je ze sobą powiązać, w wielu przykładach użyto motywu akwarium. Jeśli chcesz poznać pełną historię akwarium, zapoznaj się z kursem Kotlin Bootcamp for Programmers na platformie Udacity.

Co warto wiedzieć

  • Podstawy języka Kotlin, w tym typy, operatory i pętle
  • Składnia funkcji w Kotlinie
  • Podstawy programowania obiektowego
  • podstawowe informacje o środowisku IDE, takim jak IntelliJ IDEA lub Android Studio;

Czego się nauczysz

  • Jak tworzyć klasy i uzyskiwać dostęp do właściwości w Kotlinie
  • Jak tworzyć i używać konstruktorów klas w Kotlinie
  • Jak utworzyć podklasę i jak działa dziedziczenie
  • Informacje o klasach abstrakcyjnych, interfejsach i przekazywaniu interfejsów
  • Jak tworzyć i używać klas danych
  • Jak używać singletonów, wyliczeń i klas zapieczętowanych

Jakie zadania wykonasz

  • Tworzenie klasy z właściwościami
  • Tworzenie konstruktora klasy
  • Tworzenie podklasy
  • Przeanalizuj przykłady klas abstrakcyjnych i interfejsów
  • Tworzenie prostej klasy danych
  • Więcej informacji o singletonach, wyliczeniach i klasach zamkniętych

Powinieneś(-aś) już znać te terminy programistyczne:

  • Klasy to plany obiektów. Na przykład klasa Aquarium jest planem tworzenia obiektu akwarium.
  • Obiekty to instancje klas. Obiekt akwarium to jedno rzeczywiste Aquarium.
  • Właściwości to cechy klas, np. długość, szerokość i wysokość Aquarium.
  • Metody, zwane też funkcjami składowymi, to funkcje klasy. Metody to czynności, które możesz „wykonać” na obiekcie. Możesz na przykład fillWithWater() obiekt Aquarium.
  • Interfejs to specyfikacja, którą może implementować klasa. Na przykład czyszczenie jest typowe dla obiektów innych niż akwaria i zwykle odbywa się w podobny sposób w przypadku różnych obiektów. Możesz więc mieć interfejs o nazwie Clean, który definiuje metodę clean(). Klasa Aquarium może implementować interfejs Clean, aby czyścić akwarium miękką gąbką.
  • Pakiety to sposób na grupowanie powiązanego kodu w celu zachowania porządku lub utworzenia biblioteki kodu. Po utworzeniu pakietu możesz zaimportować jego zawartość do innego pliku i ponownie wykorzystać w nim kod i klasy.

W tym zadaniu utworzysz nowy pakiet i klasę z kilkoma właściwościami i metodą.

Krok 1. Utwórz pakiet

Pakiety pomagają w porządkowaniu kodu.

  1. W panelu Project (Projekt) w projekcie Hello Kotlin kliknij prawym przyciskiem myszy folder src.
  2. Wybierz Nowy > Pakiet i nadaj mu nazwę example.myapp.

Krok 2. Tworzenie klasy z właściwościami

Klasy są definiowane za pomocą słowa kluczowego class, a nazwy klas zwykle zaczynają się od wielkiej litery.

  1. Kliknij prawym przyciskiem myszy pakiet example.myapp.
  2. Wybierz New > Kotlin File / Class (Nowy > Plik/klasa Kotlin).
  3. W sekcji Rodzaj wybierz Zajęcia i nadaj zajęciom nazwę Aquarium. IntelliJ IDEA umieszcza nazwę pakietu w pliku i tworzy pustą klasę Aquarium.
  4. W klasie Aquarium zdefiniuj i zainicjuj właściwości var dla szerokości, wysokości i długości (w centymetrach). Zainicjuj właściwości wartościami domyślnymi.
package example.myapp

class Aquarium {
    var width: Int = 20
    var height: Int = 40
    var length: Int = 100
}

Kotlin automatycznie tworzy metody pobierające i ustawiające dla właściwości zdefiniowanych w klasie Aquarium, dzięki czemu możesz uzyskiwać do nich bezpośredni dostęp, np. myAquarium.length.

Krok 3. Utwórz funkcję main()

Utwórz nowy plik o nazwie main.kt, w którym będzie się znajdować funkcja main().

  1. W panelu Project po lewej stronie kliknij prawym przyciskiem myszy pakiet example.myapp.
  2. Wybierz New > Kotlin File / Class (Nowy > Plik/klasa Kotlin).
  3. W menu Rodzaj pozostaw wybraną opcję Plik i nadaj plikowi nazwę main.kt. IntelliJ IDEA zawiera nazwę pakietu, ale nie zawiera definicji klasy dla pliku.
  4. Zdefiniuj funkcję buildAquarium() i utwórz w niej instancję Aquarium. Aby utworzyć instancję, odwołaj się do klasy tak, jakby była funkcją, Aquarium(). Wywołuje to konstruktor klasy i tworzy instancję klasy Aquarium, podobnie jak użycie new w innych językach.
  5. Zdefiniuj funkcję main() i wywołaj funkcję buildAquarium().
package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium()
}

fun main() {
    buildAquarium()
}

Krok 4. Dodaj metodę

  1. W klasie Aquarium dodaj metodę drukowania właściwości wymiarów akwarium.
    fun printSize() {
        println("Width: $width cm " +
                "Length: $length cm " +
                "Height: $height cm ")
    }
  1. W main.kt w buildAquarium() wywołaj metodę printSize() w myAquarium.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
}
  1. Uruchom program, klikając zielony trójkąt obok funkcji main(). Sprawdź wynik.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
  1. buildAquarium() dodaj kod, aby ustawić wysokość na 60 i wydrukować zmienione właściwości wymiaru.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
    myAquarium.height = 60
    myAquarium.printSize()
}
  1. Uruchom program i obserwuj dane wyjściowe.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 100 cm Height: 60 cm 

W tym zadaniu utworzysz konstruktor klasy i będziesz kontynuować pracę z właściwościami.

Krok 1. Utwórz konstruktor

W tym kroku dodasz konstruktor do klasy Aquarium utworzonej w pierwszym zadaniu. W poprzednim przykładzie wszystkie instancje znaku Aquarium mają takie same wymiary. Po utworzeniu wymiaru możesz go zmienić, ustawiając odpowiednie właściwości, ale prościej będzie od razu utworzyć go w prawidłowym rozmiarze.

W niektórych językach programowania konstruktor jest definiowany przez utworzenie w klasie metody o tej samej nazwie co klasa. W języku Kotlin konstruktor definiuje się bezpośrednio w deklaracji klasy, podając parametry w nawiasach tak, jakby klasa była metodą. Podobnie jak w przypadku funkcji w języku Kotlin, te parametry mogą zawierać wartości domyślne.

  1. W utworzonej wcześniej klasie Aquarium zmień definicję klasy, aby zawierała 3 parametry konstruktora z wartościami domyślnymi dla length, widthheight, i przypisz je do odpowiednich właściwości.
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
   // Dimensions in cm
   var length: Int = length
   var width: Int = width
   var height: Int = height
...
}
  1. Bardziej zwięzły sposób w Kotlinie polega na zdefiniowaniu właściwości bezpośrednio w konstruktorze za pomocą var lub val. Kotlin automatycznie tworzy też metody pobierające i ustawiające. Następnie możesz usunąć definicje właściwości w treści klasy.
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
  1. Gdy utworzysz obiekt Aquarium za pomocą tego konstruktora, możesz nie podawać żadnych argumentów i uzyskać wartości domyślne, podać tylko niektóre z nich lub podać wszystkie i utworzyć obiekt Aquarium o całkowicie niestandardowym rozmiarze. W funkcji buildAquarium() wypróbuj różne sposoby tworzenia obiektu Aquarium za pomocą nazwanych parametrów.
fun buildAquarium() {
    val aquarium1 = Aquarium()
    aquarium1.printSize()
    // default height and length
    val aquarium2 = Aquarium(width = 25)
    aquarium2.printSize()
    // default width
    val aquarium3 = Aquarium(height = 35, length = 110)
    aquarium3.printSize()
    // everything custom
    val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
    aquarium4.printSize()
}
  1. Uruchom program i sprawdź dane wyjściowe.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 25 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 110 cm Height: 35 cm 
Width: 25 cm Length: 110 cm Height: 35 cm 

Zwróć uwagę, że nie musisz przeciążać konstruktora i pisać innej wersji dla każdego z tych przypadków (plus kilku innych dla pozostałych kombinacji). Kotlin tworzy to, co jest potrzebne, na podstawie wartości domyślnych i parametrów nazwanych.

Krok 2. Dodaj bloki inicjujące

Przykładowe konstruktory powyżej tylko deklarują właściwości i przypisują im wartość wyrażenia. Jeśli konstruktor potrzebuje więcej kodu inicjującego, można go umieścić w 1 lub większej liczbie bloków init. W tym kroku dodasz kilka bloków init do klasy Aquarium.

  1. W klasie Aquarium dodaj blok init, aby wydrukować informację o inicjowaniu obiektu, oraz drugi blok, aby wydrukować objętość w litrach.
class Aquarium (var length: Int = 100, var width: Int = 20, var height: Int = 40) {
    init {
        println("aquarium initializing")
    }
    init {
        // 1 liter = 1000 cm^3
        println("Volume: ${width * length * height / 1000} l")
    }
}
  1. Uruchom program i sprawdź dane wyjściowe.
aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 100 l
Width: 25 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 77 l
Width: 20 cm Length: 110 cm Height: 35 cm 
aquarium initializing
Volume: 96 l
Width: 25 cm Length: 110 cm Height: 35 cm 

Zwróć uwagę, że bloki init są wykonywane w kolejności, w jakiej występują w definicji klasy, i wszystkie są wykonywane po wywołaniu konstruktora.

Krok 3. Dowiedz się więcej o konstruktorach dodatkowych

W tym kroku dowiesz się więcej o konstruktorach dodatkowych i dodasz jeden do swojej klasy. Oprócz konstruktora głównego, który może zawierać co najmniej 1 blok init, klasa Kotlin może mieć też co najmniej 1 konstruktor dodatkowy, który umożliwia przeciążanie konstruktorów, czyli konstruktorów z różnymi argumentami.

  1. W klasie Aquarium dodaj konstruktor dodatkowy, który przyjmuje liczbę ryb jako argument, używając słowa kluczowego constructor. Utwórz usługę val, która będzie obliczać objętość akwarium w litrach na podstawie liczby ryb. Przyjmij 2 litry (2000 cm³) wody na rybę,plus trochę dodatkowego miejsca, aby woda się nie wylała.
constructor(numberOfFish: Int) : this() {
    // 2,000 cm^3 per fish + extra room so water doesn't spill
    val tank = numberOfFish * 2000 * 1.1
}
  1. W konstruktorze dodatkowym zachowaj długość i szerokość (ustawione w konstruktorze podstawowym) i oblicz wysokość potrzebną do uzyskania podanej objętości zbiornika.
    // calculate the height needed
    height = (tank / (length * width)).toInt()
  1. W funkcji buildAquarium() dodaj wywołanie, aby utworzyć Aquarium za pomocą nowego konstruktora dodatkowego. Wydrukuj rozmiar i objętość.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
  1. Uruchom program i obserwuj dane wyjściowe.
⇒ aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

Zauważ, że wartość jest drukowana 2 razy: raz przez blok init w konstruktorze podstawowym przed wykonaniem konstruktora dodatkowego i raz przez kod w buildAquarium().

Słowo kluczowe constructor można też umieścić w konstruktorze podstawowym, ale w większości przypadków nie jest to konieczne.

Krok 4. Dodaj nowy getter właściwości

W tym kroku dodasz jawny getter właściwości. Kotlin automatycznie definiuje metody pobierające i ustawiające, gdy definiujesz właściwości, ale czasami wartość właściwości wymaga dostosowania lub obliczenia. Na przykład powyżej wydrukowano objętość Aquarium. Możesz udostępnić głośność jako właściwość, definiując zmienną i jej funkcję pobierającą. Ponieważ wartość volume musi zostać obliczona, funkcja pobierająca musi zwracać obliczoną wartość, co można zrobić za pomocą funkcji jednoliniowej.

  1. W klasie Aquarium zdefiniuj właściwość Int o nazwie volume, a w następnym wierszu zdefiniuj metodę get(), która oblicza objętość.
val volume: Int
    get() = width * height * length / 1000  // 1000 cm^3 = 1 l
  1. Usuń blok init, który wyświetla głośność.
  2. Usuń kod w buildAquarium(), który wyświetla głośność.
  3. W metodzie printSize() dodaj wiersz, aby wydrukować głośność.
fun printSize() {
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm "
    )
    // 1 l = 1000 cm^3
    println("Volume: $volume l")
}
  1. Uruchom program i obserwuj dane wyjściowe.
⇒ aquarium initializing
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

Wymiary i objętość są takie same jak wcześniej, ale objętość jest drukowana tylko raz po pełnym zainicjowaniu obiektu przez konstruktor podstawowy i dodatkowy.

Krok 5. Dodaj ustawiającego właściwość

W tym kroku utworzysz nowy moduł ustawiania właściwości woluminu.

  1. W klasie Aquarium zmień volume na var, aby można było ustawić ją więcej niż raz.
  2. Dodaj funkcję ustawiającą dla właściwości volume, dodając metodę set() poniżej funkcji pobierającej, która ponownie oblicza wysokość na podstawie podanej ilości wody. Zgodnie z konwencją nazwa parametru ustawiającego to value, ale możesz ją zmienić.
var volume: Int
    get() = width * height * length / 1000
    set(value) {
        height = (value * 1000) / (width * length)
    }
  1. buildAquarium() dodaj kod, aby ustawić objętość akwarium na 70 litrów. Wydrukuj w nowym rozmiarze.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    aquarium6.volume = 70
    aquarium6.printSize()
}
  1. Uruchom program ponownie i obserwuj zmienioną wysokość i głośność.
⇒ aquarium initialized
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l
Width: 20 cm Length: 100 cm Height: 35 cm 
Volume: 70 l

W kodzie nie ma jeszcze modyfikatorów widoczności, takich jak public czy private. Domyślnie wszystko w Kotlinie jest publiczne, co oznacza, że wszystko jest dostępne wszędzie, w tym klasy, metody, właściwości i zmienne składowe.

W języku Kotlin klasy, obiekty, interfejsy, konstruktory, funkcje, właściwości i ich metody ustawiające mogą mieć modyfikatory widoczności:

  • public oznacza widoczność poza klasą. Domyślnie wszystko jest publiczne, w tym zmienne i metody klasy.
  • internal oznacza, że będzie widoczny tylko w tym module. Moduł to zestaw plików Kotlin skompilowanych razem, np. biblioteka lub aplikacja.
  • private oznacza, że będzie widoczna tylko w tej klasie (lub w pliku źródłowym, jeśli pracujesz z funkcjami).
  • protected jest tym samym co private, ale będzie też widoczny dla wszystkich podklas.

Więcej informacji znajdziesz w sekcji Modyfikatory widoczności w dokumentacji Kotlin.

Zmienne składowe

Właściwości w klasie lub zmienne składowe są domyślnie public. Jeśli zdefiniujesz je za pomocą var, będą zmienne, czyli będzie można je odczytywać i zapisywać. Jeśli zdefiniujesz je za pomocą val, po zainicjowaniu będą dostępne tylko do odczytu.

Jeśli chcesz, aby właściwość była dostępna do odczytu i zapisu dla Twojego kodu, ale tylko do odczytu dla kodu zewnętrznego, możesz pozostawić właściwość i jej funkcję pobierającą jako publiczne, a funkcję ustawiającą zadeklarować jako prywatną, jak pokazano poniżej.

var volume: Int
    get() = width * height * length / 1000
    private set(value) {
        height = (value * 1000) / (width * length)
    }

W tym ćwiczeniu dowiesz się, jak działają podklasy i dziedziczenie w języku Kotlin. Są one podobne do tych, które znasz z innych języków, ale występują między nimi pewne różnice.

W Kotlinie domyślnie nie można tworzyć podklas klas. Podobnie właściwości i zmienne członkowskie nie mogą być zastępowane przez podklasy (chociaż można do nich uzyskać dostęp).

Aby umożliwić tworzenie podklas, musisz oznaczyć klasę jako open. Podobnie, aby zastąpić właściwości i zmienne składowe w klasie podrzędnej, musisz oznaczyć je jako open. Słowo kluczowe open jest wymagane, aby zapobiec przypadkowemu ujawnieniu szczegółów implementacji w ramach interfejsu klasy.

Krok 1. Otwórz klasę Akwarium

W tym kroku ustawisz klasę Aquarium jako open, aby można było ją zastąpić w następnym kroku.

  1. Oznacz klasę Aquarium i wszystkie jej właściwości słowem kluczowym open.
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
    open var volume: Int
        get() = width * height * length / 1000
        set(value) {
            height = (value * 1000) / (width * length)
        }
  1. Dodaj otwartą właściwość shape z wartością "rectangle".
   open val shape = "rectangle"
  1. Dodaj otwartą właściwość water z metodą getter, która zwraca 90% wartości właściwości Aquarium.
    open var water: Double = 0.0
        get() = volume * 0.9
  1. Dodaj kod do metody printSize(), aby wydrukować kształt i ilość wody jako procent objętości.
fun printSize() {
    println(shape)
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm ")
    // 1 l = 1000 cm^3
    println("Volume: $volume l Water: $water l (${water/volume*100.0}% full)")
}
  1. buildAquarium() zmień kod, aby utworzyć Aquariumwidth = 25, length = 25height = 40.
fun buildAquarium() {
    val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
    aquarium6.printSize()
}
  1. Uruchom program i sprawdź nowe dane wyjściowe.
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)

Krok 2. Tworzenie podklasy

  1. Utwórz podklasę Aquarium o nazwie TowerTank, która implementuje zbiornik w kształcie zaokrąglonego walca zamiast zbiornika prostokątnego. Możesz dodać TowerTank poniżej Aquarium, ponieważ możesz dodać kolejną klasę w tym samym pliku co klasa Aquarium.
  2. TowerTank zastąp właściwość height zdefiniowaną w konstruktorze. Aby zastąpić właściwość, użyj w podklasie słowa kluczowego override.
  1. Spraw, aby konstruktor TowerTank przyjmował diameter. Użyj diameter zarówno w przypadku length, jak i width podczas wywoływania konstruktora w klasie nadrzędnej Aquarium.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. Zastąp właściwość objętości, aby obliczyć walec. Wzór na objętość walca to pi razy promień do kwadratu razy wysokość. Musisz zaimportować stałą PIjava.lang.Math.
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }
  1. TowerTank zastąp właściwość water wartością 80% głośności.
override var water = volume * 0.8
  1. Zastąp wartość shape wartością "cylinder".
override val shape = "cylinder"
  1. Ostateczna klasa TowerTank powinna wyglądać mniej więcej tak, jak w poniższym przykładzie.

Aquarium.kt:

package example.myapp

import java.lang.Math.PI

... // existing Aquarium class

class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }

    override var water = volume * 0.8
    override val shape = "cylinder"
}
  1. W buildAquarium() utwórz TowerTank o średnicy 25 cm i wysokości 45 cm. Wydrukuj rozmiar.

main.kt:

package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium(width = 25, length = 25, height = 40)
    myAquarium.printSize()
    val myTower = TowerTank(diameter = 25, height = 40)
    myTower.printSize()
}
  1. Uruchom program i obserwuj dane wyjściowe.
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)
aquarium initializing
cylinder
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 18 l Water: 14.4 l (80.0% full)

Czasami chcesz zdefiniować wspólne zachowania lub właściwości, które będą współdzielone przez niektóre powiązane klasy. Kotlin oferuje 2 sposoby na to: interfejsy i klasy abstrakcyjne. W tym zadaniu utworzysz abstrakcyjną klasę AquariumFish dla właściwości wspólnych dla wszystkich ryb. Tworzysz interfejs o nazwie FishAction, aby zdefiniować zachowania wspólne dla wszystkich ryb.

  • Klasy abstrakcyjnej ani interfejsu nie można utworzyć samodzielnie, co oznacza, że nie można bezpośrednio tworzyć obiektów tych typów.
  • Klasy abstrakcyjne mają konstruktory.
  • Interfejsy nie mogą zawierać logiki konstruktora ani przechowywać żadnego stanu.

Krok 1. Tworzenie klasy abstrakcyjnej

  1. W sekcji example.myapp utwórz nowy plik AquariumFish.kt.
  2. Utwórz zajęcia, zwane też AquariumFish, i oznacz je symbolem abstract.
  3. Dodaj jedną właściwość String, color, i oznacz ją symbolem abstract.
package example.myapp

abstract class AquariumFish {
    abstract val color: String
}
  1. Utwórz 2 podklasy klasy AquariumFish: SharkPlecostomus.
  2. color jest klasą abstrakcyjną, więc klasy podrzędne muszą ją implementować. Ustaw kolor Shark na szary, a Plecostomus na złoty.
class Shark: AquariumFish() {
    override val color = "gray"
}

class Plecostomus: AquariumFish() {
    override val color = "gold"
}
  1. W pliku main.kt utwórz funkcję makeFish(), aby przetestować klasy. Utwórz instancję klasy SharkPlecostomus, a następnie wydrukuj kolor każdej z nich.
  2. Usuń wcześniejszy kod testowy w main() i dodaj wywołanie makeFish(). Kod powinien wyglądać mniej więcej tak, jak poniżej.

main.kt:

package example.myapp

fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()

    println("Shark: ${shark.color}")
    println("Plecostomus: ${pleco.color}")
}

fun main () {
    makeFish()
}
  1. Uruchom program i obserwuj dane wyjściowe.
⇒ Shark: gray 
Plecostomus: gold

Poniższy diagram przedstawia klasy SharkPlecostomus, które są podklasami klasy abstrakcyjnej AquariumFish.

Diagram przedstawiający klasę abstrakcyjną AquariumFish i dwie podklasy: Shark i Plecostomus.

Krok 2. Tworzenie interfejsu

  1. W pliku AquariumFish.kt utwórz interfejs o nazwie FishAction z metodą eat().
interface FishAction  {
    fun eat()
}
  1. Dodaj FishAction do każdej z podklas i zastosuj eat(), aby wyświetlać, co robi ryba.
class Shark: AquariumFish(), FishAction {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

class Plecostomus: AquariumFish(), FishAction {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. W funkcji makeFish() spraw, aby każda utworzona ryba zjadła coś, wywołując funkcję eat().
fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()
    println("Shark: ${shark.color}")
    shark.eat()
    println("Plecostomus: ${pleco.color}")
    pleco.eat()
}
  1. Uruchom program i obserwuj dane wyjściowe.
⇒ Shark: gray
hunt and eat fish
Plecostomus: gold
eat algae

Poniższy diagram przedstawia klasy SharkPlecostomus, które składają się z interfejsu FishAction i go implementują.

Kiedy używać klas abstrakcyjnych, a kiedy interfejsów

Powyższe przykłady są proste, ale gdy masz wiele powiązanych ze sobą klas, klasy abstrakcyjne i interfejsy mogą pomóc Ci zachować przejrzystość, porządek i łatwość utrzymania projektu.

Jak wspomnieliśmy powyżej, klasy abstrakcyjne mogą mieć konstruktory, a interfejsy nie, ale poza tym są bardzo podobne. Kiedy więc należy używać poszczególnych typów?

Gdy używasz interfejsów do tworzenia klasy, jej funkcjonalność jest rozszerzana za pomocą instancji klas, które zawiera. Kompozycja sprawia, że kod jest łatwiejszy do ponownego wykorzystania i zrozumienia niż dziedziczenie z klasy abstrakcyjnej. W klasie możesz też używać wielu interfejsów, ale możesz utworzyć podklasę tylko z jednej klasy abstrakcyjnej.

Kompozycja często prowadzi do lepszego enkapsulowania, mniejszego powiązania (współzależności), bardziej przejrzystych interfejsów i bardziej użytecznego kodu. Z tych powodów preferowanym rozwiązaniem jest używanie kompozycji z interfejsami. Z drugiej strony dziedziczenie z klasy abstrakcyjnej w przypadku niektórych problemów jest naturalnym rozwiązaniem. Dlatego warto stosować kompozycję, ale jeśli dziedziczenie ma sens, Kotlin też na to pozwala.

  • Użyj interfejsu, jeśli masz wiele metod i 1–2 implementacje domyślne, np. jak w przykładzie AquariumAction poniżej.
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • Klasy abstrakcyjnej używaj zawsze, gdy nie możesz ukończyć klasy. Na przykład wracając do klasy AquariumFish, możesz sprawić, że wszystkie klasy AquariumFish będą implementować interfejs FishAction, i zapewnić domyślną implementację dla klasy eat, pozostawiając klasę color abstrakcyjną, ponieważ nie ma domyślnego koloru ryb.
interface FishAction  {
    fun eat()
}

abstract class AquariumFish: FishAction {
   abstract val color: String
   override fun eat() = println("yum")
}

W poprzednim zadaniu przedstawiliśmy klasy abstrakcyjne, interfejsy i koncepcję kompozycji. Delegowanie interfejsu to zaawansowana technika, w której metody interfejsu są implementowane przez obiekt pomocniczy (lub delegata), który jest następnie używany przez klasę. Ta technika może być przydatna, gdy używasz interfejsu w serii niezwiązanych ze sobą klas: dodajesz potrzebną funkcjonalność interfejsu do osobnej klasy pomocniczej, a każda z klas używa instancji klasy pomocniczej do implementowania tej funkcjonalności.

W tym zadaniu użyjesz delegowania interfejsu, aby dodać funkcje do klasy.

Krok 1. Utwórz nowy interfejs

  1. W pliku AquariumFish.kt usuń klasę AquariumFish. Zamiast dziedziczyć po klasie AquariumFish, klasy PlecostomusShark będą implementować interfejsy zarówno dla działania ryby, jak i jej koloru.
  2. Utwórz nowy interfejs FishColor, który definiuje kolor jako ciąg znaków.
interface FishColor {
    val color: String
}
  1. Zmień Plecostomus, aby zaimplementować 2 interfejsy, FishActionFishColor. Musisz zastąpić wartość color wartością FishColor, a wartość eat() wartością FishAction.
class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. Zmień klasę Shark, aby implementowała interfejsy FishActionFishColor zamiast dziedziczyć po klasie AquariumFish.
class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}
  1. Gotowy kod powinien wyglądać mniej więcej tak:
package example.myapp

interface FishAction {
    fun eat()
}

interface FishColor {
    val color: String
}

class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}

class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

Krok 2. Utwórz klasę singleton

Następnie wdrażasz konfigurację części delegowania, tworząc klasę pomocniczą, która implementuje FishColor. Tworzysz klasę podstawową o nazwie GoldColor, która implementuje interfejs FishColor. Jej jedynym zadaniem jest określenie, że jej kolor to złoty.

Tworzenie wielu instancji GoldColor nie ma sensu, ponieważ wszystkie robią dokładnie to samo. W języku Kotlin możesz zadeklarować klasę, w której można utworzyć tylko jedną instancję, używając słowa kluczowego object zamiast class. Kotlin utworzy tę jedną instancję, do której odwołuje się nazwa klasy. Wszystkie inne obiekty mogą wtedy używać tylko tej jednej instancji – nie ma możliwości utworzenia innych instancji tej klasy. Jeśli znasz wzorzec singleton, w ten sposób możesz go zaimplementować w Kotlinie.

  1. W pliku AquariumFish.kt utwórz obiekt dla GoldColor. Zastąp kolor.
object GoldColor : FishColor {
   override val color = "gold"
}

Krok 3. Dodaj delegowanie interfejsu dla FishColor

Możesz już korzystać z przekazywania dostępu do interfejsu.

  1. W pliku AquariumFish.kt usuń zastąpienie colorPlecostomus.
  2. Zmień klasę Plecostomus, aby pobierać kolor z klasy GoldColor. W tym celu dodaj by GoldColor do deklaracji klasy, tworząc delegowanie. Oznacza to, że zamiast implementować FishColor, należy użyć implementacji dostarczonej przez GoldColor. Dlatego za każdym razem, gdy uzyskiwany jest dostęp do color, jest on delegowany do GoldColor.
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

W obecnej postaci wszystkie zbrojniki będą złote, ale w rzeczywistości te ryby występują w wielu kolorach. Możesz to zrobić, dodając parametr konstruktora dla koloru z wartością GoldColor jako domyślnym kolorem dla Plecostomus.

  1. Zmień klasę Plecostomus, aby przyjmowała przekazaną wartość fishColor z konstruktorem, i ustaw jej wartość domyślną na GoldColor. Zmień delegowanie z by GoldColor na by fishColor.
class Plecostomus(fishColor: FishColor = GoldColor):  FishAction,
       FishColor by fishColor {
   override fun eat() {
       println("eat algae")
   }
}

Krok 4. Dodaj delegowanie interfejsu dla FishAction

W ten sam sposób możesz użyć przekazywania interfejsu w przypadku FishAction.

  1. W pliku AquariumFish.kt utwórz klasę PrintingFishAction, która implementuje interfejs FishAction. Klasa ta przyjmuje argumenty Stringfood, a następnie drukuje informacje o tym, co je ryba.
class PrintingFishAction(val food: String) : FishAction {
    override fun eat() {
        println(food)
    }
}
  1. W klasie Plecostomus usuń funkcję zastępowania eat(), ponieważ zastąpisz ją delegowaniem.
  2. W deklaracji Plecostomus przekaż delegowanie FishAction do PrintingFishAction, przekazując "eat algae".
  3. W wyniku tego delegowania w treści klasy Plecostomus nie ma kodu, więc usuń {}, ponieważ wszystkie zastąpienia są obsługiwane przez delegowanie interfejsu.
class Plecostomus (fishColor: FishColor = GoldColor):
        FishAction by PrintingFishAction("eat algae"),
        FishColor by fishColor

Poniższy diagram przedstawia klasy SharkPlecostomus, które składają się z interfejsów PrintingFishActionFishColor, ale przekazują im implementację.

Delegowanie interfejsu jest bardzo przydatne i warto rozważyć jego użycie zawsze, gdy w innym języku używasz klasy abstrakcyjnej. Umożliwia to używanie kompozycji do włączania zachowań zamiast tworzenia wielu podklas, z których każda jest wyspecjalizowana w inny sposób.

Klasa danych jest podobna do struct w niektórych innych językach – istnieje głównie po to, aby przechowywać dane – ale obiekt klasy danych jest nadal obiektem. Obiekty klasy danych Kotlin mają pewne dodatkowe zalety, takie jak narzędzia do drukowania i kopiowania. W tym zadaniu utworzysz prostą klasę danych i dowiesz się, jak Kotlin obsługuje klasy danych.

Krok 1. Utwórz klasę danych

  1. Dodaj nowy pakiet decor w pakiecie example.myapp, aby przechowywać nowy kod. W panelu Project (Projekt) kliknij prawym przyciskiem myszy example.myapp i wybierz File > New > Package (Plik > Nowy > Pakiet).
  2. W pakiecie utwórz nową klasę o nazwie Decoration.
package example.myapp.decor

class Decoration {
}
  1. Aby utworzyć klasę danych Decoration, dodaj przed deklaracją klasy słowo kluczowe data.
  2. Dodaj właściwość String o nazwie rocks, aby przekazać klasie dane.
data class Decoration(val rocks: String) {
}
  1. W pliku poza klasą dodaj funkcję makeDecorations(), aby utworzyć i wydrukować instancję klasy Decoration z wartością "granite".
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. Dodaj funkcję main(), aby wywołać funkcję makeDecorations(), i uruchom program. Zwróć uwagę na sensowne dane wyjściowe, które zostały utworzone, ponieważ jest to klasa danych.
⇒ Decoration(rocks=granite)
  1. makeDecorations() utwórz jeszcze 2 obiekty Decoration, które są „slate”, i je wydrukuj.
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)

    val decoration2 = Decoration("slate")
    println(decoration2)

    val decoration3 = Decoration("slate")
    println(decoration3)
}
  1. makeDecorations() dodaj instrukcję drukowania, która wyświetli wynik porównania decoration1decoration2, oraz drugą, która wyświetli wynik porównania decoration3decoration2. Użyj metody equals() udostępnianej przez klasy danych.
    println (decoration1.equals(decoration2))
    println (decoration3.equals(decoration2))
  1. Uruchom kod.
⇒ Decoration(rocks=granite)
Decoration(rocks=slate)
Decoration(rocks=slate)
false
true

Krok 2. Używanie destrukturyzacji

Aby uzyskać dostęp do właściwości obiektu danych i przypisać je do zmiennych, możesz przypisać je pojedynczo, tak jak poniżej.

val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver

Zamiast tego możesz utworzyć zmienne (po jednej dla każdej usługi) i przypisać obiekt danych do grupy zmiennych. Kotlin umieszcza wartość właściwości w każdej zmiennej.

val (rock, wood, diver) = decoration

Jest to tzw. destrukturyzacja, która jest przydatnym skrótem. Liczba zmiennych powinna być zgodna z liczbą właściwości, a zmienne są przypisywane w kolejności, w jakiej są zadeklarowane w klasie. Oto pełny przykład, który możesz wypróbować w pliku Decoration.kt.

// Here is a data class with 3 properties.
data class Decoration2(val rocks: String, val wood: String, val diver: String){
}

fun makeDecorations() {
    val d5 = Decoration2("crystal", "wood", "diver")
    println(d5)

// Assign all properties to variables.
    val (rock, wood, diver) = d5
    println(rock)
    println(wood)
    println(diver)
}
⇒ Decoration2(rocks=crystal, wood=wood, diver=diver)
crystal
wood
diver

Jeśli nie potrzebujesz niektórych właściwości, możesz je pominąć, używając znaku _ zamiast nazwy zmiennej, jak pokazano w poniższym kodzie.

    val (rock, _, diver) = d5

W tym ćwiczeniu poznasz niektóre klasy specjalnego przeznaczenia w języku Kotlin, w tym:

  • Klasy singleton
  • Wartości w polu enum
  • Klasy zapieczętowane

Krok 1. Przywołaj klasy singletonów

Przypomnij sobie wcześniejszy przykład z klasą GoldColor.

object GoldColor : FishColor {
   override val color = "gold"
}

Ponieważ każde wystąpienie GoldColor robi to samo, jest deklarowane jako object, a nie jako class, aby było singletonem. Może istnieć tylko jedno wystąpienie tego elementu.

Krok 2. Utwórz wyliczenie

Kotlin obsługuje też wyliczenia, które umożliwiają wyliczanie elementów i odwoływanie się do nich za pomocą nazwy, podobnie jak w innych językach. Zadeklaruj wyliczenie, dodając przed deklaracją słowo kluczowe enum. Podstawowa deklaracja wyliczenia wymaga tylko listy nazw, ale możesz też zdefiniować co najmniej jedno pole powiązane z każdą nazwą.

  1. W pliku Decoration.kt wypróbuj przykład wyliczenia.
enum class Color(val rgb: Int) {
   RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}

Wyliczenia są trochę podobne do singletonów – może być tylko jeden i tylko jedna wartość w każdym wyliczeniu. Na przykład może być tylko jeden element Color.RED, jeden element Color.GREEN i jeden element Color.BLUE. W tym przykładzie wartości RGB są przypisane do właściwości rgb, aby reprezentować komponenty koloru. Możesz też uzyskać wartość porządkową wyliczenia za pomocą właściwości ordinal, a jego nazwę za pomocą właściwości name.

  1. Wypróbuj inny przykład wyliczenia.
enum class Direction(val degrees: Int) {
    NORTH(0), SOUTH(180), EAST(90), WEST(270)
}

fun main() {
    println(Direction.EAST.name)
    println(Direction.EAST.ordinal)
    println(Direction.EAST.degrees)
}
⇒ EAST
2
90

Krok 3. Tworzenie klasy zapieczętowanej

Klasa zapieczętowana to klasa, która może mieć podklasy, ale tylko w pliku, w którym została zadeklarowana. Jeśli spróbujesz utworzyć podklasę w innym pliku, pojawi się błąd.

Klasy i podklasy znajdują się w tym samym pliku, więc Kotlin będzie statycznie znać wszystkie podklasy. Oznacza to, że w czasie kompilacji kompilator widzi wszystkie klasy i podklasy i wie, że to wszystkie, więc może przeprowadzić dodatkowe sprawdzanie.

  1. W pliku AquariumFish.kt wypróbuj przykład klasy zamkniętej, zachowując motyw akwatyczny.
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()

fun matchSeal(seal: Seal): String {
   return when(seal) {
       is Walrus -> "walrus"
       is SeaLion -> "sea lion"
   }
}

Klasy Seal nie można rozszerzyć w innym pliku. Jeśli chcesz dodać więcej typów Seal, musisz je dodać w tym samym pliku. Dzięki temu klasy zapieczętowane są bezpiecznym sposobem reprezentowania stałej liczby typów. Na przykład klasy zapieczętowane świetnie się sprawdzają w przypadku zwracania informacji o powodzeniu lub błędzie z interfejsu API sieci.

Ta lekcja obejmowała wiele zagadnień. Wiele elementów powinno być znanych z innych obiektowych języków programowania, ale Kotlin dodaje kilka funkcji, które sprawiają, że kod jest zwięzły i czytelny.

Klasy i konstruktory

  • Zdefiniuj klasę w Kotlinie za pomocą class.
  • Kotlin automatycznie tworzy metody ustawiające i pobierające wartości właściwości.
  • Zdefiniuj konstruktor podstawowy bezpośrednio w definicji klasy. Na przykład:
    class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
  • Jeśli konstruktor podstawowy wymaga dodatkowego kodu, wpisz go w co najmniej jednym bloku init.
  • Klasa może definiować co najmniej 1 konstruktor dodatkowy za pomocą słowa kluczowego constructor, ale w języku Kotlin zaleca się używanie funkcji fabrycznej.

Modyfikatory widoczności i podklasy

  • Wszystkie klasy i funkcje w Kotlinie są domyślnie public, ale możesz użyć modyfikatorów, aby zmienić widoczność na internal, private lub protected.
  • Aby utworzyć podklasę, klasa nadrzędna musi być oznaczona jako open.
  • Aby zastąpić metody i właściwości w klasie podrzędnej, metody i właściwości muszą być oznaczone jako open w klasie nadrzędnej.
  • Klasę zamkniętą można rozszerzyć tylko w pliku, w którym została zdefiniowana. Utwórz klasę zamkniętą, dodając przed deklaracją prefiks sealed.

Klasy danych, singletony i wyliczenia

  • Utwórz klasę danych, dodając przed deklaracją znak data.
  • Destrukturyzacja to skrótowe przypisywanie właściwości obiektu data do osobnych zmiennych.
  • Utwórz klasę singleton, używając object zamiast class.
  • Zdefiniuj enum za pomocą enum class.

Klasy abstrakcyjne, interfejsy i delegowanie

  • Klasy abstrakcyjne i interfejsy to 2 sposoby na udostępnianie wspólnych zachowań między klasami.
  • Klasa abstrakcyjna definiuje właściwości i zachowanie, ale pozostawia implementację podklasom.
  • Interfejs określa zachowanie i może udostępniać domyślne implementacje niektórych lub wszystkich zachowań.
  • Gdy używasz interfejsów do tworzenia klasy, jej funkcjonalność jest rozszerzana za pomocą instancji klas, które zawiera.
  • Delegowanie interfejsu wykorzystuje kompozycję, ale deleguje też implementację do klas interfejsu.
  • Kompozycja to skuteczny sposób dodawania funkcjonalności do klasy za pomocą delegowania interfejsu. Ogólnie preferowana jest kompozycja, ale w przypadku niektórych problemów lepszym rozwiązaniem jest dziedziczenie z klasy abstrakcyjnej.

Dokumentacja języka Kotlin

Jeśli chcesz uzyskać więcej informacji na dowolny temat w tym kursie lub jeśli napotkasz trudności, najlepszym punktem wyjścia będzie strona https://kotlinlang.org.

Samouczki dotyczące języka Kotlin

Witryna https://try.kotlinlang.org zawiera rozbudowane samouczki Kotlin Koans, internetowy interpreter i pełny zestaw dokumentacji z przykładami.

Kurs Udacity

Aby obejrzeć kurs Udacity na ten temat, zobacz Kotlin Bootcamp for Programmers (w języku angielskim).

IntelliJ IDEA

Dokumentację IntelliJ IDEA znajdziesz w witrynie JetBrains.

W tej sekcji znajdziesz listę możliwych zadań domowych dla uczniów, którzy wykonują ten moduł w ramach kursu prowadzonego przez instruktora. Nauczyciel musi:

  • W razie potrzeby przypisz pracę domową.
  • Poinformuj uczniów, jak przesyłać projekty.
  • Oceń zadania domowe.

Instruktorzy mogą korzystać z tych sugestii w dowolnym zakresie i mogą zadawać inne zadania domowe, które uznają za odpowiednie.

Jeśli wykonujesz ten kurs samodzielnie, możesz użyć tych zadań domowych, aby sprawdzić swoją wiedzę.

Odpowiedz na te pytania

Pytanie 1

Klasy mają specjalną metodę, która służy jako wzorzec do tworzenia obiektów tej klasy. Jak nazywa się ta metoda?

▢ Firma budowlana

▢ Instancjator

▢ konstruktor,

▢ Plan

Pytanie 2

Które z poniższych stwierdzeń dotyczących interfejsów i klas abstrakcyjnych jest NIEPRAWIDŁOWE?

▢ Klasy abstrakcyjne mogą mieć konstruktory.

▢ Interfejsy nie mogą mieć konstruktorów.

▢ Interfejsy i klasy abstrakcyjne można tworzyć bezpośrednio.

▢ Właściwości abstrakcyjne muszą być implementowane przez podklasy klasy abstrakcyjnej.

Pytanie 3

Który z tych modyfikatorów widoczności w języku Kotlin NIE jest przeznaczony dla właściwości, metod itp.?

▢ internal

▢ nosubclass

▢ protected

▢ private

Pytanie 4

Rozważmy tę klasę danych:
data class Fish(val name: String, val species:String, val colors:String)
Który z tych kodów NIE jest prawidłowy do utworzenia i dekonstrukcji obiektu Fish?

▢ val (name1, species1, colors1) = Fish("Pat", "Plecostomus", "gold")

▢ val (name2, _, colors2) = Fish("Bitey", "shark", "gray")

▢ val (name3, species3, _) = Fish("Amy", "angelfish", "blue and black stripes")

▢ val (name4, species4, colors4) = Fish("Harry", "halibut")

Pytanie 5

Załóżmy, że masz zoo z wieloma zwierzętami, które wymagają opieki. Która z tych czynności NIE należy do opieki nad dzieckiem?

▢ interface dla różnych rodzajów pokarmu, który jedzą zwierzęta.

▢ abstract Caretaker klasa, z której możesz utworzyć różne typy opiekunów;

▢ interface za podanie zwierzęciu czystej wody.

▢ data – zajęcia w harmonogramie karmienia.

Przejdź do następnej lekcji: 5.1 Rozszerzenia

Omówienie kursu, w tym linki do innych ćwiczeń, znajdziesz w artykule „Kotlin Bootcamp for Programmers: Welcome to the course” (w języku angielskim).