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()
obiektAquarium
. - 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()
. KlasaAquarium
może zaimplementować interfejsClean
, 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.
- W panelu Project (Projekt) w projekcie Hello Kotlin kliknij prawym przyciskiem myszy folder src.
- 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.
- Kliknij prawym przyciskiem myszy pakiet example.myapp.
- Wybierz New > Kotlin File / Class.
- W sekcji Rodzaj wybierz Klasa i nazwij zajęcia
Aquarium
. IntelliJ IDEA podaje nazwę pakietu w pliku i tworzy pustą klasęAquarium
. - W klasie
Aquarium
określ i zainicjuj właściwościvar
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()
.
- W okienku Project (Projekt) po lewej stronie kliknij prawym przyciskiem myszy pakiet example.myapp.
- Wybierz New > Kotlin File / Class.
- W menu Rodzaj zachowaj wybór Plik i nazwij plik
main.kt
. IntelliJ IDEA zawiera nazwę pakietu, ale nie zawiera definicji klasy pliku. - Zdefiniuj funkcję
buildAquarium()
, a wewnątrz utwórz instancjęAquarium
. Aby utworzyć instancję, odwołaj się do klasy tak, jakby była to funkcjaAquarium()
. Wywołuje konstruktor klasy i tworzy wystąpienie klasyAquarium
podobnie jaknew
w innych językach. - Zdefiniuj funkcję
main()
i wywołajbuildAquarium()
.
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium()
}
fun main() {
buildAquarium()
}
Krok 4. Dodaj metodę
- W klasie
Aquarium
dodaj metodę drukowania właściwości wymiaru oceanarium.
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
}
- W
main.kt
możesz za pomocą metodybuildAquarium()
wywołać metodęprintSize()
wmyAquarium
.
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
}
- Uruchom program, klikając zielony trójkąt obok funkcji
main()
. Obserwuj wyniki.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm
- 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()
}
- 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.
- W utworzonej wcześniej klasie
Aquarium
zmień definicję klasy, aby zawierała 3 parametry konstruktora z domyślnymi wartościamilength
,width
iheight
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
...
}
- 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
lubval
. 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) {
...
}
- 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 funkcjibuildAquarium()
wypróbuj różne sposoby tworzenia obiektówAquarium
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()
}
- 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
).
- W klasie
Aquarium
dodaj blokinit
, 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")
}
}
- 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.
- W klasie
Aquarium
dodaj dodatkowy konstruktor, w którym jako argument podano wartość rybną, używając słowa kluczowegoconstructor
. 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
}
- 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()
- 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")
}
- 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.
- W klasie
Aquarium
określ właściwośćInt
o nazwievolume
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
- Usuń blok
init
, który drukuje wolumin. - Usuń kod (
buildAquarium()
), który drukuje tom. - 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")
}
- 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.
- W klasie
Aquarium
zmieńvolume
navar
, aby można go było ustawić więcej niż raz. - 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 tovalue
, ale możesz ją zmienić.
var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
- 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()
}
- 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 atrybutprivate
, 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.
- Określ klasę
Aquarium
i wszystkie jej właściwości za pomocą słowa kluczowegoopen
.
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)
}
- Dodaj otwartą właściwość
shape
o wartości"rectangle"
.
open val shape = "rectangle"
- Dodaj otwartą właściwość
water
z metodą pobierającą, która zwraca 90% objętości woluminuAquarium
.
open var water: Double = 0.0
get() = volume * 0.9
- 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)")
}
- W
buildAquarium()
zmień kod, by utworzyćAquarium
zwidth = 25
,length = 25
iheight = 40
.
fun buildAquarium() {
val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
aquarium6.printSize()
}
- 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ę
- Utwórz podklasę
Aquarium
o nazwieTowerTank
, która ma zaokrąglony cylinder zamiast prostokątnego. Możesz dodać atrybutTowerTank
poniżejAquarium
, bo możesz dodać kolejną klasę w tym samym pliku co klasaAquarium
. - W
TowerTank
zastąp właściwośćheight
, która jest zdefiniowana w konstruktorze. Aby zastąpić właściwość, użyj słowa kluczowegooverride
w podklasie.
- Ustaw konstruktor elementu
TowerTank
jakodiameter
. Gdy wywołujesz konstruktor w klasie nadrzędnejAquarium
, używaj właściwościdiameter
zarówno dlalength
, jak iwidth
.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
- Zastąp właściwość woluminu, by obliczyć cylinder. Wzór walca to pi razy kwadrat promienia razy wysokość. Musisz zaimportować stałą
PI
zjava.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()
}
- W zasadzie
TowerTank
zastąp właściwośćwater
wartością 80% głośności.
override var water = volume * 0.8
- Zastąp wartość
shape
wartością"cylinder"
.
override val shape = "cylinder"
- 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"
}
- W
buildAquarium()
utwórzTowerTank
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()
}
- 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
- W sekcji example.myapp utwórz nowy plik,
AquariumFish.kt
. - Utwórz klasę o nazwie
AquariumFish
i oznacz ją jakoabstract
. - Dodaj jedną właściwość
String
(color
) i oznacz ją jakoabstract
.
package example.myapp
abstract class AquariumFish {
abstract val color: String
}
- Utwórz 2 podgrupy
AquariumFish
,Shark
iPlecostomus
. - Ponieważ ustawienie
color
jest abstrakcyjne, podklasy muszą je zaimplementować. Zmień kolorShark
na szary i złoto (Plecostomus
).
class Shark: AquariumFish() {
override val color = "gray"
}
class Plecostomus: AquariumFish() {
override val color = "gold"
}
- Aby przetestować zajęcia, utwórz w main.kt funkcję
makeFish()
. UtwórzShark
iPlecostomus
, a następnie wydrukuj kolory każdej z nich. - 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()
}
- Uruchom program i obserwuj wyniki.
⇒ Shark: gray Plecostomus: gold
Poniższy diagram przedstawia klasę Shark
i klasę Plecostomus
, które podklasy abstrakcyjne AquariumFish
.
Krok 2. Tworzenie interfejsu
- W witrynie AquariumFish.kt utwórz interfejs
FishAction
o nazwieeat()
.
interface FishAction {
fun eat()
}
- 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")
}
}
- W funkcji
makeFish()
, aby każdy z Twoich ryb jadł coś, wywołując użytkownikaeat()
.
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
shark.eat()
println("Plecostomus: ${pleco.color}")
pleco.eat()
}
- 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
naFishAction
i podać domyślną implementację dla etykietyeat
, pozostawiając jedynie abstrakcyjnycolor
, 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
- W AquariumFish.kt usuń klasę
AquariumFish
. Zamiast tego dziedziczemy z klasyAquariumFish
Plecostomus
iShark
implementują interfejsy zarówno dla czynności rybnych, jak i ich kolorów. - Utwórz nowy interfejs
FishColor
, który określa kolor jako ciąg znaków.
interface FishColor {
val color: String
}
- Zmień
Plecostomus
, aby zaimplementować 2 interfejsy:FishAction
iFishColor
. Musisz zastąpićcolor
(FishColor
) ieat()
(FishAction
).
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
- Zmień klasę
Shark
, aby zaimplementować też 2 interfejsy:FishAction
iFishColor
, zamiast dziedziczyć je z elementuAquariumFish
.
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
- 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.
- 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.
- W AquariumFish.kt usuń zastąpienie
color
zPlecostomus
. - Zmień klasę
Plecostomus
, aby pobierała kolor zGoldColor
. W tym celu dodaj deklarację klasy do wartościby GoldColor
, tworząc przekazywanie dostępu. Oznacza to, że zamiast implementacji funkcjiFishColor
użyj implementacji udostępnionej przezGoldColor
. Za każdym razem, gdy otwierany jest plikcolor
, jest on przekazywany doGoldColor
.
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
.
- Zmień klasę
Plecostomus
, aby wykorzystać klasę w ramach metodyfishColor
za pomocą jej konstruktora i ustawić wartość domyślną naGoldColor
. 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
.
- W aplikacji AquariumFish.kt utwórz klasę
PrintingFishAction
, która implementujeFishAction
, która pobieraString
,food
, a następnie drukuje, co zjadła ryba.
class PrintingFishAction(val food: String) : FishAction {
override fun eat() {
println(food)
}
}
- W klasie
Plecostomus
usuń funkcję zastępowaniaeat()
, bo zastąpisz ją przekazywaniem. - W deklaracji
Plecostomus
przekazano rolęFishAction
użytkownikowiPrintingFishAction
, przekazując"eat algae"
. - 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
- 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. - W pakiecie utwórz nową klasę o nazwie
Decoration
.
package example.myapp.decor
class Decoration {
}
- Aby nadać klasie
Decoration
klasę danych, poprzedź deklarację klasy słowem kluczowymdata
. - Dodaj właściwość
String
o nazwierocks
, aby dać klasom pewne dane.
data class Decoration(val rocks: String) {
}
- W pliku poza funkcją dodaj funkcję
makeDecorations()
, aby utworzyć i wydrukować instancjęDecoration
z"granite"
.
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
}
- 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)
- Utwórz w
makeDecorations()
2 kolejne obiektyDecoration
, 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)
}
- W
makeDecorations()
dodaj odbitkę z drukiem, która zawiera wynik porównaniadecoration1
zdecoration2
, i drugą z porównaniem wartościdecoration3
zdecoration2
. Użyj metody równes(), którą zapewnia klasy danych.
println (decoration1.equals(decoration2))
println (decoration3.equals(decoration2))
- 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ą.
- 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
.
- 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.
- 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ść nainternal
,private
lubprotected
. - 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
zamiastclass
. - 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.
- Klasy i dziedziczenie
- Konstruktorzy
- Funkcje fabryczne
- Właściwości i pola
- Modyfikatory widoczności
- Zajęcia abstrakcyjne
- Interfejsy
- Przekazywanie dostępu
- Klasy danych
- Równość
- Nękanie
- Deklaracje obiektów
- Klasa Enum
- Szczytowe zajęcia
- Obsługa opcjonalnych błędów przy użyciu klas zamkniętych Kotlin
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:
Omówienie kursu, w tym linki do innych ćwiczeń z programowania, znajdziesz w artykule "Kotlin Bootcamp for Programmers: Witamy na kursie."