Kotlin Botcamp dla programistów 4: programowanie zorientowane na obiekty

Te ćwiczenia są częścią kursu Kotlin dla programistów. Skorzystaj z tego kursu, jeśli będziesz wykonywać kolejno kilka ćwiczeń z programowania. W zależności od Twojej wiedzy możesz mieć dostęp do niektórych sekcji. Ten kurs jest przeznaczony dla programistów, którzy znają język zorientowany na obiekty i chcą się dowiedzieć Kotlina.

Wprowadzenie

W ramach tego ćwiczenia z programowania tworzysz program Kotlin i ucz się na nim klas i obiektów. Wiele z tych treści będzie Ci znajomych, jeśli znasz inny język zorientowany na obiekty, ale Kotlin różni się istotnymi elementami, które ograniczają ilość kodu do pisania. Znajdą się też też abstrakcyjne klasy i przekazywanie interfejsu.

Lekcje nie obejmują jednej przykładowej aplikacji, ale mają one na celu pogłębianie wiedzy. Między innymi należy jednak pamiętać o zależności od siebie. W połączeniu wiele z przykładów wykorzystuje motyw akwariowy. A jeśli chcesz zobaczyć pełne informacje o oceanarium, zapoznaj się z kursem Kotlin Bootcamp for Programmers (Ukocity) na platformie Udacity.

Co musisz wiedzieć

  • Podstawowe informacje o Kotlinie, w tym typy, operatory i pętle
  • Składnia funkcji Kotlin
  • Podstawy programowania zorientowanego na obiekty
  • podstawowe elementy IDE, takie jak IntelliJ IDEA czy Android Studio;

Czego się nauczysz

  • Jak tworzyć klasy i korzystać z właściwości w Kotlinie
  • Tworzenie i używanie konstruktorów klas w Kotlinie
  • Jak utworzyć podklasę i jak działa dziedziczenie
  • Informacje o abstrakcyjnych klasach, interfejsach i przekazywaniu interfejsu
  • Tworzenie i używanie klas danych
  • Jak używać singli, enum i zajęć klasy

Jakie zadania wykonasz:

  • Tworzenie klasy za pomocą właściwości
  • Tworzenie konstruktora dla klasy
  • Tworzenie podkategorii
  • Zapoznaj się z przykładami abstrakcyjnych klas i interfejsów
  • Utwórz prostą klasę danych
  • Więcej informacji o pojedynczych kształtach, wyliczeniach i zajęciach

Zapoznaj się z poniższymi warunkami programowania:

  • Klasy to plany obiektów. Na przykład klasa Aquarium to plan tworzenia obiektu oceanarium.
  • Obiekty to wystąpienia klas. Obiekt akwarium to jeden rzeczywisty Aquarium.
  • Właściwości to cechy klas, takie jak długość, szerokość i wysokość elementu Aquarium.
  • Metody określane też jako funkcje członków to funkcje klasy. Metody to te, które możesz wykonywać za pomocą obiektu. Na przykład możesz fillWithWater() obiekt Aquarium.
  • Interfejs to specyfikacja, którą może implementować klasa. Na przykład czyszczenie jest powszechne w przypadku przedmiotów innych niż akwaria, a czyszczenie zazwyczaj odbywa się w podobny sposób w przypadku różnych obiektów. Dzięki temu możesz mieć interfejs o nazwie Clean, który określa metodę clean(). Klasa Aquarium może zaimplementować interfejs Clean, aby wyczyścić akwarium za pomocą miękkiej gąbki.
  • Pakiety to sposób grupowania powiązanego kodu, by zachować porządek lub utworzyć bibliotekę kodu. Po utworzeniu pakietu możesz zaimportować jego zawartość do innego pliku i ponownie użyć znajdujących się w nim kodu oraz klas.

W tym zadaniu utworzysz nowy pakiet oraz klasę z niektórymi właściwościami i metodą.

Krok 1. Utwórz pakiet

Pakiety ułatwiają utrzymanie porządku w kodzie.

  1. W panelu Project (Projekt) w projekcie Hello Kotlin kliknij prawym przyciskiem myszy folder src.
  2. Wybierz Nowy > pakiet i nazwij go example.myapp.

Krok 2. Utwórz zajęcia z właściwościami

Klasy są określane słowem kluczowym class, a nazwy klas zaczynają się od dużej litery.

  1. Kliknij prawym przyciskiem myszy pakiet example.myapp.
  2. Wybierz New > Kotlin File / Class.
  3. W sekcji Rodzaj wybierz Klasa i nazwij zajęcia Aquarium. IntelliJ IDEA podaje nazwę pakietu w pliku i tworzy pustą klasę Aquarium.
  4. W klasie Aquarium określ i zainicjuj właściwości var określające szerokość, wysokość i długość. Zainicjuj właściwości domyślnymi wartościami.
package example.myapp

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

Kotlin automatycznie tworzy metody getter i setery dla właściwości zdefiniowanych w klasie Aquarium. Dzięki temu masz bezpośredni dostęp do właściwości, na przykład myAquarium.length.

Krok 3. Utwórz funkcję main()

Utwórz nowy plik o nazwie main.kt, aby umieścić funkcję main().

  1. W okienku Project (Projekt) po lewej stronie kliknij prawym przyciskiem myszy pakiet example.myapp.
  2. Wybierz New > Kotlin File / Class.
  3. W menu Rodzaj zachowaj wybór Plik i nazwij plik main.kt. IntelliJ IDEA zawiera nazwę pakietu, ale nie zawiera definicji klasy pliku.
  4. Zdefiniuj funkcję buildAquarium(), a wewnątrz utwórz instancję Aquarium. Aby utworzyć instancję, odwołaj się do klasy tak, jakby była to funkcja Aquarium(). Wywołuje konstruktor klasy i tworzy wystąpienie klasy Aquarium podobnie jak new w innych językach.
  5. Zdefiniuj funkcję main() i wywołaj 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 wymiaru oceanarium.
    fun printSize() {
        println("Width: $width cm " +
                "Length: $length cm " +
                "Height: $height cm ")
    }
  1. W main.kt możesz za pomocą metody buildAquarium() wywołać metodę printSize() w myAquarium.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
}
  1. Uruchom program, klikając zielony trójkąt obok funkcji main(). Obserwuj wyniki.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
  1. W narzędziu buildAquarium() dodaj kod, by ustawić wysokość na 60, i wydrukuj zmienione właściwości wymiaru.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
    myAquarium.height = 60
    myAquarium.printSize()
}
  1. Uruchom program i obserwuj wyniki.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 100 cm Height: 60 cm 

W tym zadaniu utworzysz konstruktor dla 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 każde wystąpienie ciągu Aquarium jest tworzone z tymi samymi wymiarami. Wymiary możesz zmienić po ich utworzeniu, ustawiając ich właściwości, ale prościej jest utworzyć odpowiedni rozmiar na początek.

W niektórych językach programowania konstruktor definiuje się, tworząc w ramach klasy metodę o takiej samej nazwie jak klasa. W Kotlin definiujesz konstruktor bezpośrednio w deklaracji klasy, określając parametry w nawiasach tak, jakby klasa była metodą. Podobnie jak w przypadku funkcji w Kotlinie, parametry te mogą zawierać wartości domyślne.

  1. W utworzonej wcześniej klasie Aquarium zmień definicję klasy, aby zawierała 3 parametry konstruktora z domyślnymi wartościami length, width i height oraz 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. W przypadku bardziej kompaktowego sposobu Kotlin można bezpośrednio definiować właściwości za pomocą konstruktora za pomocą właściwości var lub val. Kotlin automatycznie tworzy też gettery i setery. 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 z tym konstruktorem, możesz określić brak argumentów i uzyskać wartości domyślne lub wskazać tylko niektóre z nich albo określić wszystkie i utworzyć Aquarium o pełnym rozmiarze niestandardowym. W funkcji buildAquarium() wypróbuj różne sposoby tworzenia obiektów 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 obserwuj 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 

Zauważ, że w każdym z tych przypadków nie musisz przeciążać konstruktora i zapisać osobnej wersji (i kilku innych elementów w przypadku pozostałych kombinacji). Kotlin tworzy potrzebne elementy na podstawie wartości domyślnych i parametrów nazwanych.

Krok 2. Dodaj bloki inicjowania

Przykładowe konstruktory powyżej deklarują właściwości i przypisują do nich wartość wyrażenia. Jeśli konstruktor potrzebuje więcej kodu inicjowania, można go umieścić w co najmniej jednym bloku init. W tym kroku dodasz do tablicy Aquarium niektóre bloki (init).

  1. W klasie Aquarium dodaj blok init, aby wydrukować inicjowany obiekt, i drugi, aby drukować głośność 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 obserwuj 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 

Blokady init są wykonywane w kolejności, w jakiej występują w definicji klasy, a wszystkie są wykonywane po wywołaniu konstruktora.

Krok 3. Informacje o konstruktorach dodatkowych

Podczas tego kroku dowiesz się więcej o konstruktorach dodatkowych i dodasz je do swojej klasy. Oprócz podstawowego konstruktora, który może mieć co najmniej jeden blok init, klasa Kotlin może mieć również jeden lub więcej konstruktorów dodatkowych, aby umożliwić przeciążenie konstruktora, czyli konstruktory z różnymi argumentami.

  1. W klasie Aquarium dodaj dodatkowy konstruktor, w którym jako argument podano wartość rybną, używając słowa kluczowego constructor. Utwórz właściwość zbiornika (val) na potrzeby obliczania objętości akwarium w litrach na podstawie liczby ryb. Załóżmy,że na każdą rybę przypada 2000 cm wody i dodatkowo trochę dodatkowego miejsca, żeby woda nie wyciekał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 obliczyć wysokość potrzebną do obsługi 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 wyniki.
⇒ aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

Zauważ, że wolumin jest drukowany 2 razy – raz za pomocą bloku init w konstruktorze podstawowym, a nie przez kod w buildAquarium().

Słowo kluczowe constructor mogło również występować w podstawowym konstruktorze, ale w większości przypadków nie jest to konieczne.

Krok 4. Dodaj nowy moduł pobierania właściwości

W tym kroku dodasz jednoznaczny obiekt getter właściwości. Kotlin automatycznie definiuje elementy pobierające i ustawiające, gdy definiujesz właściwości, ale czasami wartość właściwości musi zostać dostosowana lub obliczona. Na przykład powyżej wydrukowano numer Aquarium. Możesz udostępnić wolumin jako właściwość, definiując zmienną i getter. Ponieważ funkcja volume musi zostać obliczona, metoda pobierająca musi zwrócić obliczoną wartość. Można to zrobić za pomocą funkcji jednowierszowej.

  1. W klasie Aquarium określ właściwość Int o nazwie volume i zdefiniuj metodę get(), która oblicza objętość w następnym wierszu.
val volume: Int
    get() = width * height * length / 1000  // 1000 cm^3 = 1 l
  1. Usuń blok init, który drukuje wolumin.
  2. Usuń kod (buildAquarium()), który drukuje tom.
  3. W metodzie printSize() dodaj wiersz, aby wydrukować wolumin.
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 wyniki.
⇒ 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 wolumin jest drukowany tylko raz po całkowitym zainicjowaniu obiektu przez konstruktor podstawowy i dodatkowy.

Krok 5. Dodaj ustawienie właściwości

W tym kroku utworzysz nowe ustawienie właściwości woluminu.

  1. W klasie Aquarium zmień volume na var, aby można go było ustawić więcej niż raz.
  2. Dodaj element ustawiający dla właściwości volume, dodając metodę set() poniżej metody 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. W polu buildAquarium() dodaj kod, aby ustawić głośność akwarium na 70 litrów. Wydrukuj nowy rozmiar.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    aquarium6.volume = 70
    aquarium6.printSize()
}
  1. Uruchom ponownie program i obserwuj zmiany wysokości i liczby.
⇒ 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. Wszystko to jest domyślnie, ponieważ wszystko w Kotlinie jest publiczne, co oznacza, że dostęp do wszystkiego, w tym do klas, metod, właściwości i zmiennych członków, jest możliwy wszędzie.

W Kotlin klasy, obiekty, interfejsy, konstruktory, funkcje, właściwości i ich elementy ustawić mogą mieć modyfikatory widoczności:

  • Wartość public jest widoczna 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, np. biblioteka lub aplikacja.
  • Wartość private oznacza, że dokument będzie widoczny w danej klasie (lub w pliku źródłowym, jeśli korzystasz z jego funkcji).
  • Element protected jest taki sam jak atrybut private, ale jest też widoczny dla wszystkich podkategorii.

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

Zmienne członkostwa

Właściwości w klasie lub zmiennych użytkownika mają domyślnie wartość public. Jeśli zdefiniujesz je za pomocą atrybutu var, będą one zmienne, tzn. będą czytelne i dostępne do zapisu. Jeśli je zdefiniujesz w polu val, po zainicjowaniu są one tylko do odczytu.

Jeśli chcesz, by kod miał możliwość odczytu lub zapisu, ale zewnętrzny kod może tylko odczytywać, możesz pozostawić właściwość i jej obiekt pobierający jako publiczny oraz zadeklarować element ustawiający jako prywatny (jak pokazano poniżej).

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

W tym zadaniu dowiesz się, jak działają podklasy i dziedziczenie w Kotlinie. Są podobne do tych, które znasz z innych języków, ale występują pewne różnice.

W Kotlinie domyślnie nie można dodawać klas. Podobnie klasy usług i członków grupy nie mogą być zastąpione przez podklasy (chociaż można do nich uzyskać dostęp).

Aby można było przypisać klasę do klasy, musisz ją oznaczyć jako open. Podobnie właściwości i zmienne użytkowników należy oznaczyć jako open, aby zastąpić je w klasie podrzędnej. Słowo kluczowe open jest wymagane, aby zapobiec przypadkowemu wyciekowi szczegółów implementacji w ramach interfejsu zajęć.

Krok 1. Otwórz zajęcia w oceanarium

W tym kroku utworzysz klasę Aquarium open, którą możesz zastąpić w następnym kroku.

  1. Określ klasę Aquarium i wszystkie jej właściwości za pomocą słowa kluczowego 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 o wartości "rectangle".
   open val shape = "rectangle"
  1. Dodaj otwartą właściwość water z metodą pobierającą, która zwraca 90% objętości woluminu Aquarium.
    open var water: Double = 0.0
        get() = volume * 0.9
  1. Dodaj kod do metody printSize(), by wydrukować kształt, oraz 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. W buildAquarium() zmień kod, by utworzyć Aquarium z width = 25, length = 25 i height = 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. Utwórz podklasę

  1. Utwórz podklasę Aquarium o nazwie TowerTank, która ma zaokrąglony cylinder zamiast prostokątnego. Możesz dodać atrybut TowerTank poniżej Aquarium, bo możesz dodać kolejną klasę w tym samym pliku co klasa Aquarium.
  2. W TowerTank zastąp właściwość height, która jest zdefiniowana w konstruktorze. Aby zastąpić właściwość, użyj słowa kluczowego override w podklasie.
  1. Ustaw konstruktor elementu TowerTank jako diameter. Gdy wywołujesz konstruktor w klasie nadrzędnej Aquarium, używaj właściwości diameter zarówno dla length, jak i width.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. Zastąp właściwość woluminu, by obliczyć cylinder. Wzór walca to pi razy kwadrat promienia razy wysokość. Musisz zaimportować stałą PI z java.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. W zasadzie 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. Końcowa klasa TowerTank powinna wyglądać podobnie do poniższego kodu.

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 wyniki.
⇒ 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)

Czasem chcesz określić typowe zachowania lub właściwości, które mają być wspólne dla niektórych powiązanych klas. Kotlin oferuje dwie metody: interfejsy i zajęcia abstrakcyjne. W tym zadaniu tworzysz abstrakcyjną klasę AquariumFish dla właściwości, które są typowe dla wszystkich ryb. Tworzysz interfejs o nazwie FishAction, który określa zachowanie typowe dla wszystkich ryb.

  • Ani abstrakcyjna klasa, ani interfejs nie mogą być tworzone samodzielnie, co oznacza, że nie możesz bezpośrednio tworzyć obiektów tego typu.
  • Klasy abstrakcyjne zawierają konstruktory.
  • Interfejsy nie mogą zawierać żadnego konstruktora ani zapisywać żadnych stanów.

Krok 1. Tworzenie abstrakcyjnej klasy

  1. W sekcji example.myapp utwórz nowy plik, AquariumFish.kt.
  2. Utwórz klasę o nazwie AquariumFish i oznacz ją jako abstract.
  3. Dodaj jedną właściwość String (color) i oznacz ją jako abstract.
package example.myapp

abstract class AquariumFish {
    abstract val color: String
}
  1. Utwórz 2 podgrupy AquariumFish, Shark i Plecostomus.
  2. Ponieważ ustawienie color jest abstrakcyjne, podklasy muszą je zaimplementować. Zmień kolor Shark na szary i złoto (Plecostomus).
class Shark: AquariumFish() {
    override val color = "gray"
}

class Plecostomus: AquariumFish() {
    override val color = "gold"
}
  1. Aby przetestować zajęcia, utwórz w main.kt funkcję makeFish(). Utwórz Shark i Plecostomus, a następnie wydrukuj kolory każdej z nich.
  2. Usuń poprzedni kod testowy w: main() i dodaj połączenie do: makeFish(). Twój kod powinien wyglądać podobnie do tego 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 wyniki.
⇒ Shark: gray 
Plecostomus: gold

Poniższy diagram przedstawia klasę Shark i klasę Plecostomus, które podklasy abstrakcyjne AquariumFish.

Schemat przedstawiający abstrakcyjną klasę, OceanFish oraz 2 podgrupy: Shark i Plecostumus.

Krok 2. Tworzenie interfejsu

  1. W witrynie AquariumFish.kt utwórz interfejs FishAction o nazwie eat().
interface FishAction  {
    fun eat()
}
  1. Dodaj atrybut FishAction do każdej z podkategorii i zaimplementuj właściwość eat(), tak aby obsługiwała ryby.
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(), aby każdy z Twoich ryb jadł coś, wywołując użytkownika 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 wyniki.
⇒ Shark: gray
hunt and eat fish
Plecostomus: gold
eat algae

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

Kiedy używać abstrakcyjnych klas a interfejsów

Te przykłady są proste, ale jeżeli masz wiele powiązanych zajęć, abstrakcyjne klasy i interfejsy pomogą Ci zadbać o czystość, porządek i łatwość konserwacji.

Jak wspomniano powyżej, abstrakcyjne klasy mogą mieć konstruktory i interfejsy nie mogą, ale poza tym są bardzo podobne. Kiedy należy ich używać?

Gdy tworzysz zajęcia w interfejsach, zawarte w nich funkcje są rozszerzone przez umieszczone w nich wystąpienia. Kompozycja zazwyczaj sprawia, że kod jest łatwiejszy do ponownego wykorzystania i jego przyczyna niż dziedziczenie z abstrakcyjnej klasy. Na zajęciach możesz też używać kilku interfejsów, ale możesz wybrać tylko jedną klasę abstrakcyjną.

Kompozycja często prowadzi do lepszej kapsuły, obniżenia parowania (zależności), bardziej przejrzystych interfejsów i bardziej użytecznego kodu. Z tego powodu preferowana jest kompozycja z interfejsami. Z drugiej strony dziedziczenie z abstrakcyjnej klasy jest naturalnym rozwiązaniem w przypadku niektórych problemów. Zalecamy więc komponowanie, ale kiedy dziedziczenie na to pozwala, Kotlin również na to pozwala.

  • Użyj interfejsu, jeśli masz dużo metod i 1 lub 2 domyślne implementacje, na przykład AquariumAction poniżej.
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • Jeśli nie możesz ukończyć zajęć, użyj ich abstrakcyjnych. Jeśli na przykład wrócisz do klasy AquariumFish, możesz zmienić konfigurację wszystkich urządzeń AquariumFish na FishAction i podać domyślną implementację dla etykiety eat, pozostawiając jedynie abstrakcyjny color, ponieważ nie jest to domyślny kolor ryb.
interface FishAction  {
    fun eat()
}

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

Poprzednie zadanie dotyczyło abstrakcyjnych klas, interfejsów oraz koncepcji kompozycji. Przekazywanie dostępu do interfejsu to zaawansowana technika, w której metody interfejsu są implementowane przez obiekt pomocniczy (lub przedstawiciela), który jest następnie używany przez klasę. Ta metoda może być przydatna, gdy korzystasz z interfejsu w seriach klas, które nie są ze sobą powiązane. Dodajesz niezbędną funkcję do osobnej klasy pomocniczej, a każda z nich używa do tego celu klasy pomocniczej.

W tym zadaniu dodasz funkcje klasy do przekazywania dostępu do interfejsu.

Krok 1. Utwórz nowy interfejs

  1. W AquariumFish.kt usuń klasę AquariumFish. Zamiast tego dziedziczemy z klasy AquariumFish Plecostomus i Shark implementują interfejsy zarówno dla czynności rybnych, jak i ich kolorów.
  2. Utwórz nowy interfejs FishColor, który określa kolor jako ciąg znaków.
interface FishColor {
    val color: String
}
  1. Zmień Plecostomus, aby zaimplementować 2 interfejsy: FishAction i FishColor. Musisz zastąpić color (FishColor) i eat() (FishAction).
class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. Zmień klasę Shark, aby zaimplementować też 2 interfejsy: FishAction i FishColor, zamiast dziedziczyć je z elementu 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 zajęcia pojedynczej

Następnie implementujesz konfigurację przekazywania dostępu, tworząc klasę pomocniczą implementującą FishColor. Tworzysz podstawową klasę o nazwie GoldColor, która ma funkcję FishColor – wystarczy, że podasz kolor jej złotym kolorem.

Utworzenie wielu wystąpień elementu GoldColor nie ma sensu, ponieważ wszystkie one działają dokładnie tak samo. Kotlin umożliwia więc zadeklarowanie klasy, gdzie możesz utworzyć tylko jedną jej instancję, używając słowa kluczowego object zamiast class. Kotlin utworzy tę instancję, do której odwołuje się nazwa klasy. Wtedy wszystkie inne obiekty będą mogły używać tej instancji – nie będzie można utworzyć innych instancji tej klasy. Jeśli znasz wzór jednotonowy, możesz go wdrożyć w Kotlinie w ten sposób.

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

Krok 3. Dodaj przekazywanie dostępu do interfejsu w FishColor

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

  1. W AquariumFish.kt usuń zastąpienie color z Plecostomus.
  2. Zmień klasę Plecostomus, aby pobierała kolor z GoldColor. W tym celu dodaj deklarację klasy do wartości by GoldColor, tworząc przekazywanie dostępu. Oznacza to, że zamiast implementacji funkcji FishColor użyj implementacji udostępnionej przez GoldColor. Za każdym razem, gdy otwierany jest plik color, jest on przekazywany do GoldColor.
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

Zupełnie niezwykła klasa ma złoto, ale te ryby występują w wielu kolorach. Możesz rozwiązać ten problem, dodając parametr konstruktor dla koloru GoldColor z domyślnym kolorem Plecostomus.

  1. Zmień klasę Plecostomus, aby wykorzystać klasę w ramach metody fishColor za pomocą jej konstruktora i ustawić wartość domyślną na GoldColor. Zmień przekazywanie dostępu z: by GoldColor na: by fishColor.
class Plecostomus(fishColor: FishColor = GoldColor):  FishAction,
       FishColor by fishColor {
   override fun eat() {
       println("eat algae")
   }
}

Krok 4. Dodaj przekazywanie dostępu do interfejsu FishAction

W podobny sposób możesz używać przekazywania dostępu do interfejsu dla interfejsu FishAction.

  1. W aplikacji AquariumFish.kt utwórz klasę PrintingFishAction, która implementuje FishAction, która pobiera String, food, a następnie drukuje, co zjadła ryba.
class PrintingFishAction(val food: String) : FishAction {
    override fun eat() {
        println(food)
    }
}
  1. W klasie Plecostomus usuń funkcję zastępowania eat(), bo zastąpisz ją przekazywaniem.
  2. W deklaracji Plecostomus przekazano rolę FishAction użytkownikowi PrintingFishAction, przekazując "eat algae".
  3. Przy takim przekazywaniu nie ma żadnego kodu w treści klasy Plecostomus, więc usuń {}, bo wszystkie zastąpienia są obsługiwane przez przekazywanie dostępu do interfejsu.
class Plecostomus (fishColor: FishColor = GoldColor):
        FishAction by PrintingFishAction("eat algae"),
        FishColor by fishColor

Poniższy diagram przedstawia klasy Shark i Plecostomus, które składają się z interfejsów PrintingFishAction i FishColor, ale przekazujesz im wdrożenie.

Przekazywanie dostępu do interfejsu jest bardzo skuteczne i zastanów się, jak z niej korzystać, gdy używasz abstrakcyjnej klasy w innym języku. Dzięki niemu możesz korzystać z kompozycji, aby podłączać się do zachowań, nie wymagając przy tym używania wielu podkategorii.

Klasa danych jest podobna do struct w innych językach – istnieje ona głównie do przechowywania niektórych danych, ale obiekt klasy danych nadal jest obiektem. Obiekty klas danych Kotlin mają dodatkowe zalety, np. narzędzia do drukowania i kopiowania. W tym zadaniu utworzysz prostą klasę danych i dowiesz się, jaką pomoc dla klas danych zapewnia Kotlin.

Krok 1. Utwórz klasę danych

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

class Decoration {
}
  1. Aby nadać klasie Decoration klasę danych, poprzedź deklarację klasy słowem kluczowym data.
  2. Dodaj właściwość String o nazwie rocks, aby dać klasom pewne dane.
data class Decoration(val rocks: String) {
}
  1. W pliku poza funkcją dodaj funkcję makeDecorations(), aby utworzyć i wydrukować instancję Decoration z "granite".
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. Dodaj funkcję main(), by wywołać makeDecorations() i uruchom swój program. Zauważ rozsądne dane wyjściowe, które są tworzone, ponieważ jest to klasa danych.
⇒ Decoration(rocks=granite)
  1. Utwórz w makeDecorations() 2 kolejne obiekty Decoration, które są &"Slate" i wydrukuj je.
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)

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

    val decoration3 = Decoration("slate")
    println(decoration3)
}
  1. W makeDecorations() dodaj odbitkę z drukiem, która zawiera wynik porównania decoration1 z decoration2, i drugą z porównaniem wartości decoration3 z decoration2. Użyj metody równes(), którą zapewnia 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. Destrukcja

Aby uzyskać właściwości obiektu danych i przypisać je do zmiennych, możesz je przypisywać pojedynczo.

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

Nazywamy to niszczeniem. Liczba zmiennych powinna odpowiadać liczbie 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ć na stronie decorationion.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 co najmniej jednej właściwości, możesz je pominąć, używając _ zamiast nazwy zmiennej, jak pokazano w poniższym kodzie.

    val (rock, _, diver) = d5

W tym zadaniu poznasz specjalne zajęcia w Kotlinie, takie jak:

  • Zajęcia dla singtonów
  • Wartości w polu enum
  • Zajęcia zamknięte

Krok 1. Możliwość wycofania klas jednotonowych

Przypomnij poprzedni przykład klasy GoldColor.

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

Każde wystąpienie elementu GoldColor działa tak samo, dlatego zadeklarowano je jako object, a nie class, Może być tylko jedno jego wystąpienie.

Krok 2. Utwórz wyliczenie

Kotlin obsługuje też wyliczenia, które pozwalają wyliczyć i odwoływać się do nazwy, podobnie jak w przypadku innych języków. Zadeklaruj wyliczenie, umieszczając w prefiksie słowo kluczowe enum. Podstawowa deklaracja wyliczenia wymaga tylko listy nazw, ale możesz również zdefiniować jedno lub więcej pól powiązanych z każdą nazwą.

  1. W pliku decorationion.kt użyj przykładu wyliczenia.
enum class Color(val rgb: Int) {
   RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}

Wartość w liczbie jest nieco podobna do jedynków – w wynikach może znajdować się tylko jedna wartość i jedna wartość. Pojedynczy może być np. tylko Color.RED, Color.GREEN i Color.BLUE. W tym przykładzie wartości RGB są przypisywane do właściwości rgb, która reprezentuje komponenty kolorów. Możesz też uzyskać wartość porządkową wyliczeniową za pomocą właściwości ordinal, a jej nazwę – za pomocą właściwości name.

  1. Wypróbuj inny wyliczenie.
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. Utwórz zapieczętowane zajęcia

Klasa Seave może być podklasami, które są podklasy, ale tylko wewnątrz pliku, w którym są zadeklarowane. Jeśli spróbujesz przypisać zajęcia do innego pliku, wystąpi błąd.

Ponieważ podkategorie są w tym samym pliku, Kotlin będzie statycznie znać wszystkie podgrupy. Oznacza to, że w momencie kompilacji kompilator widzi wszystkie zajęcia i podklasy i wie, że to wszystkie, więc kompilator może przeprowadzić dla Ciebie dodatkowe testy.

  1. Na stronie AquariumFish.kt znajdziesz przykład zajęć zamkniętych na wodzie.
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()

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

Klasa Seal nie może zostać zaklasyfikowana w innym pliku. Jeśli chcesz dodać więcej typów Seal, musisz dodać je w tym samym pliku. Dzięki temu zajęcia są stabilne i reprezentują stałą liczbę typów. Szczególnie przydatne mogą być te zamknięte zajęcia, które pozwalają zwrócić błąd lub błąd interfejsu API sieci.

Ta lekcja poruszała wiele zagadnień. Choć większość z nich powinna być znana z innych języków programowania zorientowanego na obiekty, Kotlin dodaje pewne funkcje, aby kod był zwięzły i czytelny.

Klasy i konstruktory

  • Określ klasę w Kotlinie za pomocą class.
  • Kotlin automatycznie tworzy elementy ustawiające i pobierające właściwości.
  • Zdefiniuj główny konstruktor bezpośrednio w definicji klasy. Przykład:
    class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
  • Jeśli główny konstruktor potrzebuje dodatkowego kodu, wpisz go w co najmniej jednym bloku init.
  • Klasa może zawierać jeden lub więcej konstruktorów dodatkowych za pomocą właściwości constructor, ale styl Kotlin zamiast tego używa funkcji fabrycznej.

Modyfikatory widoczności i podkategorie

  • Wszystkie klasy i funkcje w Kotlin są domyślnie public, ale możesz użyć modyfikatorów, aby zmienić widoczność na internal, private lub protected.
  • Aby można było utworzyć klasę podrzędną, 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 w klasie nadrzędnej open.
  • Zapieczętowanych klas można użyć tylko w tym samym pliku, w którym zostały zdefiniowane. Utwórz zapieczętowaną klasę, dodając prefiks sealed do przedrostka.

Klasy danych, pojedyncze tony i enum

  • Aby utworzyć klasę danych, poprzedź deklarację prefiksem data.
  • Destrukcja to skrócony sposób na przypisywanie właściwości obiektu data do osobnych zmiennych.
  • Utwórz klasę pojedynczego użytku, używając object zamiast class.
  • Określ wyliczenie za pomocą enum class.

Abstrakcyjne klasy, interfejsy i przekazywanie dostępu

  • Streszczenia i interfejsy to 2 sposoby udostępniania wspólnych zachowań pomiędzy zajęciami.
  • Klasa abstrakcyjna określa właściwości i zachowanie, ale pozostawia implementację w podklasach.
  • Interfejs definiuje zachowanie i może udostępniać domyślne implementacje w przypadku niektórych lub wszystkich zachowań.
  • Gdy tworzysz zajęcia w interfejsach, zawarte w nich funkcje są rozszerzone przez umieszczone w nich wystąpienia.
  • Przekazywanie dostępu do interfejsu używa kompozycji, ale jednocześnie przekazuje wdrożenie do klas interfejsu.
  • Kompozycja to skuteczny sposób dodawania funkcji do klasy za pomocą przekazywania dostępu do interfejsu. Ogólnie preferowana jest kompozycja, ale w przypadku niektórych problemów lepszym rozwiązaniem jest dziedziczenie z abstrakcyjnej klasy.

Dokumentacja Kotlin

Jeśli chcesz dowiedzieć się więcej na dowolny temat dotyczący tego kursu lub nie wiesz, co zrobić, wejdź na https://kotlinlang.org.

Samouczki Kotlin

Na stronie https://try.kotlinlang.org znajdziesz szczegółowe samouczki o nazwie Kotlin Koans, internetowe narzędzie do tłumaczenia, a także kompletny zestaw dokumentacji referencyjnej z przykładami.

Kurs Udacity

Kurs Udacity na ten temat znajdziesz na kursie Kotlin dla programistów.

IntelLIJ IDEA

Dokumentację IntelliJ IDEA znajdziesz na stronie JetBrains.

Ta sekcja zawiera listę możliwych zadań domowych dla uczniów, którzy pracują w ramach tego ćwiczenia w ramach kursu prowadzonego przez nauczyciela. To nauczyciel może wykonać te czynności:

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

Nauczyciele mogą wykorzystać te sugestie tak długo, jak chcą lub chcą, i mogą przypisać dowolne zadanie domowe.

Jeśli samodzielnie wykonujesz te ćwiczenia z programowania, możesz sprawdzić swoją wiedzę w tych zadaniach domowych.

Odpowiedz na te pytania

Pytanie 1

Klasy mają specjalną metodę tworzenia obiektów w tej klasie. Jak nazywa się ta metoda?

▢ Kreator

▢ Utworzenie instancji

▢ Konstruktor

▢ Plan

Pytanie 2

Które z tych stwierdzeń na temat interfejsów i abstrakcyjnych klas NIE jest poprawne?

▢ Streszczenia mogą zawierać konstruktory.

▢ Interfejsy nie mogą zawierać konstrukcji.

▢ Interfejsy i klasy abstrakcyjne mogą być tworzone bezpośrednio.

▢ abstrakcyjne właściwości muszą być zaimplementowane przez podgrupy klas abstrakcyjnych.

Pytanie 3

Który z tych elementów NIE jest modyfikatorem widoczności Kotlin dla właściwości, metod itp.?

internal

nosubclass

protected

private

Pytanie 4

Rozważ tę klasę danych:
data class Fish(val name: String, val species:String, val colors:String)
Który z poniższych NIE jest prawidłowym kodem do tworzenia i niszczenia 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

Powiedzmy, że masz zoo z wieloma gatunkami zwierząt, którymi trzeba się zająć. Która z poniższych opcji NIE jest częścią wdrażania opieki zdrowotnej?

interface dla różnych rodzajów pokarmu zwierząt

▢ Klasa abstract Caretaker, z której możesz tworzyć różne rodzaje opiekunów.

interface – dostarcza zwierzętom czystą wodę.

▢ Klasa data wpisu w harmonogramie przesyłania.

Przejdź do następnej lekcji: 5.1 Rozszerzenia

Omówienie kursu, w tym linki do innych ćwiczeń z programowania, znajdziesz w artykule "Kotlin Bootcamp for Programmers: Witamy na kursie."