Typ wyliczeniowy w Javie

W javie w wersji 5 zawitał enumerator czyli tak zwany typ wyliczeniowy. W tym wpisie powiemy sobie o nim, do czego się przydaje, kiedy warto go stosować, a to wszystko będzie oparte o praktyczne przykłady

Enumerator…

Tak jak wspomniałem wyżej enumerator został wprowadzony w Javie 5 głównie po to, aby reprezentować konkretne, ustalone zbiory wartości o ograniczonej ich liczbie.

Dobrym przykładem odzwierciedlający powyższy zbiór mogą być stałe matematyczne, kolory, wzory lub rozmiary.

Przykładów jest wiele więcej, póki co wszystko co mówię, może wydawać Ci się abstrakcją, dlatego jak najszybciej przejdźmy do implementacji enuma – w końcu to kod powie nam najwięcej.

Gdzie go użyć…

Tak wyżej wspomniałem enumeratora używa się do określenia zbiorów o określonej liczbe elementów – elementów, które będą przez nas używane podczas tworzenia aplikacji.

Przejdźmy, więc do implementacji zbioru rozmiarów ubrań.

Jest to idealne zastosowanie, ponieważ nasz zbiór będzie miał określoną liczbę elementów oraz będzie mógłbyć często wykorzystywany podczas pisania aplikacji np. przypisany do ubrania.

Tworzenie enumeratora nie różni się za bardzo od tworzenia klasy, wystarczy zamienić słowo class na enum:

public enum CoatSize {
}

I o to mamy zbiór na rozmiary płaszczy, czas wypełnić go – a wypełnianie go jest dosyć specyficzne.

Wartości…

Wartości enumeratora są zapisywane po przecinku, zazwyczaj zapisywane wielkimi literami i po ostatnim elemencie występuje średnik. Za dużo tych słow, tak to po prostu wygląda w prac

public enum CoatSize {
    XS, S, M, L, XL;
}

Zapisane przez nas wartości tak naprawdę są stałymi statycznymi, do których możemy się odwołać z każdej części aplikacji.

Co już wiemy?

Wiemy już, że enumerator jest zbiorem publicznych statycznych stałych.

Jest głownie wykorzystywany do definiowania zbiorów o określonej liczbie elementów np. kolory, rozmiary ubrań itd.

Zagłębmy się dalej w typ wyliczeniowy.

Idźmy w głąb…

Sprawdźmy definicję w dokumentacji Javy czym jest tak naprawdę enumerator.

public abstract class Enum<E extends Enum<E>>
extends Object
implements Comparable<E>, Serializable

Warto zwrócić uwagę na słówko class – tak to prawda – enumerator jest tak naprawdę klasą!

Co za tym idzie – enumerator może mieć pola, metody oraz konstruktor!

Zróbmy więcej!

Czas na wzbogacenie naszego typu wyliczeniowego – dodajmy pola identyfikujące rozmiar w cm klatki piersiowej oraz talii.

public enum CoatSize {
    XS, S, M, L, XL;

    int chestLength;
    int waistLength;
    
}

Skoro mamy pola to warto je wypełnić wartościami np. przy pomocy konstruktora parametrowego.

public enum CoatSize {
    XS, S, M, L, XL;

    int chestLength;
    int waistLength;

    CoatSize(int chestLength, int waistLength) {
        this.chestLength = chestLength;
        this.waistLength = waistLength;
    }
}

Skoro zdefiniowaliśmy już konstruktor parametrowy to kod się nie skompiluje, ponieważ zdefiniowane wcześniej rozmiary nie mogą już korzystać z konstruktora bezparametrowego.

Czyli tak naprawdę zdefiniowane wartości XS, S, M… są obiektami typu CoatSize!

package pl.maniaq;

public enum CoatSize {
    XS(80, 80), S(90, 90), M(100, 100), L(110, 110), XL(120, 120);

    int chestLength;
    int waistLength;

    CoatSize(int chestLength, int waistLength) {
        this.chestLength = chestLength;
        this.waistLength = waistLength;
    }
}

I brawo w jednym miejscu przechowujemy rozmiary płaszcza wraz z ich parametrami długości. Nie uważasz, że dużo prościej niż zastosowanie stałych np. w ten sposób: ?

public final static int chestLengthXS = 80;
public final static int waistLengthXS = 80;
public final static int chestLengthS = 80;
public final static int waistLengthS = 80;
.
.
.

Powinieneś już zwracać uwagę na korzyści jakie płyną z korzystania enumeratora.

Wykorzystajmy go!

Skoro już mamy tak pięknie napisany enumerator to czas go wykorzystać w konkretnej aplikacji. Skoro mamy określone rozmiary płaszczy to stwórzmy model dla samego płaszcza, a w nim zawrzyjmy wcześniej utworzony enum.

Klasa płaszcza, która zawiera rozmiar oraz nazwę modelu.

public class Coat {

    private String modelName;
    private CoatSize size;

    public Coat(String modelName, CoatSize size) {
        this.modelName = modelName;
        this.size = size;
    }

    @Override
    public String toString() {
        return "Coat{" +
                "modelName='" + modelName + '\'' +
                ", size=" + size +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Coat coat = (Coat) o;
        return Objects.equals(modelName, coat.modelName) &&
                size == coat.size;
    }

    @Override
    public int hashCode() {

        return Objects.hash(modelName, size);
    }

    public String getModelName() {
        return modelName;
    }

    public CoatSize getSize() {
        return size;
    }
}

Idźmy dalej, stwórzmy jeszcze sklep, który będzie zawierał listę takich płaszczy, aby przykład był bardziej przejrzysty.

public class Shop {

    private List<Coat> coats;

    public Shop(){
        coats = new LinkedList<>();
    }
    
}

Skoro to sklep to płaszcze przyjeżdzają z hurtownii oraz są sprzedawane. Dodajmy sobie dwie metody: jedna do usuwania płaszcza z listy, druga do dodawania kolejnych.

public class Shop {

    private List<Coat> coats;

    public Shop(){
        coats = new LinkedList<>();
    }

    public void addCoat(Coat coat){
        coats.add(coat);
    }

    public void removeCoat(Coat coat){
        coats.remove(coat);
    }

}

Mając już przygotowany grunt to dalszej nauki przejdźmy do testowania naszego enumeratora.

Otwórzmy sklep

Na początek utwórzmy sklep, do którego będziemy dostarczać nowe płaszcze.

public class Main {

    public static void main(String[] args) {
      Shop shop = new Shop();
    }
}

Stwórzmy kilka płaszczy, od których rozpoczniemy naszą działalność. 😉

Coat hugeCoat = new Coat("Huge Coat", CoatSize.XL);

Zatrzymajmy się na chwilę po stworzeniu pierwszego płaszcza. Jak widzisz w konstruktorze przekazałem rozmiar korzystając z naszego enumeratora.

Co by było, gdyby go nie było? Prawdopodobnie musiałbym tworzyć jakąś stałą w klasie lub użyć do reprezentacji rozmiaru Stringa co jest bardzo złym pomysłem.

Dokończmy produkcję naszych płaszczy…

public class Main {

    public static void main(String[] args) {
      Shop shop = new Shop();

      Coat hugeCoat = new Coat("Huge Coat", CoatSize.XL);
      Coat smallCoat = new Coat("Small Coat", CoatSize.XS);
      Coat wavyCoat = new Coat("Wavy Coat", CoatSize.L);
      Coat longCoat = new Coat("Long Coat", CoatSize.L);
      Coat darkCoat = new Coat("Dark Coat", CoatSize.M);
      Coat assassinCoat = new Coat("Assassin Coat", CoatSize.S);
    }
}

I w taki oto sposób mamy płascze, wrzućmy je teraz do sklepu:

shop.addCoat(hugeCoat);
shop.addCoat(smallCoat);
shop.addCoat(wavyCoat);
shop.addCoat(longCoat);
shop.addCoat(darkCoat);
shop.addCoat(assassinCoat);

Pobawmy się teraz trochę enumeratorami, dodajmy do klasy Shop metodę wyszukującą płaszcze o określonym rozmiarze.

public List<Coat> getCoatsBySize(CoatSize coatSize){
    List<Coat> coatList = new LinkedList<>();

}

Tak wygląda sygnatura metody, stworzyliśmy pustę listę, do której będziemy wrzucać płaszcze o konkretnym rozmiarze, a na koniec ją zwrócimy.

Skoro będziemy musieli przejrzeć wszystkie płaszcze to warto będzie wykorzystać pętle foreach.

for(Coat coat : coats){
    
}

W ten oto sposób możemy sprawdzić rozmiar każdego płaszcza, który jest w sklepie.

Proste użycie instrukcji warunkowej i wszystko zrobione.

public List<Coat> getCoatsBySize(CoatSize coatSize){
    List<Coat> coatList = new LinkedList<>();

    for(Coat coat : coats){
        boolean sameSize = coat.getSize() == coatSize;
        if(sameSize){
            coatList.add(coat);
        }
    }

    return coatList;
}

Teraz, gdy przyjdzie klient i zechce zobaczyć wszystkie płaszcze o rozmiarze L to wywołamy tę metodę.

Klient…

Wcielmy się w role takiego klienta.

Przypuśćmy, że chcemy zobaczyć wszystkie płaszcze w rozmiarze L.

List<Coat> coats = shop.getCoatsBySize(CoatSize.L);

I ponownie nie kombinuję jaki rozmiar wstawić, używam po prostu wcześniej zdefiniowanego zbioru!

Wyświetlmy jeszcze listę tych płaszczy:

public class Main {

    public static void main(String[] args) {
  Shop shop = new Shop();

  Coat hugeCoat = new Coat("Huge Coat", CoatSize.XL);
        Coat smallCoat = new Coat("Small Coat", CoatSize.XS);
        Coat wavyCoat = new Coat("Wavy Coat", CoatSize.L);
        Coat longCoat = new Coat("Long Coat", CoatSize.L);
        Coat darkCoat = new Coat("Dark Coat", CoatSize.M);
        Coat assassinCoat = new Coat("Assassin Coat", CoatSize.S);

        shop.addCoat(hugeCoat);
        shop.addCoat(smallCoat);
        shop.addCoat(wavyCoat);
        shop.addCoat(longCoat);
        shop.addCoat(darkCoat);
        shop.addCoat(assassinCoat);

        List<Coat> coats = shop.getCoatsBySize(CoatSize.L);

        System.out.println(coats);
    }
}

I otrzymujemy taki rezultat w konsoli:

[Coat{modelName='Wavy Coat', size=L}, Coat{modelName='Long Coat', size=L}]

Dzięki metodzie getCoatsBySize możemy stworzyć bardzo szybko filtry wyszukiwania po rozmiarze ubrania.

Niezdecydowany klient…

Czasami się zdarzy, że przyjdzie taki bardziej dociekliwy klient i będzie chciał się dowiedzieć co się kryje za rozmiarem L, wtedy my w enumeratorze – jak w normalnej klasie – przeciążymy metodę toString i pokażemy mu co się kryje pod tym rozmiarem.

public enum CoatSize {
    XS(80, 80), S(90, 90), M(100, 100), L(110, 110), XL(120, 120);

    int chestLength;
    int waistLength;

    CoatSize(int chestLength, int waistLength) {
        this.chestLength = chestLength;
        this.waistLength = waistLength;
    }

    @Override
    public String toString() {
        return "CoatSize{" +
                "chestLength=" + chestLength +
                ", waistLength=" + waistLength +
                '}';
    }
}

Wywołamy tylko:

System.out.print(CoatSize.L);

I od razu otrzymujemy

CoatSize{chestLength=110, waistLength=110}

Bez żadnego grzebania po klasach i sprawdzania jak się ta stała nazywała. 😉

Wniosek jest prosty, wszystko mamy w jednym miejscu.

Poróbmy dziwaczne rzeczy…

Skoro każdy element zdefiniowany w enumeratorze jest obiektem enumeratora,w którym się znajduje to możemy porobić przeróżne rzeczy.

Na przykład przeciążyć metodę dla konkretnego obiektu, czyli zdefiniować zachowanie dla konkretnej wartości.

Stwórzmy w enumeratorze metodę printInfo, która coś nam powie o rozmiarze.

public void printInfo(){
    System.out.println("Jestem rozmiarem płaszcza, ale dokładnie nie wiem jakiego. :/");
}

Ale czy, aby na pewno nie wiemy? W enumeratorze tak naprawdę wiemy!

public void printInfo(){
    System.out.println("Jestem rozmiarem płaszcza: "+name());
}

Jak widać w enumeratorze (w klasie nie!) można wykorzystać metodę name() – która zwraca nam nazwę obiektu zdefiniowanego w enumeratorze.

Skoro wszystkie zdefiniowane wartości są obiektami tego typu, to przeciążmy metodę printInfo dla każdego rozmiaru.

public enum CoatSize {
    XS(80, 80){
        @Override
        public void printInfo(){
            System.out.println("Jestem bardzo mały bo jestem rozmiarem XS");
        }
    },
    S(90, 90){
        @Override
        public void printInfo(){
            System.out.println("Jestem mały bo jestem rozmiarem S");
        }
    },
    M(100, 100){
        @Override
        public void printInfo(){
            System.out.println("Jestem średni bo jestem rozmiarem M");
        }
    },
    L(110, 110){
        @Override
        public void printInfo(){
            System.out.println("Jestem duży bo jestem rozmiarem L");
        }
    },
    XL(120, 120){
        @Override
        public void printInfo(){
            System.out.println("Jestem bardzo duży bo jestem rozmiarem XL");
        }
    };

    int chestLength;
    int waistLength;

    CoatSize(int chestLength, int waistLength) {
        this.chestLength = chestLength;
        this.waistLength = waistLength;
    }

    @Override
    public String toString() {
        return "CoatSize{" +
                "chestLength=" + chestLength +
                ", waistLength=" + waistLength +
                '}';
    }

    public void printInfo(){
        System.out.println("Jestem rozmiarem płaszcza: "+name());
    }
}


Dzięki takiej operacji możemy definiować zachowanie zależne od tego jakiego obiektu używamy.

Zapominalski..

Gdy podczas dodawania nowego obiektu zapomnimy o przeciażeniu metody printInfo to obiekt skorzysta z domyślnej definicji, tzn. użyje tego:

System.out.println("Jestem rozmiarem płaszcza: "+name());

Co zrobić, gdy chcemy wymusić zdefiniowanie konkretnej metody w każdym obiekcie?

Na pomoc przychodzi nam metoda abstrakcyjna.

Nie ma wielkiej filozofii, w definicji enumeratora zapisujemy nagłówek metody ze słowkiem kluczowym abstract, a kompilator dba, aby każdy obiekt zawierał jej definicję.

Możemy, więc w naszym kodzie zmienić metodę printInfo na abstrakcyjną oraz usunąć jej definicję:

public abstract  void printInfo();

Kod się nadal kompiluje, no chyba, że z jednej obiektu usuniemy definicję tej metody np. w ten sposób:

XS(80, 80){
    @Override
    public void printInfo(){
        System.out.println("Jestem bardzo mały bo jestem rozmiarem XS");
    }
},
S(90, 90){
    @Override
    public void printInfo(){
        System.out.println("Jestem mały bo jestem rozmiarem S");
    }
},
M(100, 100){
    @Override
    public void printInfo(){
        System.out.println("Jestem średni bo jestem rozmiarem M");
    }
},
L(110, 110){
    @Override
    public void printInfo(){
        System.out.println("Jestem duży bo jestem rozmiarem L");
    }
},
XL(120, 120){
    
};

To kompilator nas szybko przywróci do porządku komunikatem:

Error:(28, 5) java: <anonymous pl.maniaq.CoatSize$5> is not abstract and does not override abstract method printInfo() in pl.maniaq.CoatSize

Co po prostu oznacza, że metode printInfo musi posiadać definicję w obiekcie XL.

Zróbmy jeszcze jedną ciekawą rzecz…

Weźmy na warsztat pustego maina:

public class Main {

    public static void main(String[] args) {

    }
}

I stwórzmy w nim nowy rozmiar płaszcza!

Jak to zrobimy?

No to chyba tak samo jak w przypadku zwykłej klasy!

CoatSize hugeSize = new CoatSize(180, 180);

No, ale coś nie idzie. Otrzymujemy powiadomienie, że:

CoatSize(int, int) has private access

Hmm, czyli konstruktor jest prywatny. Zmieńmy, więc modyfikator dostępu konstruktora na publiczny w enumeratorze.

public CoatSize(int chestLength, int waistLength) {
    this.chestLength = chestLength;
    this.waistLength = waistLength;
}

No i nadal jest problem, podobno nie można użyć modyfikatora dostępu public w tym miejscu.

I to prawda, konstruktor w enumeratorze jest zawsze prywatny! 

Wnioski? Nie można tworzyć obiektów enumeratora poza jego definicją! Wszystkie obiekty tego typu muszą być utworzone w ciele enumeratora, tak jak zrobiliśmy to na początku:

XS(80, 80), S(90, 90), M(100, 100), L(110, 110), XL(120, 120);

Czemu typ wyliczeniowy…

Może od początku wpisu zastanawiasz się, czemu enumerator jest nazywany typem wyliczeniowym.

Już spieszę z wyjaśnieniami, jednak na początku krótki przykład:

package pl.maniaq;

public class Main {

    public static void main(String[] args) {

        for (CoatSize size:CoatSize.values()
             ) {
            System.out.println(size.ordinal());
        }

    }
}

Co otrzymujemy w konsoli?

0
1
2
3
4

Może nie jest jeszcze wszystko jasne, dodam jeszcze coś do powyższego kodu:

public class Main {

    public static void main(String[] args) {

        for (CoatSize size:CoatSize.values()
             ) {
            System.out.println(size.name() + ": " + size.ordinal());
        }

    }
}

A w konsoli mamy…

XS: 0
S: 1
M: 2
L: 3
XL: 4

Już rozumiesz?

Enumerator jest nazywany typem wyliczeniowym, ponieważ każdy obiekt utworzony w enumeratorze ma przypisaną liczbę całkowitą rozpoczynając od 0.

Dlatego pierwszy obiekt ma wartość 0, a kolejne są jego inkrementacją.

Warto, też zwrócić na wbudowaną w enumerator metodę values(), która zwraca nam listę obiektów enumeratora. Bardzo się przydaje podczas iteracji po nich – tak jak zrobiłem to w przykładzie powyżej. Wykorzystałem ją w pętli foreach.

Po tych wszystkich dziwacznych rzeczach, przejdźmy jeszcze na chwilę do ważnej kwestii.

Dziedziczenie, implementacja…

Jak już wiesz enumerator jest zwykłą klasą – no może nie do końca – co argumentuje kolejna kwestia: Enumerator nie może dziedziczyć żadnej klasy, ani enumeratora.

Może tylko implementować inne enumeratory!

To zagadnienie naprawdę warto zapamiętać, aby się w przyszłości nie zdziwić, że enumerator nie chce coś dziedziczyć tej naszej klaski.

Skoro może implementować interfejsy, to przejdźmy do przykładu – w końcu to on zawsze wyjaśnia sprawę.

Co może zawierać taki interfejs, który wykorzystamy w enumeratorze?

Przypuśćmy, że będziemy mieli dwa enumeratory:

  • LittleDogBreed – enumerator zawierający rasy mniejszy psów
  • HugeDogBreed – no i dla większy psów 😉

Stwórzmy na szybko dwa enumeratory:

public enum LittleDogBreed {
    CHIHUAHUA, COTON, DANDIE;
}

I jeszcze dla większych psów 😉

public enum HugeDogBreed {
    KARELIAN, KOMONDOR, LEONBERGER;
}

I teraz interfejs, przemyślmy jak mógłby on wyglądać – szukamy część wspólnych.

Z pewnością, każdy z tych psów będzie szczekał, ale każdy trochę inaczej.

public interface DogBehaviour {
    String bark();
}

Czas na jego implementację w enumeratorach i definicję metody bark.

public enum HugeDogBreed implements DogBehaviour {
    KARELIAN, KOMONDOR, LEONBERGER;

    @Override
    public String bark() {
        return "HAU!";
    }
}

I dla mniejszych psów 😉

public enum LittleDogBreed implements DogBehaviour{
    CHIHUAHUA, COTON, DANDIE;

    @Override
    public String bark() {
        return "hau!";
    }
}

Co zyskujemy? Pewność, że nasz enumerator będzie posiadał potrzebne metody, które można później wykorzystać podczas tworzenia aplikacji.

Podsumowanie

Jak zdążyłeś zauważyć brnąc przez ten artykuł, że typ wyliczeniowy jest bardzo przydatnym narzędziem podczas programowania. Pozwala on nam w jednym miejscu posiadam zbiór określonych wartości, po których później możemy definiować obiektów np. czerwone koszulki, płaszcze L.

Enumerator można również wykorzystać jako zbiór chociażby operacji podczas tworzenia kalkulatora (ADD, SUBTRACT, MULTIPLY, DIVIDE), zamiast używać bardzo brzydkich ciągów znaku.

Na koniec krótka refleksja dla Ciebie – zastanów się, który kod jest czytelniejszy…

Calculator.operator(10, 20, "ADD");

Czy może…

Calculator.operator(10, 20, Operations.ADD);

Jednak jeszcze to nie koniec, przygotowałem dla Ciebie proste zadania do przyswojenia powyższej wiedzy.

  1. Stwórz enumerator dla kolorów ubrań, dodaj kilka kolorów oraz w klasie Coat dodaj pole koloru płaszcza. W klasie Shop dodaj funkcję, która zwraca listę płaszczy zależnie od wybranego koloru.
  2. Stwórz enumerator dla rodzaju materiału jaki może być wykorzystany podczas szycia ubrania. Dodaj takie materiały jak: Cotton, Polyester oraz Wool. W klasie Coat dodaj pole oznaczające wykorzystany materiał, a w klasie Shop dodaj funkcję zwracającą listę płaszczy uszytych z konkretnego materiału. Przy użyciu trzech funkcji filtrujących stwórz jedną, która będzie zwracać płaszcze zależnie od wybranego: koloru, rodzaju materiału i rozmiaru. Tak jak w każdym odzieżowym sklepie internetowym. 😉