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()
obiektAquarium
. - 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()
. KlasaAquarium
może implementować interfejsClean
, 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.
- W panelu Project (Projekt) w projekcie Hello Kotlin kliknij prawym przyciskiem myszy folder src.
- 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.
- Kliknij prawym przyciskiem myszy pakiet example.myapp.
- Wybierz New > Kotlin File / Class (Nowy > Plik/klasa Kotlin).
- W sekcji Rodzaj wybierz Zajęcia i nadaj zajęciom nazwę
Aquarium
. IntelliJ IDEA umieszcza nazwę pakietu w pliku i tworzy pustą klasęAquarium
. - W klasie
Aquarium
zdefiniuj i zainicjuj właściwościvar
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()
.
- W panelu Project po lewej stronie kliknij prawym przyciskiem myszy pakiet example.myapp.
- Wybierz New > Kotlin File / Class (Nowy > Plik/klasa Kotlin).
- 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. - 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ę klasyAquarium
, podobnie jak użycienew
w innych językach. - Zdefiniuj funkcję
main()
i wywołaj funkcjębuildAquarium()
.
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 wymiarów akwarium.
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
}
- W
main.kt
wbuildAquarium()
wywołaj metodęprintSize()
wmyAquarium
.
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
}
- Uruchom program, klikając zielony trójkąt obok funkcji
main()
. Sprawdź wynik.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm
- W
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()
}
- 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.
- W utworzonej wcześniej klasie
Aquarium
zmień definicję klasy, aby zawierała 3 parametry konstruktora z wartościami domyślnymi dlalength
,width
iheight
, 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
...
}
- Bardziej zwięzły sposób w Kotlinie polega na zdefiniowaniu właściwości bezpośrednio w konstruktorze za pomocą
var
lubval
. 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) {
...
}
- 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ć obiektAquarium
o całkowicie niestandardowym rozmiarze. W funkcjibuildAquarium()
wypróbuj różne sposoby tworzenia obiektuAquarium
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 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
.
- W klasie
Aquarium
dodaj blokinit
, 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")
}
}
- 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.
- W klasie
Aquarium
dodaj konstruktor dodatkowy, który przyjmuje liczbę ryb jako argument, używając słowa kluczowegoconstructor
. 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
}
- 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()
- 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 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.
- W klasie
Aquarium
zdefiniuj właściwośćInt
o nazwievolume
, 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
- Usuń blok
init
, który wyświetla głośność. - Usuń kod w
buildAquarium()
, który wyświetla głośność. - 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")
}
- 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.
- W klasie
Aquarium
zmieńvolume
navar
, aby można było ustawić ją więcej niż raz. - 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 tovalue
, ale możesz ją zmienić.
var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
- W
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()
}
- 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 coprivate
, 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.
- Oznacz klasę
Aquarium
i wszystkie jej właściwości słowem kluczowymopen
.
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
z wartością"rectangle"
.
open val shape = "rectangle"
- Dodaj otwartą właściwość
water
z metodą getter, która zwraca 90% wartości właściwościAquarium
.
open var water: Double = 0.0
get() = volume * 0.9
- 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)")
}
- W
buildAquarium()
zmień kod, aby 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. Tworzenie podklasy
- Utwórz podklasę
Aquarium
o nazwieTowerTank
, która implementuje zbiornik w kształcie zaokrąglonego walca zamiast zbiornika prostokątnego. Możesz dodaćTowerTank
poniżejAquarium
, ponieważ możesz dodać kolejną klasę w tym samym pliku co klasaAquarium
. - W
TowerTank
zastąp właściwośćheight
zdefiniowaną w konstruktorze. Aby zastąpić właściwość, użyj w podklasie słowa kluczowegooverride
.
- Spraw, aby konstruktor
TowerTank
przyjmowałdiameter
. Użyjdiameter
zarówno w przypadkulength
, jak iwidth
podczas wywoływania konstruktora w klasie nadrzędnejAquarium
.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
- 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łą
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
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"
- 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"
}
- 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 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
- W sekcji example.myapp utwórz nowy plik
AquariumFish.kt
. - Utwórz zajęcia, zwane też
AquariumFish
, i oznacz je symbolemabstract
. - Dodaj jedną właściwość
String
,color
, i oznacz ją symbolemabstract
.
package example.myapp
abstract class AquariumFish {
abstract val color: String
}
- Utwórz 2 podklasy klasy
AquariumFish
:Shark
iPlecostomus
. color
jest klasą abstrakcyjną, więc klasy podrzędne muszą ją implementować. Ustaw kolorShark
na szary, aPlecostomus
na złoty.
class Shark: AquariumFish() {
override val color = "gray"
}
class Plecostomus: AquariumFish() {
override val color = "gold"
}
- W pliku main.kt utwórz funkcję
makeFish()
, aby przetestować klasy. Utwórz instancję klasyShark
iPlecostomus
, a następnie wydrukuj kolor każdej z nich. - Usuń wcześniejszy kod testowy w
main()
i dodaj wywołaniemakeFish()
. 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()
}
- Uruchom program i obserwuj dane wyjściowe.
⇒ Shark: gray Plecostomus: gold
Poniższy diagram przedstawia klasy Shark
i Plecostomus
, które są podklasami klasy abstrakcyjnej AquariumFish
.
Krok 2. Tworzenie interfejsu
- W pliku AquariumFish.kt utwórz interfejs o nazwie
FishAction
z metodąeat()
.
interface FishAction {
fun eat()
}
- Dodaj
FishAction
do każdej z podklas i zastosujeat()
, 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")
}
}
- 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()
}
- Uruchom program i obserwuj dane wyjściowe.
⇒ 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 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 klasyAquariumFish
będą implementować interfejsFishAction
, i zapewnić domyślną implementację dla klasyeat
, 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
- W pliku AquariumFish.kt usuń klasę
AquariumFish
. Zamiast dziedziczyć po klasieAquariumFish
, klasyPlecostomus
iShark
będą implementować interfejsy zarówno dla działania ryby, jak i jej koloru. - Utwórz nowy interfejs
FishColor
, który definiuje kolor jako ciąg znaków.
interface FishColor {
val color: String
}
- Zmień
Plecostomus
, aby zaimplementować 2 interfejsy,FishAction
iFishColor
. 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")
}
}
- Zmień klasę
Shark
, aby implementowała interfejsyFishAction
iFishColor
zamiast dziedziczyć po klasieAquariumFish
.
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 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.
- 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.
- W pliku AquariumFish.kt usuń zastąpienie
color
zPlecostomus
. - Zmień klasę
Plecostomus
, aby pobierać kolor z klasyGoldColor
. W tym celu dodajby GoldColor
do deklaracji klasy, tworząc delegowanie. Oznacza to, że zamiast implementowaćFishColor
, należy użyć implementacji dostarczonej przezGoldColor
. Dlatego za każdym razem, gdy uzyskiwany jest dostęp docolor
, jest on delegowany doGoldColor
.
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
.
- Zmień klasę
Plecostomus
, aby przyjmowała przekazaną wartośćfishColor
z konstruktorem, i ustaw jej wartość domyślną naGoldColor
. Zmień delegowanie zby GoldColor
naby 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
.
- W pliku AquariumFish.kt utwórz klasę
PrintingFishAction
, która implementuje interfejsFishAction
. Klasa ta przyjmuje argumentyString
ifood
, a następnie drukuje informacje o tym, co je ryba.
class PrintingFishAction(val food: String) : FishAction {
override fun eat() {
println(food)
}
}
- W klasie
Plecostomus
usuń funkcję zastępowaniaeat()
, ponieważ zastąpisz ją delegowaniem. - W deklaracji
Plecostomus
przekaż delegowanieFishAction
doPrintingFishAction
, przekazując"eat algae"
. - 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 Shark
i Plecostomus
, które składają się z interfejsów PrintingFishAction
i FishColor
, 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
- 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). - W pakiecie utwórz nową klasę o nazwie
Decoration
.
package example.myapp.decor
class Decoration {
}
- Aby utworzyć klasę danych
Decoration
, dodaj przed deklaracją klasy słowo kluczowedata
. - Dodaj właściwość
String
o nazwierocks
, aby przekazać klasie dane.
data class Decoration(val rocks: String) {
}
- W pliku poza klasą dodaj funkcję
makeDecorations()
, aby utworzyć i wydrukować instancję klasyDecoration
z wartością"granite"
.
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
}
- 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)
- W
makeDecorations()
utwórz jeszcze 2 obiektyDecoration
, 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)
}
- W
makeDecorations()
dodaj instrukcję drukowania, która wyświetli wynik porównaniadecoration1
zdecoration2
, oraz drugą, która wyświetli wynik porównaniadecoration3
zdecoration2
. Użyj metody equals() udostępnianej przez 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. 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ą.
- 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
.
- 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.
- 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ść nainternal
,private
lubprotected
. - 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
zamiastclass
. - 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.
- Klasy i dziedziczenie
- Konstruktory
- Funkcje fabryczne
- Usługi i pola
- Modyfikatory widoczności
- Klasy abstrakcyjne
- Interfejsy
- Delegowanie
- Klasy danych
- Equality
- Destrukturyzacja
- Deklaracje obiektów
- Klasy wyliczeniowe
- Klasy zamknięte
- Obsługa błędów opcjonalnych za pomocą klas zapieczętowanych w Kotlinie
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:
Omówienie kursu, w tym linki do innych ćwiczeń, znajdziesz w artykule „Kotlin Bootcamp for Programmers: Welcome to the course” (w języku angielskim).