Utwórz wyliczeniowe stałe w Javie

Zbiór „wyliczalnych stałych” to uporządkowany zbiór stałych, które można policzyć, podobnie jak liczby. Ta właściwość pozwala używać ich jak liczb do indeksowania tablicy lub jako zmiennych indeksu w pętli for. W Javie takie obiekty są najczęściej określane jako „stałe wyliczeniowe”.

Użycie wyliczonych stałych może zwiększyć czytelność kodu. Na przykład możesz chcieć zdefiniować nowy typ danych o nazwie Kolor z możliwymi wartościami stałymi CZERWONY, ZIELONY i NIEBIESKI. Chodzi o to, aby kolor był atrybutem innych tworzonych obiektów, takich jak obiekty samochodowe:

class Car {Color color; ...}

Następnie możesz napisać przejrzysty, czytelny kod, na przykład:

 myCar.color = RED; 

zamiast czegoś takiego:

 myCar.color = 3; 

Jeszcze ważniejszym atrybutem stałych wyliczeniowych w językach takich jak Pascal jest to, że są bezpieczne dla typów. Innymi słowy, nie można przypisać nieprawidłowego koloru do atrybutu koloru - zawsze musi to być CZERWONY, ZIELONY lub NIEBIESKI. W przeciwieństwie do tego, gdyby zmienną koloru była liczba int, można by przypisać do niej dowolną prawidłową liczbę całkowitą, nawet jeśli ta liczba nie reprezentuje prawidłowego koloru.

W tym artykule przedstawiono szablon do tworzenia wyliczeniowych stałych, które są:

  • Wpisz bezpieczne
  • Nadający się do druku
  • Zamówione do użytku jako indeks
  • Połączony, do zapętlania do przodu lub do tyłu
  • Niezliczone

W przyszłym artykule dowiesz się, jak rozszerzyć wyliczone stałe, aby zaimplementować zachowanie zależne od stanu.

Dlaczego nie skorzystać z finałów statycznych?

Typowy mechanizm dla wyliczanych stałych wykorzystuje statyczne końcowe zmienne int, na przykład:

statyczne końcowe int RED = 0; statyczny końcowy int GREEN = 1; statyczny końcowy int NIEBIESKI = 2; ...

Przydają się finały statyczne

Ponieważ są ostateczne, wartości są stałe i niezmienne. Ponieważ są statyczne, są tworzone tylko raz dla klasy lub interfejsu, w którym są zdefiniowane, a nie raz dla każdego obiektu. A ponieważ są to zmienne całkowite, mogą być wyliczane i używane jako indeks.

Na przykład możesz napisać pętlę, aby utworzyć listę ulubionych kolorów klienta:

for (int i = 0; ...) {if (customerLikesColor (i)) {favouriteColors.add (i); }}

Możesz również indeksować do tablicy lub wektora, używając zmiennych, aby uzyskać wartość skojarzoną z kolorem. Na przykład załóżmy, że masz grę planszową, która ma różne kolorowe elementy dla każdego gracza. Powiedzmy, że masz mapę bitową dla każdego elementu koloru i metodę o nazwie, display()która kopiuje tę mapę bitową do bieżącej lokalizacji. Jednym ze sposobów umieszczenia figury na planszy może być coś takiego:

PiecePicture redPiece = new PiecePicture (RED); PiecePicture greenPiece = new PiecePicture (GREEN); PiecePicture bluePiece = nowy PiecePicture (NIEBIESKI);

void placePiece (int location, int color) {setPosition (location); if (color == RED) {display (redPiece); } else if (kolor == ZIELONY) {display (greenPiece); } else {display (bluePiece); }}

Ale używając wartości całkowitych do indeksowania w tablicy elementów, możesz uprościć kod, aby:

PiecePicture [] piece = {new PiecePicture (RED), new PiecePicture (GREEN), new PiecePicture (BLUE)}; void placePiece (int location, int color) {setPosition (location); wyświetlacz (sztuka [kolor]); }

Możliwość zapętlenia zakresu stałych i indeksowania do tablicy lub wektora to główne zalety statycznych liczb całkowitych. A kiedy liczba wyborów rośnie, efekt uproszczenia jest jeszcze większy.

Ale statyczne finały są ryzykowne

Mimo to stosowanie statycznych liczb całkowitych ma kilka wad. Główną wadą jest brak bezpieczeństwa typu. Każda obliczona lub wczytana liczba całkowita może być użyta jako „kolor”, niezależnie od tego, czy ma to sens. Możesz zapętlić się tuż za końcem zdefiniowanych stałych lub przestać je wszystkie pokrywać, co może się łatwo zdarzyć, jeśli dodasz lub usuniesz stałą z listy, ale zapomnisz o dostosowaniu indeksu pętli.

Na przykład pętla preferencji kolorów może wyglądać tak:

for (int i = 0; i <= BLUE; i ++) {if (customerLikesColor (i)) {favouriteColors.add (i); }}

Później możesz dodać nowy kolor:

statyczne końcowe int RED = 0; statyczny końcowy int GREEN = 1; statyczny końcowy int NIEBIESKI = 2; statyczny końcowy int MAGENTA = 3;

Lub możesz usunąć jeden:

statyczne końcowe int RED = 0; statyczny końcowy int NIEBIESKI = 1;

W obu przypadkach program nie będzie działał poprawnie. Jeśli usuniesz kolor, pojawi się błąd w czasie wykonywania, który zwraca uwagę na problem. Jeśli dodasz kolor, nie otrzymasz żadnego błędu - program po prostu nie uwzględni wszystkich wybranych kolorów.

Kolejną wadą jest brak czytelnego identyfikatora. Jeśli używasz okna komunikatu lub wyjścia konsoli do wyświetlenia bieżącego wyboru koloru, otrzymasz liczbę. To sprawia, że ​​debugowanie jest dość trudne.

Problemy z tworzeniem czytelnego identyfikatora są czasami rozwiązywane za pomocą statycznych stałych ciągów końcowych, na przykład:

statyczny końcowy String RED = "czerwony" .intern (); ...

Użycie tej intern()metody gwarantuje, że w wewnętrznej puli ciągów jest tylko jeden ciąg z taką zawartością. Ale intern()aby była skuteczna, każda zmienna łańcuchowa lub łańcuchowa, która jest kiedykolwiek porównywana z RED, musi jej używać. Nawet wtedy statyczne ciągi końcowe nie pozwalają na zapętlenie lub indeksowanie tablicy i nadal nie rozwiązują problemu bezpieczeństwa typów.

Bezpieczeństwo typów

Problem ze statycznymi końcowymi liczbami całkowitymi polega na tym, że zmienne, które ich używają, są z natury nieograniczone. Są to zmienne typu int, co oznacza, że ​​mogą przechowywać dowolną liczbę całkowitą, a nie tylko stałe, które miały przechowywać. Celem jest zdefiniowanie zmiennej typu Color, aby uzyskać błąd kompilacji, a nie błąd czasu wykonania, gdy do tej zmiennej zostanie przypisana nieprawidłowa wartość.

Eleganckie rozwiązanie zostało przedstawione w artykule Philipa Bishopa w JavaWorld „Stałe bezpieczne dla typów w C ++ i Javie”.

Pomysł jest naprawdę prosty (kiedy już go zobaczysz!):

publiczna klasa końcowa Color {// klasa końcowa !! private Color () {} // prywatny konstruktor !!

public static final Color RED = nowy Color (); public static final Color GREEN = nowy Color (); public static final Color BLUE = new Color (); }

Because the class is defined as final, it can't be subclassed. No other classes will be created from it. Because the constructor is private, other methods can't use the class to create new objects. The only objects that will ever be created with this class are the static objects the class creates for itself the first time the class is referenced! This implementation is a variation of the Singleton pattern that limits the class to a predefined number of instances. You can use this pattern to create exactly one class any time you need a Singleton, or use it as shown here to create a fixed number of instances. (The Singleton pattern is defined in the book Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, and Vlissides, Addison-Wesley, 1995. See the Resources section for a link to this book.)

The mind-boggling part of this class definition is that the class uses itself to create new objects. The first time you reference RED, it doesn't exist. But the act of accessing the class that RED is defined in causes it to be created, along with the other constants. Admittedly, that kind of recursive reference is rather difficult to visualize. But the advantage is total type safety. A variable of type Color can never be assigned anything other than the RED, GREEN, or BLUE objects that the Color class creates.

Identifiers

The first enhancement to the typesafe enumerated constant class is to create a string representation of the constants. You want to be able to produce a readable version of the value with a line like this:

 System.out.println(myColor); 

Whenever you output an object to a character output stream like System.out, and whenever you concatenate an object to a string, Java automatically invokes the toString() method for that object. That's a good reason to define a toString() method for any new class you create.

If the class does not have a toString() method, the inheritance hierarchy is inspected until one is found. At the top of the hierarchy, the toString() method in the Object class returns the class name. So the toString() method always has some meaning, but most of the time the default method will not be very useful.

Here is a modification to the Color class that provides a useful toString() method:

public final class Color { private String id; private Color(String anID) {this.id = anID; } public String toString() {return this.id; }

public static final Color RED = new Color(

"Red"

); public static final Color GREEN = new Color(

"Green"

); public static final Color BLUE = new Color(

"Blue"

); }

This version adds a private String variable (id). The constructor has been modified to take a String argument and store it as the object's ID. The toString() method then returns the object's ID.

One trick you can use to invoke the toString() method takes advantage of the fact that it is automatically invoked when an object is concatenated to a string. That means you could put the object's name in a dialog by concatenating it to a null string using a line like the following:

 textField1.setText("" + myColor); 

Unless you happen to love all the parentheses in Lisp, you will find that a bit more readable than the alternative:

 textField1.setText(myColor.toString()); 

It's also easier to make sure you put in the right number of closing parentheses!

Ordering and indexing

The next question is how to index into a vector or an array using members of the

Color

class. The mechanism will be to assign an ordinal number to each class constant and reference it using the attribute

.ord

, like this:

 void placePiece(int location, int color) { setPosition(location); display(piece[color.ord]); } 

Although tacking on .ord to convert the reference to color into a number is not particularly pretty, it is not terribly obtrusive either. It seems like a fairly reasonable tradeoff for typesafe constants.

Here is how the ordinal numbers are assigned:

public final class Color { private String id; public final int ord;private static int upperBound = 0; private Color(String anID) { this.id = anID; this.ord = upperBound++; } public String toString() {return this.id; } public static int size() { return upperBound; }

public static final Color RED = new Color("Red"); public static final Color GREEN = new Color("Green"); public static final Color BLUE = new Color("Blue"); }

This code uses the new JDK version 1.1 definition of a "blank final" variable -- a variable that is assigned a value once and once only. This mechanism allows each object to have its own non-static final variable, ord, which will be assigned once during object creation and which will thereafter remain immutable. The static variable upperBound keeps track of the next unused index in the collection. That value becomes the ord attribute when the object is created, after which the upper bound is incremented.

For compatibility with the Vector class, the method size() is defined to return the number of constants that have been defined in this class (which is the same as the upper bound).

A purist might decide that the variable ord should be private, and the method named ord() should return it -- if not, a method named getOrd(). I lean toward accessing the attribute directly, though, for two reasons. The first is that the concept of an ordinal is unequivocally that of an int. There is little likelihood, if any, that the implementation would ever change. The second reason is that what you really want is the ability to use the object as though it were an int, as you could in a language like Pascal. For example, you might want to use the attribute color to index an array. But you cannot use a Java object to do that directly. What you would really like to say is:

 display(piece[color]); // desirable, but does not work 

But you can't do that. The minimum change necessary to get what you want is to access an attribute, instead, like this:

 display(piece[color.ord]); // closest to desirable 

instead of the lengthy alternative:

 display(piece[color.ord()]); // extra parentheses 

or the even lengthier:

 display(piece[color.getOrd()]); // extra parentheses and text 

The Eiffel language uses the same syntax for accessing attributes and invoking methods. That would be the ideal. Given the necessity of choosing one or the other, however, I've gone with accessing ord as an attribute. With any luck, the identifier ord will become so familiar as a result of repetition that using it will seem as natural as writing int. (As natural as that may be.)

Looping

Następnym krokiem jest możliwość iteracji stałych klas. Chcesz mieć możliwość zapętlenia od początku do końca:

 for (Color c = Color.first (); c! = null; c = c.next ()) {...} 

lub od końca do początku:

 for (Color c = Color.last (); c! = null; c = c.prev ()) {...} 

Te modyfikacje wykorzystują zmienne statyczne, aby śledzić ostatnio utworzony obiekt i łączyć go z następnym obiektem: