Gunakan tipe konstan untuk kode yang lebih aman dan lebih bersih

Dalam tutorial ini akan memperluas ide konstanta enumerasi seperti yang dibahas dalam Eric Armstrong, "Buat konstanta terenumerasi di Java." Saya sangat menyarankan membaca artikel itu sebelum Anda membenamkan diri dalam artikel ini, karena saya akan berasumsi bahwa Anda sudah familiar dengan konsep yang terkait dengan konstanta yang disebutkan, dan saya akan memperluas beberapa contoh kode yang disajikan Eric.

Konsep konstanta

Dalam menangani konstanta enumerasi, saya akan membahas bagian konsep enumerasi di akhir artikel. Untuk saat ini, kami hanya akan fokus pada aspek konstan . Konstanta pada dasarnya adalah variabel yang nilainya tidak dapat berubah. Dalam C / C ++, kata kunci constdigunakan untuk mendeklarasikan variabel konstan ini. Di Java, Anda menggunakan kata kunci final. Namun, alat yang diperkenalkan di sini bukan hanya variabel primitif; itu adalah contoh objek yang sebenarnya. Instance objek tidak dapat diubah dan tidak dapat diubah - status internalnya tidak dapat diubah. Ini mirip dengan pola singleton, di mana sebuah kelas hanya dapat memiliki satu instance tunggal; dalam kasus ini, bagaimanapun, kelas mungkin hanya memiliki sekumpulan contoh yang terbatas dan telah ditentukan sebelumnya.

Alasan utama menggunakan konstanta adalah kejelasan dan keamanan. Misalnya, potongan kode berikut tidak cukup jelas:

public void setColor (int x) {...} public void someMethod () {setColor (5); }

Dari kode ini, kita dapat memastikan bahwa suatu warna sedang disetel. Tapi warna apa yang diwakili 5? Jika kode ini ditulis oleh salah satu programmer langka yang mengomentari karyanya, kami mungkin menemukan jawabannya di bagian atas file. Tetapi kemungkinan besar kita harus mencari beberapa dokumen desain lama (jika memang ada) untuk penjelasan.

Solusi yang lebih jelas adalah dengan memberikan nilai 5 ke variabel dengan nama yang bermakna. Sebagai contoh:

RED public int static final = 5; public void someMethod () {setColor (RED); }

Sekarang kita dapat langsung mengetahui apa yang terjadi dengan kode tersebut. Warnanya disetel ke merah. Ini jauh lebih bersih, tetapi apakah lebih aman? Bagaimana jika pembuat kode lain bingung dan menyatakan nilai yang berbeda seperti ini:

RED public int static final = 3; HIJAU public int static final = 5;

Sekarang kita punya dua masalah. Pertama-tama, REDtidak lagi disetel ke nilai yang benar. Kedua, nilai merah diwakili oleh variabel bernama GREEN. Mungkin bagian yang paling menakutkan adalah kode ini akan dikompilasi dengan baik, dan bug mungkin tidak terdeteksi sampai produk telah dikirimkan.

Kami dapat memperbaiki masalah ini dengan membuat kelas warna definitif:

Warna kelas publik {public static final int RED = 5; HIJAU public int static final = 7; }

Kemudian, melalui dokumentasi dan tinjauan kode, kami mendorong pemrogram untuk menggunakannya seperti ini:

public void someMethod () {setColor (Color.RED); }

Saya katakan mendorong karena desain dalam daftar kode itu tidak memungkinkan kita untuk memaksa pembuat kode untuk mematuhinya; kode akan tetap dikompilasi meskipun semuanya tidak beres. Jadi, meskipun ini sedikit lebih aman, ini tidak sepenuhnya aman. Meskipun pemrogram harus menggunakan Colorkelas, mereka tidak diharuskan. Pemrogram dapat dengan mudah menulis dan mengompilasi kode berikut:

 setColor (3498910); 

Apakah setColormetode ini mengenali angka besar ini sebagai warna? Mungkin tidak. Jadi bagaimana kita bisa melindungi diri kita dari programmer nakal ini? Di situlah jenis konstanta datang untuk menyelamatkan.

Kami mulai dengan mendefinisikan kembali tanda tangan metode:

 public void setColor (Warna x) {...} 

Sekarang programmer tidak dapat memberikan nilai integer yang berubah-ubah. Mereka dipaksa untuk memberikan Colorobjek yang valid . Contoh penerapannya mungkin terlihat seperti ini:

public void someMethod () {setColor (Color baru ("Red")); }

Kami masih bekerja dengan kode yang bersih dan dapat dibaca, dan kami semakin dekat untuk mencapai keamanan mutlak. Tapi kami belum sampai di sana. Pemrogram masih memiliki ruang untuk membuat kekacauan dan dapat membuat warna baru secara sewenang-wenang seperti:

public void someMethod () {setColor (Color baru ("Hai, nama saya Ted.")); }

Kami mencegah situasi ini dengan membuat Colorkelas tidak dapat diubah dan menyembunyikan instance dari pemrogram. Kami membuat setiap jenis warna yang berbeda (merah, hijau, biru) menjadi tunggal. Ini dilakukan dengan membuat konstruktor pribadi dan kemudian mengekspos pegangan publik ke daftar contoh yang terbatas dan terdefinisi dengan baik:

kelas publik Warna {Warna pribadi () {} warna akhir statis publik MERAH = Warna baru (); Warna public static akhir HIJAU = Warna baru (); Warna BIRU public static akhir = Warna baru (); }

Dalam kode ini kami akhirnya mencapai keamanan mutlak. Programmer tidak dapat membuat warna palsu. Hanya warna yang ditentukan yang dapat digunakan; jika tidak, program tidak akan bisa dikompilasi. Beginilah tampilan implementasi kami sekarang:

public void someMethod () {setColor (Color.RED); }

Kegigihan

Oke, sekarang kita punya cara yang bersih dan aman untuk menangani tipe konstan. Kita bisa membuat objek dengan atribut color dan memastikan nilai warna akan selalu valid. Tetapi bagaimana jika kita ingin menyimpan objek ini dalam database atau menulisnya ke file? Bagaimana cara menyimpan nilai warna? Kita harus memetakan tipe ini ke nilai.

Dalam artikel JavaWorld yang disebutkan di atas, Eric Armstrong menggunakan nilai string. Menggunakan string memberikan bonus tambahan berupa memberi Anda sesuatu yang berarti untuk dikembalikan dalam toString()metode, yang membuat keluaran debugging menjadi sangat jelas.

Namun, string bisa jadi mahal untuk disimpan. Integer membutuhkan 32 bit untuk menyimpan nilainya sementara string membutuhkan 16 bit per karakter (karena dukungan Unicode). Misalnya, angka 49858712 dapat disimpan dalam 32 bit, tetapi string TURQUOISEtersebut membutuhkan 144 bit. Jika Anda menyimpan ribuan objek dengan atribut warna, perbedaan bit yang relatif kecil ini (antara 32 dan 144 dalam kasus ini) dapat bertambah dengan cepat. Jadi mari gunakan nilai integer sebagai gantinya. Apa solusi untuk masalah ini? Kami akan mempertahankan nilai string, karena itu penting untuk presentasi, tetapi kami tidak akan menyimpannya.

Versi Java dari 1.1 hingga dapat membuat serial objek secara otomatis, selama mereka mengimplementasikan Serializableantarmukanya. Untuk mencegah Java menyimpan data asing, Anda harus mendeklarasikan variabel tersebut dengan transientkata kunci. Jadi, untuk menyimpan nilai integer tanpa menyimpan representasi string, kami mendeklarasikan atribut string menjadi transient. Inilah kelas baru, bersama dengan aksesor ke atribut integer dan string:

public class Color mengimplementasikan java.io.Serializable {private int value; nama String transien pribadi; public static final Color RED = new Color (0, "Red"); Warna BIRU public static final = Warna baru (1, "Biru"); Warna public static akhir HIJAU = Warna baru (2, "Hijau"); Warna pribadi (nilai int, nama String) {this.value = value; this.name = name; } public int getValue () {nilai kembali; } public String toString () {return name; }}

Sekarang kita dapat secara efisien menyimpan instance tipe konstan Color. Tapi bagaimana dengan memulihkannya? Itu akan menjadi sedikit rumit. Sebelum kita melangkah lebih jauh, mari kita kembangkan ini menjadi kerangka kerja yang akan menangani semua perangkap yang disebutkan di atas untuk kita, memungkinkan kita untuk fokus pada masalah sederhana dalam menentukan jenis.

Kerangka tipe konstan

With our firm understanding of constant types, I can now jump into this month's tool. The tool is called Type and it is a simple abstract class. All you have to do is create a very simple subclass and you've got a full-featured constant type library. Here's what our Color class will look like now:

public class Color extends Type { protected Color( int value, String desc ) { super( value, desc ); } public static final Color RED = new Color( 0, "Red" ); public static final Color BLUE = new Color( 1, "Blue" ); public static final Color GREEN = new Color( 2, "Green" ); } 

The Color class consists of nothing but a constructor and a few publicly accessible instances. All of the logic discussed to this point will be defined and implemented in the superclass Type; we'll be adding more as we go along. Here's what Type looks like so far:

public class Type implements java.io.Serializable { private int value; private transient String name; protected Type( int value, String name ) { this.value = value; this.name = name; } public int getValue() { return value; } public String toString() { return name; } } 

Back to persistence

With our new framework in hand, we can continue where we left off in the discussion of persistence. Remember, we can save our types by storing their integer values, but now we want to restore them. This is going to require a lookup -- a reverse calculation to locate the object instance based on its value. In order to perform a lookup, we need a way to enumerate all of the possible types.

In Eric's article, he implemented his own enumeration by implementing the constants as nodes in a linked list. I'm going to forego this complexity and use a simple hashtable instead. The key for the hash will be the integer values of the type (wrapped in an Integer object), and the value of the hash will be a reference to the type instance. For example, the GREEN instance of Color would be stored like so:

 hashtable.put( new Integer( GREEN.getValue() ), GREEN ); 

Of course, we don't want to type this out for each possible type. There could be hundreds of different values, thus creating a typing nightmare and opening the doors to some nasty problems -- you might forget to put one of the values in the hashtable and then not be able to look it up later, for instance. So we'll declare a global hashtable within Type and modify the constructor to store the mapping upon creation:

 private static final Hashtable types = new Hashtable(); protected Type( int value, String desc ) { this.value = value; this.desc = desc; types.put( new Integer( value ), this ); } 

But this creates a problem. If we have a subclass called Color, which has a type (that is, Green) with a value of 5, and then we create another subclass called Shade, which also has a type (that is Dark) with a value of 5, only one of them will be stored in the hashtable -- the last one to be instantiated.

In order to avoid this, we have to store a handle to the type based on not only its value, but also its class. Let's create a new method to store the type references. We'll use a hashtable of hashtables. The inner hashtable will be a mapping of values to types for each specific subclass (Color, Shade, and so on). The outer hashtable will be a mapping of subclasses to inner tables.

This routine will first attempt to acquire the inner table from the outer table. If it receives a null, the inner table doesn't exist yet. So, we create a new inner table and put it into the outer table. Next, we add the value/type mapping to the inner table and we're done. Here's the code:

 private void storeType( Type type ) { String className = type.getClass().getName(); Hashtable values; synchronized( types ) // avoid race condition for creating inner table { values = (Hashtable) types.get( className ); if( values == null ) { values = new Hashtable(); types.put( className, values ); } } values.put( new Integer( type.getValue() ), type ); } 

And here's the new version of the constructor:

 protected Type( int value, String desc ) { this.value = value; this.desc = desc; storeType( this ); } 

Now that we are storing a road map of types and values, we can perform lookups and thus restore an instance based on a value. The lookup requires two things: the target subclass identity and the integer value. Using this information, we can extract the inner table and find the handle to the matching type instance. Here's the code:

 public static Type getByValue( Class classRef, int value ) { Type type = null; String className = classRef.getName(); Hashtable values = (Hashtable) types.get( className ); if( values != null ) { type = (Type) values.get( new Integer( value ) ); } return( type ); } 

Thus, restoring a value is as simple as this (note that the return value must be casted):

 int value = // read from file, database, etc. Color background = (ColorType) Type.findByValue( ColorType.class, value ); 

Enumerating the types

Berkat organisasi hashtable-of-hashtables kami, sangatlah mudah untuk mengekspos fungsionalitas enumerasi yang ditawarkan oleh implementasi Eric. Satu-satunya peringatan adalah penyortiran, yang ditawarkan oleh desain Eric, tidak dijamin. Jika Anda menggunakan Java 2, Anda dapat mengganti peta yang diurutkan untuk hashtable bagian dalam. Tapi, seperti yang saya nyatakan di awal kolom ini, saya hanya peduli dengan versi 1.1 JDK sekarang.

Satu-satunya logika yang diperlukan untuk menghitung tipe adalah untuk mengambil tabel dalam dan mengembalikan daftar elemennya. Jika tabel bagian dalam tidak ada, kami hanya mengembalikan null. Berikut seluruh metode: