Buat konstanta terenumerasi di Java

Sekumpulan "konstanta yang dapat dihitung" adalah kumpulan konstanta yang dapat dihitung, seperti angka. Properti itu memungkinkan Anda menggunakannya seperti angka untuk mengindeks array, atau Anda dapat menggunakannya sebagai variabel indeks dalam perulangan for. Di Java, objek seperti itu paling sering dikenal sebagai "konstanta yang disebutkan".

Menggunakan konstanta yang disebutkan dapat membuat kode lebih mudah dibaca. Misalnya, Anda mungkin ingin menentukan tipe data baru bernama Warna dengan konstanta MERAH, HIJAU, dan BIRU sebagai kemungkinan nilainya. Idenya adalah memiliki Warna sebagai atribut objek lain yang Anda buat, seperti objek Mobil:

kelas Mobil {Warna warna; ...}

Kemudian Anda dapat menulis kode yang jelas dan dapat dibaca, seperti ini:

 myCar.color = MERAH; 

alih-alih sesuatu seperti:

 myCar.color = 3; 

Atribut yang lebih penting dari konstanta enumerasi dalam bahasa seperti Pascal adalah bahwa mereka bertipe aman. Dengan kata lain, tidak mungkin untuk menetapkan warna yang tidak valid ke atribut warna - warna harus selalu MERAH, HIJAU, atau BIRU. Sebaliknya, jika variabel warna adalah int, maka Anda dapat menetapkan bilangan bulat yang valid ke dalamnya, meskipun nomor tersebut tidak mewakili warna yang valid.

Artikel ini memberi Anda template untuk membuat konstanta yang dihitung yaitu:

  • Ketik aman
  • Dapat dicetak
  • Dipesan, untuk digunakan sebagai indeks
  • Ditautkan, untuk melakukan perulangan ke depan atau ke belakang
  • Tak terhitung

Di artikel mendatang, Anda akan mempelajari cara memperluas konstanta yang disebutkan untuk mengimplementasikan perilaku yang bergantung pada keadaan.

Mengapa tidak menggunakan final statis?

Mekanisme umum untuk konstanta yang disebutkan menggunakan variabel int akhir statis, seperti ini:

RED akhir statis = 0; static final int GREEN = 1; BIRU int akhir statis = 2; ...

Final statis berguna

Karena bersifat final, nilainya konstan dan tidak dapat diubah. Karena bersifat statis, mereka hanya dibuat sekali untuk kelas atau antarmuka tempat mereka didefinisikan, bukan sekali untuk setiap objek. Dan karena mereka adalah variabel integer, mereka dapat dihitung dan digunakan sebagai indeks.

Misalnya, Anda dapat menulis lingkaran untuk membuat daftar warna favorit pelanggan:

untuk (int i = 0; ...) {if (customerLikesColor (i)) {favoriteColors.add (i); }}

Anda juga dapat mengindeks ke dalam array atau vektor menggunakan variabel untuk mendapatkan nilai yang terkait dengan warna. Misalnya, Anda memiliki permainan papan yang memiliki bidak berwarna berbeda untuk setiap pemain. Katakanlah Anda memiliki bitmap untuk setiap potongan warna dan metode yang disebut display()menyalin bitmap itu ke lokasi saat ini. Salah satu cara untuk meletakkan sepotong di papan tulis mungkin seperti ini:

PiecePicture redPiece = PiecePicture baru (MERAH); PiecePicture greenPiece = PiecePicture baru (HIJAU); PiecePicture bluePiece = PiecePicture baru (BLUE);

void placePiece (lokasi int, warna int) {setPosition (lokasi); if (color == RED) {display (redPiece); } lain jika (color == GREEN) {display (greenPiece); } lain {tampilan (bluePiece); }}

Tetapi dengan menggunakan nilai integer untuk mengindeks menjadi serangkaian potongan, Anda dapat menyederhanakan kode untuk:

PiecePicture [] buah = {PiecePicture baru (MERAH), PiecePicture baru (HIJAU), PiecePicture baru (BIRU)}; void placePiece (lokasi int, warna int) {setPosition (lokasi); tampilan (bagian [warna]); }

Mampu mengulang berbagai konstanta dan indeks ke dalam array atau vektor adalah keuntungan utama dari bilangan bulat akhir statis. Dan ketika jumlah pilihan bertambah, efek penyederhanaan menjadi lebih besar.

Tapi final statis berisiko

Namun, ada beberapa kelemahan menggunakan bilangan bulat final statis. Kelemahan utama adalah kurangnya keamanan tipe. Setiap bilangan bulat yang dihitung atau dibaca dapat digunakan sebagai "warna", terlepas dari apakah masuk akal untuk dilakukan. Anda dapat melakukan loop tepat melewati akhir konstanta yang ditentukan atau berhenti untuk menutupi semuanya, yang dapat dengan mudah terjadi jika Anda menambah atau menghapus konstanta dari daftar tetapi lupa untuk menyesuaikan indeks loop.

Misalnya, lingkaran preferensi warna Anda mungkin terbaca seperti ini:

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

Nanti, Anda bisa menambahkan warna baru:

RED akhir statis = 0; static final int GREEN = 1; BIRU int akhir statis = 2; statis akhir int MAGENTA = 3;

Atau Anda mungkin menghapus satu:

RED akhir statis = 0; BIRU int akhir statis = 1;

Dalam kedua kasus tersebut, program tidak akan beroperasi dengan benar. Jika Anda menghapus warna, Anda akan mendapatkan error runtime yang menarik perhatian ke masalah tersebut. Jika Anda menambahkan warna, Anda tidak akan mendapatkan kesalahan sama sekali - program akan gagal menutupi semua pilihan warna.

Kelemahan lainnya adalah kurangnya pengenal yang bisa dibaca. Jika Anda menggunakan kotak pesan atau keluaran konsol untuk menampilkan pilihan warna saat ini, Anda mendapatkan nomor. Itu membuat debugging menjadi cukup sulit.

Masalah dalam membuat pengenal yang dapat dibaca terkadang diselesaikan menggunakan konstanta string akhir statis, seperti ini:

static final String RED = "red" .intern (); ...

Menggunakan intern()metode ini menjamin hanya ada satu string dengan konten tersebut di kumpulan string internal. Namun intern()agar efektif, setiap string atau variabel string yang pernah dibandingkan dengan RED harus menggunakannya. Meskipun demikian, string final statis tidak memungkinkan perulangan atau pengindeksan ke dalam array, dan string tersebut tetap tidak mengatasi masalah keamanan tipe.

Ketik keamanan

Masalah dengan bilangan bulat final statis adalah variabel yang menggunakannya secara inheren tidak terbatas. Mereka adalah variabel int, yang berarti mereka dapat menyimpan bilangan bulat apa pun, bukan hanya konstanta yang dimaksudkan untuk dipegang. Tujuannya adalah untuk menentukan variabel jenis Warna sehingga Anda mendapatkan kesalahan kompilasi daripada kesalahan waktu proses setiap kali nilai yang tidak valid ditetapkan ke variabel itu.

An elegant solution was provided in Philip Bishop's article in JavaWorld, "Typesafe constants in C++ and Java."

The idea is really simple (once you see it!):

public final class Color { // final class!! private Color() {} // private constructor!!

public static final Color RED = new Color(); public static final Color GREEN = new 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

Langkah selanjutnya adalah mampu melakukan iterasi terhadap konstanta kelas. Anda ingin dapat mengulang dari awal hingga akhir:

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

atau dari akhir kembali ke awal:

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

Modifikasi ini menggunakan variabel statis untuk melacak objek terakhir yang dibuat dan menautkannya ke objek berikutnya: