30 marca 2008

SCJP - Typy wyliczeniowe

Typy wyliczeniowe, czyli streszczenie sekcji "Declaring Enums" – kontynuacja działań zapowiedzianych w artykule "Przygotowania do SCJP czas zacząć". Znowu przyjdzie mi jednak czytać specyfikację języka Java, jako że informacje w Książce są mocno nie kompletne.

Typy wyliczeniowe deklarujemy za pomocą słówka kluczowego enum bezpośrednio w pliku, w klasie, interfejsie albo wewnątrz innego typu wyliczeniowego, ale nie w metodzie. Typ wyliczeniowy, oprócz tego, że definiuje stałe wyliczeniowe może także zawierać konstruktory, zmienne i metody oraz klasy wewnętrzne i inne typy wyliczeniowe – prawie jak klasa, ale prawie robi ogromną różnicę.

Zacznijmy od stałych wyliczeniowych. Poniżej przykład deklaracji typu wyliczeniowego, który zawiera trzy takie stałe.

enum Size {
SMALL, LARGE, HUGE
}

Na tego typu deklaracjach kończy się wiedza wielu programistów, a to dopiero początek. Zacznijmy powoli, od modyfikatorów, jakich możemy użyć w deklaracji typu wyliczeniowego – dla deklaracji bezpośrednio w pliku są to tylko public oraz strictfp, a dla deklaracji wewnątrz innych typów dodatkowo private, protected i static. Ich znaczenie jest takie jak w przypadku klas i opisałem je w artykule "SCJP - Modyfikatory dla deklaracji klas". Idźmy dalej. Poniżej przykład deklaracji zawierającej stałe wyliczeniowe, metodę, zmienną i konstruktor. Zwróćmy uwagę na średnik po deklaracjach stałych wyliczeniowych – jest obowiązkowy, jeśli po tych deklaracjach znajduje się coś jeszcze. Zapamiętajmy też, że stałe wyliczeniowe muszą być zadeklarowane jako pierwsze, dopiero potem, po średniku następują kolejne deklaracje!

enum Size {
SMALL(1), LARGE(2), HUGE(3);

Size(int size) {
this.size = size;
}

public int getSize() {
return size;
}

private int size;
}

To, czym typy wyliczeniowe różnią się zdecydowanie od klas jest, że mogą one zawierać jedynie konstruktory prywatne. Nie jest możliwe utworzenie instancji typu wyliczeniowego w inny sposób, jak tylko poprzez deklarację stałej wyliczeniowej. Tak, stałe wyliczeniowe są takiego typu, w jakim je zadeklarowano i są to jedyne instancje tego typu. W powyższym przykładzie stałe Size.SMALL, Size.LARGE i Size.HUGE są zatem typu Size i są to jedyne instancje tego typu jakie kiedykolwiek będą występowały w przyrodzie, tj. w JVM – innych utworzyć się nie da, także używając refleksji czy operacji clone(). Oznacza to między innymi, że możemy używać operatora == zamiast operacji equals(). Mimo, że tak czy inaczej konstruktory typów wyliczeniowych są prywatne, możemy oznaczyć je słówkiem kluczowym private explicite. Jest to jedyny modyfikator, jakiego możemy użyć w deklaracji takiego konstruktora.

Zerknijmy teraz ponownie na deklarację stałych wyliczeniowych w powyższym przykładzie. Są to stałe i do dobrych praktyk należy, aby ich nazwy były pisane samymi wielkimi literami, ale nie jest to wymóg bezwzględny, może to być dowolny poprawny identyfikator – co to znaczy poprawny opisałem w artykule "SCJP - Poprawne identyfikatory i konwencje nazewnicze". Tuż po nazwie stałej możemy podać parametry dla wywołania konstruktora. Jeśli argumentów nie podano – tj. użyto samej nazwy, np. SMALL – to dla utworzenia instancji (stałej) wywoływany jest konstruktor bez-argumentowy. Tak jak w przypadku klas, możemy zdefiniować i użyć dowolny inny konstruktor.

Typy wyliczeniowe mogą implementować interfejsy, ale nie mogą dziedziczyć z innych typów wyliczeniowych. Istnieje jednak sposób na przedefiniowanie metod dla wybranych stałych wyliczeniowych, zupełnie jakby były one podtypami zawierającego typu wyliczeniowego. Po deklaracji stałej i jej ewentualnych parametrach dla konstruktora można umieścić ciało klasy anonimowej, która dla tej jednej instancji określi typ na bazie typu zawierającego. Spójrzmy na poniższy przykład.

enum Size {
SMALL {
public String getDescription() {
return "Taki calkiem maly";
}
},

LARGE {
public String getDescription() {
return "Dosyc duzy";
}
};

public abstract String getDescription();
}

Mimo, że nie możemy zadeklarować, że dany typ wyliczeniowy dziedziczy z innego typu, to jak najbardziej typy wyliczeniowe również należą do wspólnej hierarchii z klasą Object w korzeniu. Każdy typ wyliczeniowy E dziedziczy bowiem implicite z typu Enum<E>, a ten z typu Object. Oprócz metod odziedziczonych z Enum<E> każdy typ wyliczeniowy E ma zdefiniowane metody ‘public static E[] values()’ oraz ‘public static E valueOf(String name)’. Metoda values() zwraca tablicę stałych wyliczeniowych zdefiniowanych w typie E a metoda valueOf() służy do konwersji z typu String – zwraca instancję typu E (jedną ze stałych wyliczeniowych) o podanej nazwie.

Jeśli chodzi o deklaracje metod, to dozwolone są wszystkie te modyfikatory, co dla deklaracji w klasach, z jednym tylko obostrzeniem dla modyfikatora abstract – jeśli w typie wyliczeniowym zadeklarowano metodę abstrakcyjną, to musi być ona zdefiniowana przez każdą ze stałych wyliczeniowych w tym typie. Musi przy tym być zadeklarowana co najmniej jedna taka stała. Zmienne obowiązują te same zasady, co zmienne w klasach.

Było ciężko, ale się udało – zakończyliśmy niniejszym lekturę pierwszego rozdziału referowanej książki "SCJP Sun Certified Programmer for Java 5 Study Guide (Exam 310-055)", a to już mały sukces.

2 komentarze:

Mariusz Lipiński pisze...

Jeszcze małe uzupełnienie, bo wyraziłem się trochę nie ścisło. Właściwie to średnik oddzielający deklaracje stałych wyliczeniowych od reszty nie jest stawiany po deklaracjach stałych, tylko przed deklaracjami tej reszty. Znaczy to tyle, że nie zależnie od tego czy zdefiniowano jakieś stałe wyliczeniowe ten średnik i tak być musi, jeśli obecne są jakieś inne deklaracje. Sprawdźmy czy to co napisałem jest zrozumiałe. Czy poprawny jest poniższy kod?

enum Du {
public String getDescription() {
return "Jakis opis";
}
}

NIE! Brakuje średnika. Powinno być tak:

enum Du {
;

public String getDescription() {
return "Jakis opis";
}
}

Mariusz Pratnicki pisze...

Co prawda to prawda :) Wiedza sie czesto ogranicza do enum Du { A, B, C}

Fajne przypomnienie ;)

Pozdrowienia