Enum

Koziołek

1 Problem
2 Definicja typu enum
3 Pola i metody o typie enum
     3.1 Pola
     3.2 Metody własne typu enum
     3.3 Definiowanie metod
4 Typ enum w pętlach i przełącznikach (switch)
          4.3.1 Blok switch
          4.3.2 Pętle
5 Przykładowy program

Problem

Wiele programów w Javie korzysta ze składni "stałych sterujących":

public class A {
    public static final int STAN_A = 0;

    public static final int STAN_B = 1;

    public void metoda( int stan ) {
        if ( stan == STAN_A ) {
            //......
        }
        else if ( stan == STAN_B ) {
            //......
        }
        else {
            throw new IllegalArgumentException("nieprawidłowy stan!");
        }
    }
}

Takie podejście, choć w ogólności słuszne, jest obarczone wieloma poważnymi wadami:

  • Brak ochrony typu. Nic nie stoi na przeszkodzie, by wywołać metodę z parametrem będącym sumą STAN_A i STAN_B. Taki zapis nie ma sensu. Jeżeli dodatkowo będzie brakowało ostatniego bloku – to znaczy funkcja nie będzie zwracała wyjątku, gdy parametr będzie nieprawidłowy – to programista może zostać zaskoczony pozbawionym sensu zachowaniem aplikacji. Nic nie stoi też na przeszkodzie, by przekazać parametr typu float lub double (oczywiście rzutujemy).
  • Konieczność korzystania z przedrostków (by się po prostu nie pogubić). Dodatkowo przedrostki i nazwy zmiennych wystarczają do określenia, co chcemy zrobić, a tak musimy jeszcze definiować zmienne.
  • Sztywne zaszycie wartości w kodzie. Zmienne STAN_A i STAN_B są zmiennymi czasu kompilacji co oznacza, że
    • w miejscach wywołania kompilator podstawia ich wartości, a nie referencje do nich, a tym samym
    • jakakolwiek zmiana ich wartości lub dodanie nowej (np. STAN_C) wymaga powtórnej kompilacji kodu wraz z cały kodem klientów z nich korzystających.
  • Wartości nie są znaczące. Jeżeli wypiszemy wartości STAN_A i STAN_B, to otrzymamy nic nie znaczące cyfry. Dopiero lektura dokumentacji umożliwi identyfikację ich znaczenia.

W celu rozwiązania tych problemów w Javie 5 wprowadzono typ enum.

Definicja typu enum

Typ `enum` jest typem wyliczeniowym, literałem, który jest traktowany jak klasa specjalna, zawierająca w swojej definicji wszystkie możliwe do stworzenia instancje obiektów.

Typ enum wygląda bardzo podobnie jak w C/C++/C#, lecz jest bardziej rozbudowany. Najprostsza wersja ma postać:

enum Stany{ A, B}

Sam typ enum jest traktowany tak jak klasa i może być przetwarzany w ten sam sposób. Jest on porównywalny (Comparable) i serializowany (Serializable).

Pola i metody o typie enum

Pola

Typ enum w języku Java pozwala, w przeciwieństwie do swoich odpowiedników z innych języków, definiować pola i metody dla każdej z wartości. Wynika to z "klasowej" charakterystyki tego typu. Poniższy zapis jest więc prawidłowy:

enum Wartosci {
    A, B;

    public String name;

    protected String value;

    private int number;
}

Metody własne typu enum

Typ enum zawiera trzy metody:

  • static values() – zwraca tablicę zawierającą wszystkie obiekty enum. W naszym przypadku będą to A i B.
  • static valueOf(String) – zwraca wartość enuma w postaci obiektu enum, dla klucza o typie String.
  • static valueOf(Class< T >, String) – zwraca wartość typu enum w postaci obiektu enum, dla klucza o typie String z klasy enumów T.

Przykładowy kod:

System.out.println(Wartosci.values());
System.out.println(Wartosci.valueOf( "A" ));
System.out.println(Wartosci.valueOf( W.class, "B" ));

zwróci (w konsoli):

[Lenums.Wartosci;@13e8d89
A
B

Definiowanie metod

Typ enum jest traktowany jak klasa, zatem może posiadać też metody. Metody definiuje się tak samo jak w przypadku zwykłych klas. Możemy zatem napisać:

enum Wartosci {
    A, B("enum B");

    //...

    public void metoda1(){
        //...
    }

    protected void metoda2(){
        //...
    }

    private void metoda3(){
        //...
    }

    private Wartosci(){
       //...
    }

    private Wartosci(String s){
        name = s;
    }
}

Należy zwrócić uwagę na dwie rzeczy. Po pierwsze, konstruktor typu enum może mieć albo modyfikator private, albo nie mieć modyfikatora (tzw. "package-private"). Może on przyjmować parametry, ale nie można wywoływać go bezpośrednio (należy użyć do tego zdefiniowanych stałych). W powyższym przykładzie napisanie Wartosci.B stworzy obiekt o typie Wartosci i przekaże do konstruktora ciąg znaków "enum B" (obiekt o typie String). Nie można natomiast napisać new Wartosci("jakis napis").

Po drugie, metody mogą mieć dowolny modyfikator dostępu. Dostęp do nich realizowany jest tak samo jak w przypadku zwykłych obiektów.

Typ enum w pętlach i przełącznikach (switch)

Dotychczas omówiliśmy, jak tworzy się typ enum, oraz co można, a czego nie można definiować w jego ramach. Jednak prawdziwa siła enumów leży nie w samym sposobie ich definiowania, lecz w możliwościach ich wykorzystania.

Blok switch

Na początku wspomniany był przykład "zmiennej sterującej" w danej klasie. Bardzo często zmienne takie wykorzystywane są w blokach switch. Co jednak się stanie, gdy wartość przekazana do takiego bloku jest inna niż te, które zdefiniowano? Oczywiście zostanie wywołany fragment po instrukcji default – co może spowodować nieintuicyjne działanie aplikacji lub, w gorszym wypadku, pojawienie się wyjątku (na przykład IllegalArgumentException). Dlaczego tak się dzieje? Jest to wynik problemów związanych ze słabą kontrolą typu zmiennej, a w rzeczywistości brakiem kontroli jej wartości. Przyjrzyjmy się takiemu oto fragmentowi:

void metoda1(){
    metoda (2);
}

void metoda(int i){
    switch (i) {
    case 0:
        break;
    case 1:
        break;
    default:
        throw new IllegalArgumentException();
    }
}

Z punktu widzenia kompilatora (a w zasadzie parsera kodu), wszystko jest poprawne. metoda() została wywołana z prawidłowym parametrem typu int, zatem można przyjąć, że programista wie, co robi, gdy ją wywołuje. W rzeczywistości wywołanie spowoduje błąd. Jak widać, w procesie kompilacji nie udało się (i nie ma na to metody) wychwycić błędnego parametru.

Jeżeli chcielibyśmy, aby wszytko przebiegło poprawnie, należałoby napisać kod, który pozwalałby na ścisłą kontrolę typu i wyłapywał takie błędy już w trakcie kompilacji. Typ enum może być użyty w bloku switch. Ten fakt pozwala nam na napisanie poprawnego kodu:

void metoda1(){
    metoda(Wartosci.A);
}

void metoda(Wartosci w){
    switch (w) {
    case A:
        break;
    case B:
        break;
    }
}

To rozwiązanie jest dla nas podwójnie korzystne. Z jednej strony pozwala uniknąć błędu złego argumentu, a po drugie pozwoli wyłapać potencjalne błędy już na poziomie kompilacji dzięki kontroli typu.

Pętle

Typ enum może zostać też użyty do reprezentowania dobrze określonych (przeliczalnych), skończonych i uporządkowanych zbiorów – na przykład talii kart, układu planetarnego, czy układu okresowego. A jeśli pracujemy na zbiorach, to zawsze spotykamy się z problemem wykonania jakiejś operacji na wszystkich elementach tego zbioru. Typ enum może być użyty w pętli for w wersji z iteratorem. Przykładowy kod:

for(Wartosci w : Wartosci.values()){
    System.out.println(w.value);
}

Przykładowy program

Na zakończenie chciałbym przedstawić przykładowy program, w którym korzystam z typu enum. Program wyznaczy, ile na innych planetach będzie ważyło ciało, które na ziemi waży 100kg:

public class Masa {

    public static void main(String[] args) {
        double masaZiemi = 100;
        double masa = masaZiemi / Planeta.ZIEMIA.przeliczGrawitacje();
        for (Planeta p : Planeta.values())
            System.out.printf("twoja masa na %s wynosi %f kilogramów\n", p, p
                    .przeliczMasy(masa));

    }

    public enum Planeta {
        MERKURY(3.303e+23, 2.4397e6), WENUS(4.869e+24, 6.0518e6), ZIEMIA(
                5.976e+24, 6.37814e6), MARS(6.421e+23, 3.3972e6), JOWISZ(
                1.9e+27, 7.1492e7), SATURN(5.688e+26, 6.0268e7), URAN(
                8.686e+25, 2.5559e7), NEPTUN(1.024e+26, 2.4746e7), PLUTON(
                1.27e+22, 1.137e6);

        private final double masa; // w kilogramach
        private final double promien; // w metrach

        Planeta(double masa, double promien) {
            this.masa = masa;
            this.promien = promien;
        }

        public double masa() {
            return masa;
        }

        public double radius() {
            return promien;
        }

        // uniwersalna stała grawitacyjna (m3 kg-1 s-2)
        public static final double G = 6.67300E-11;

        public double przeliczGrawitacje() {
            return G * masa / (promien * promien);
        }

        public double przeliczMasy(double innaMasa) {
            return innaMasa * przliczGrawitacje();
        }
    }

}

5 komentarzy

ten enum to trochę jak factor w R.

return innaMasa * przliczGrawitacje(); // literowka :)

Enum posiada więcej metod. Posiada trzy metody statyczne, które można wywołać bez konieczności tworzenia obiektu.
Dodatkowo ma:
public final ordinal()- zwraca wartość int informującą o miejscu deklaracji danej stałej w wyliczeniu
public final int compareTo(E o) - porównuje enum z obiektem podanym jako argument pod względem pozycji w wyliczeniu.
Pozostałe: Equals, hasCode, name, toString... Wszystkie klasy Enum.

Artykuł pominął kwestię tworzenia obiektów danej klasy Enum.

Ma i to nawet jeszcze większe, ale jakoś nie opisałem... może kiedyś

Coż powiedzieć - brawo! Oby tak dalej. Sam mam na dysku niedokończony artykuł o Apache Ant, ale jakoś brak mi weny by go dokończyć...

Nawet nie wiedziałem, że Enum w Javie ma takie możliwości ;)