Buat pemalar yang dihitung di Java

Satu set "pemalar yang dapat dikira" adalah kumpulan pemalar teratur yang boleh dikira, seperti nombor. Properti itu membolehkan anda menggunakannya seperti nombor untuk mengindeks array, atau anda boleh menggunakannya sebagai pemboleh ubah indeks dalam gelung untuk. Di Jawa, objek seperti itu paling sering dikenal sebagai "pemalar yang dihitung."

Menggunakan pemalar yang dihitung dapat menjadikan kod lebih mudah dibaca. Sebagai contoh, anda mungkin ingin menentukan jenis data baru yang dinamakan Warna dengan pemalar MERAH, HIJAU, dan BIRU sebagai nilai yang mungkin. Ideanya ialah menjadikan Warna sebagai atribut objek lain yang anda buat, seperti objek Kereta:

kereta kelas {Warna warna; ...}

Kemudian anda boleh menulis kod yang jelas dan boleh dibaca, seperti ini:

 myCar.color = MERAH; 

bukannya seperti:

 myCar.color = 3; 

Atribut yang lebih penting bagi pemalar yang dihitung dalam bahasa seperti Pascal ialah ketiknya selamat. Dengan kata lain, tidak mungkin menetapkan warna yang tidak sah ke atribut warna - ia mesti selalu berwarna MERAH, HIJAU, atau BIRU. Sebaliknya, jika pembolehubah warna adalah int, maka Anda dapat menetapkan bilangan bulat yang valid, bahkan jika angka itu tidak mewakili warna yang valid.

Artikel ini memberi anda templat untuk membuat pemalar yang dihitung yang:

  • Taip selamat
  • Boleh dicetak
  • Dipesan, untuk digunakan sebagai indeks
  • Terpaut, untuk gelung ke hadapan atau ke belakang
  • Tidak terkira

Dalam artikel yang akan datang, anda akan belajar bagaimana memperluas pemalar yang dihitung untuk melaksanakan perilaku yang bergantung pada keadaan.

Mengapa tidak menggunakan final statik?

Mekanisme biasa untuk pemalar yang dihitung menggunakan pemboleh ubah int akhir statik, seperti ini:

int statik akhir RED = 0; akhir statik int HIJAU = 1; int statik akhir BLUE = 2; ...

Final statik berguna

Kerana ia adalah muktamad, nilainya tetap dan tidak dapat ditukar. Kerana ia statik, mereka hanya dibuat sekali untuk kelas atau antara muka di mana mereka ditentukan, bukan sekali untuk setiap objek. Dan kerana mereka adalah pemboleh ubah integer, mereka dapat dihitung dan digunakan sebagai indeks.

Sebagai contoh, anda boleh menulis gelung untuk membuat senarai warna kegemaran pelanggan:

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

Anda juga dapat mengindeks ke dalam array atau vektor menggunakan pemboleh ubah untuk mendapatkan nilai yang berkaitan dengan warna. Sebagai contoh, anggap anda mempunyai permainan papan yang mempunyai kepingan warna yang berbeza untuk setiap pemain. Katakan anda mempunyai bitmap untuk setiap kepingan warna dan kaedah yang disebut display()yang menyalin bitmap tersebut ke lokasi semasa. Salah satu cara meletakkan kepingan di papan mungkin seperti ini:

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

batal placePiece (int lokasi, int warna) {setPosition (lokasi); jika (warna == MERAH) {paparan (redPiece); } lain jika (warna == HIJAU) {paparan (greenPiece); } lain {paparan (bluePiece); }}

Tetapi dengan menggunakan nilai bilangan bulat untuk mengindeks menjadi sebilangan keping, anda dapat mempermudah kodnya untuk:

PiecePicture [] piece = {PiecePicture baru (MERAH), PiecePicture baru (HIJAU), PiecePicture baru (BIRU)}; batal placePiece (int lokasi, int warna) {setPosition (lokasi); paparan (sekeping [warna]); }

Mampu melancarkan pelbagai pemalar dan indeks ke dalam array atau vektor adalah kelebihan utama bilangan bulat akhir statik. Dan apabila jumlah pilihan bertambah, kesan penyederhanaan akan semakin besar.

Tetapi final statik berisiko

Namun, terdapat beberapa kelemahan untuk menggunakan bilangan bulat akhir statik. Kelemahan utama adalah kekurangan keselamatan jenis. Setiap bilangan bulat yang dihitung atau dibaca boleh digunakan sebagai "warna", tidak kira sama ada masuk akal untuk melakukannya. Anda boleh melengkung melewati akhir pemalar yang ditentukan atau berhenti menutup semuanya, yang mudah berlaku jika anda menambah atau mengeluarkan pemalar dari senarai tetapi lupa untuk menyesuaikan indeks gelung.

Contohnya, gelung pilihan warna anda mungkin berbunyi seperti ini:

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

Kemudian, anda mungkin menambah warna baru:

int statik akhir RED = 0; akhir statik int HIJAU = 1; int statik akhir BLUE = 2; akhir statik MAGENTA = 3;

Atau anda mungkin membuangnya:

int statik akhir RED = 0; int statik akhir BLUE = 1;

Dalam kedua-dua kes, program ini tidak akan beroperasi dengan betul. Sekiranya anda mengeluarkan warna, anda akan mendapat ralat runtime yang menarik perhatian kepada masalah tersebut. Sekiranya anda menambah warna, anda tidak akan mendapat kesalahan sama sekali - program ini akan gagal merangkumi semua pilihan warna.

Kelemahan lain ialah kekurangan pengecam yang boleh dibaca. Sekiranya anda menggunakan kotak mesej atau output konsol untuk memaparkan pilihan warna semasa, anda akan mendapat nombor. Itu menjadikan penyahpepijatan agak sukar.

Masalah membuat pengecam yang dapat dibaca kadang-kadang diselesaikan menggunakan pemalar rentetan akhir statik, seperti ini:

String akhir statik RED = "red" .intern (); ...

Dengan menggunakan intern()kaedah tersebut, hanya ada satu tali dengan kandungannya di kumpulan tali dalaman. Tetapi untuk intern()menjadi berkesan, setiap rentetan atau pemboleh ubah rentetan yang pernah dibandingkan dengan RED mesti menggunakannya. Walaupun begitu, rentetan akhir statik tidak memungkinkan untuk menggulung atau mengindeks ke dalam array, dan mereka masih tidak menangani masalah keselamatan jenis.

Jenis keselamatan

Masalah dengan bilangan bulat akhir statik adalah bahawa pemboleh ubah yang menggunakannya secara semula jadi tidak terikat. Mereka adalah pemboleh ubah int, yang bermaksud mereka dapat menahan bilangan bulat, bukan hanya pemalar yang ingin mereka pegang. Tujuannya adalah untuk menentukan pemboleh ubah jenis Warna sehingga anda mendapat ralat kompilasi dan bukannya ralat runtime setiap kali nilai tidak sah diberikan pada pemboleh ubah tersebut.

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 seterusnya ialah melakukan lelaran terhadap pemalar kelas. Anda mahu dapat gelung dari awal hingga akhir:

 untuk (Warna c = Warna. pertama (); c! = null; c = c.next ()) {...} 

atau dari akhir hingga awal:

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

Pengubahsuaian ini menggunakan pemboleh ubah statik untuk melacak objek terakhir yang dibuat dan menghubungkannya ke objek seterusnya: