Obiekty w Kotlinie
To jest rozdział z książki Kotlin Essentials.
Czym jest obiekt? To pytanie, którym często zaczynam tę sekcję na moich warsztatach i zazwyczaj dostaję natychmiastową odpowiedź: "Instancją klasy". To prawda, a więc jak tworzymy obiekty? Jednym ze sposobów jest proste użycie konstruktorów.
Nie jest to jednak jedyny sposób. W Kotlinie możemy również tworzyć obiekty za pomocą wyrażeń tworzących obiekty (ang. object expression) oraz deklaracji obiektów (ang. object declaration). Omówmy te dwie opcje.
Wyrażenia tworzące obiekty
Aby utworzyć pusty obiekt za pomocą wyrażenia, używamy słowa kluczowego object
i nawiasów klamrowych.
Pusty obiekt nie rozszerza żadnych klas (oprócz Any
, rozszerzanego przez wszystkie obiekty w Kotlinie), nie implementuje żadnych interfejsów i nie ma niczego w swoim ciele. Mimo to jest przydatny. Jego moc tkwi w unikalności: taki obiekt równa się tylko sobie. Dlatego doskonale nadaje się do użycia jako rodzaj tokena lub klucza synchronizacji.
Pusty obiekt można również utworzyć przy pomocy konstruktora Any
, więc Any()
jest alternatywą dla object {}
.
Jednak obiekty utworzone za pomocą wyrażeń tworzących obiekt nie muszą być puste. Mogą mieć ciała, rozszerzać klasy, implementować interfejsy itp. Składnia jest taka sama jak dla klas, ale deklaracje obiektów używają słowa kluczowego object
zamiast class
i nie mogą definiować nazwy ani konstruktora.
W lokalnym zakresie wyrażenia tworzące obiekt definiują anonimowy typ, który nie będzie działać poza klasą, w której został zdefiniowany. Oznacza to, że nieodziedziczone składniki wyrażeń obiektów są dostępne tylko wtedy, gdy anonimowy obiekt jest deklarowany w lokalnym zakresie lub w zakresie prywatnym klasy; w przeciwnym razie typ obiektu jest określony jako Any
lub typ klasy/interfejsu, po którym dziedziczy. Sprawia to, że nieodziedziczone składniki wyrażeń obiektów są trudne do wykorzystania w rzeczywistych projektach.
W praktyce wyrażenia obiektów są używane jako alternatywa dla anonimowych klas Java, tj. gdy musimy utworzyć obserwatora lub listenera z wieloma metodami.
Zauważ, że "wyrażenie tworzące obiekt" to trafniejsza nazwa niż "anonimowa klasa", ponieważ jest to wyrażenie, które tworzy obiekt. Choć faktem jest, że tworzy też anonimową klasę, ale to jest detal implementacyjny, mało istotny z perspektywy programisty.
Deklaracja obiektu
Jeśli weźmiemy wyrażenie tworzące obiekt i nadamy mu nazwę, otrzymamy deklarację obiektu. Ta struktura również tworzy pojedynczy obiekt, aczkolwiek obiekt ten ma nazwę, która może być użyta do odwołania się do niego. Zauważ, że składnia deklaracji obiektu jest identyczna jak składnia deklaracji klasy, tylko że deklaracja obiektu nie ma konstruktora i używa słowa kluczowego object
zamiast class
.
Deklaracja obiektu to implementacja wzorca singleton4, tworzy więc klasę z pojedynczą instancją. Kiedykolwiek chcemy jej użyć, musimy działać na tej pojedynczej instancji. Deklaracje obiektów obsługują wszystkie funkcjonalności, które obsługują klasy; na przykład mogą rozszerzać klasy lub implementować interfejsy.
Companion obiekty
Kiedy wspominam czasy, gdy pracowałem jako programista Javy, pamiętam dyskusje na temat tego, jakie funkcjonalności powinny być wprowadzone do tego języka. Często słyszałem o pomyśle wprowadzenia dziedziczenia dla elementów statycznych. W końcu dziedziczenie jest bardzo ważne w Javie, więc dlaczego nie można go użyć dla elementów statycznych? Kotlin rozwiązał ten problem za pomocą companion obiektów; jednak, aby to było możliwe, musiał najpierw zlikwidować rzeczywiste elementy statyczne, tj. elementy, które są wywoływane na klasach, a nie na obiektach.
// Java
class User {
// Definicja elementu statycznego
public static User empty() {
return new User();
}
}
// Użycie elementu statycznego
User user = User.empty()
Tak, w Kotlinie nie mamy elementów statycznych, ale nie potrzebujemy ich, ponieważ używamy zamiast tego deklaracji obiektów. Jeśli zdefiniujemy deklarację obiektu w klasie, jest ona domyślnie statyczna (podobnie jak klasy zdefiniowane wewnątrz klas), więc możemy bezpośrednio wywołać jej elementy.
To nie jest tak wygodne, jak elementy statyczne, ale możemy to poprawić. Jeśli użyjemy słowa kluczowego companion
przed deklaracją obiektu zdefiniowaną w klasie, wówczas możemy wywoływać te metody obiektu niejawnie przy użyciu nazwy klasy. A więc User.Producer.empty()
może być zastąpione przez User.empty()
.
Obiekty z modyfikatorem companion
nie muszą mieć określonej nazwy. Ich domyślna nazwa to Companion
.
W ten sposób osiągnęliśmy składnię, która jest prawie tak wygodna, jak elementy statyczne. Jedyną niedogodnością jest to, że musimy umieścić wszystkie "statyczne" elementy wewnątrz pojedynczego obiektu (w klasie może być tylko jeden companion obiekt). Jest to ograniczenie, ale mamy coś w zamian: obiekty companion to obiekty, więc mogą rozszerzać klasy lub implementować interfejsy.
Pozwól, że pokażę Ci przykład. Powiedzmy, że reprezentujesz pieniądze w różnych walutach za pomocą dedykowanych klas, takich jak USD
, EUR
czy PLN
. Dla wygody każda z nich definiuje funkcje konstruujące from
, które upraszczają tworzenie obiektów.
Powtarzające się funkcje do tworzenia obiektów z różnych typów można wyodrębnić do abstrakcyjnej klasy MoneyMaker
, która może być rozszerzana przez companion obiekty różnych walut. Ta klasa może oferować szereg metod do tworzenia waluty. W ten sposób wykorzystujemy dziedziczenie companion obiektów do wyodrębnienia wzorca, który jest wspólny dla wszystkich companion obiektów klas reprezentujących pieniądze.
Nasza społeczność nadal uczy się korzystać z tych możliwości, ale już teraz można znaleźć wiele przykładów w projektach i bibliotekach. Oto kilka interesujących przykładów6:
Deklaracje obiektów danych
Począwszy od wersji 1.8 w Kotlinie można używać modyfikatora data
dla deklaracji obiektów. Generuje on metodę toString
dla obiektu; ta metoda zwraca nazwę obiektu jako string.
Stałe wartości
Powszechną praktyką jest wyodrębnianie stałych wartości jako właściwości companion obiektów i nazywanie ich, używając UPPER_SNAKE_CASE5. W ten sposób nazywamy te wartości i upraszczamy ich zmiany w przyszłości. Nadajemy stałym wartościom charakterystyczne nazwy, aby było jasne, że reprezentują stałą2.
Gdy właściwości companion obiektów lub właściwości plików reprezentują stałą wartość (znaną w czasie kompilacji), będącą albo wartością prymitywną, albo typu String
3, to możemy dodać modyfikator const
. Jest to optymalizacja. Wszystkie użycia takich zmiennych zostaną zastąpione ich wartościami w czasie kompilacji.
Właściwości z modyfikatorem const
można również używać w adnotacjach:
Podsumowanie
W tym rozdziale dowiedzieliśmy się, że obiekty można tworzyć nie tylko z klas, ale także za pomocą wyrażeń tworzących obiekty i deklaracji obiektów. Obie te funkcjonalności mają swoje praktyczne zastosowania. Wyrażenie tworzące obiekt jest używane jako alternatywa dla anonimowych klas w Javie, ale oferująca więcej możliwości. Deklaracja obiektu to implementacja wzorca singleton w Kotlinie. Specjalna forma deklaracji obiektu, znana jako companion obiekt, jest używana jako alternatywa dla elementów statycznych, ale z dodatkowym wsparciem dla dziedziczenia. Mamy również modyfikator const
, który oferuje lepsze wsparcie dla stałych elementów zdefiniowanych w plikach lub w deklaracjach obiektów.
W poprzednim rozdziale omówiliśmy data klasy, ale Kotlin wspiera wiele różnych rodzajów klas. W następnym rozdziale poznamy kolejny ważny rodzaj: wyjątki.
Ta praktyka jest lepiej opisana w Effective Kotlin, Temat 27: Użyj abstrakcji, aby chronić kod przed zmianami.
Akceptowane typy to Int
, Long
, Double
, Float
, Short
, Byte
, Boolean
, Char
i String
.
Wzorzec programowania, w którym klasa jest implementowana tak, aby mogła mieć tylko jedną instancję.
UPPER_SNAKE_CASE to konwencja nazewnictwa, w której każdy znak jest pisany wielką literą, a słowa oddzielamy podkreśleniem, jak w nazwie UPPER_SNAKE_CASE. Użycie go dla stałych jest sugerowane w dokumentacji Kotlina w sekcji Kotlin Coding Convention.
Nie traktuj ich jako najlepsze praktyki, ale raczej jako przykłady tego, co można zrobić z faktem, że companion obiekty mogą dziedziczyć po klasach i implementować interfejsy.