Gunakan jenis tetap untuk kod yang lebih selamat dan bersih

Dalam tutorial ini akan dikembangkan gagasan pemalar yang dihitung seperti yang tercakup dalam Eric Armstrong, "Buat pemalar yang dihitung di Jawa." Saya sangat mengesyorkan membaca artikel itu sebelum anda melibatkan diri dalam artikel ini, kerana saya akan menganggap bahawa anda sudah biasa dengan konsep yang berkaitan dengan pemalar yang dihitung, dan saya akan memperluas beberapa kod contoh yang dikemukakan oleh Eric.

Konsep pemalar

Dalam menangani pemalar yang dihitung, saya akan membincangkan bahagian konsep yang dihitung di akhir artikel. Buat masa ini, kami hanya akan menumpukan pada aspek yang berterusan . Pemalar pada dasarnya adalah pemboleh ubah yang nilainya tidak dapat berubah. Dalam C / C ++, kata kunci constdigunakan untuk menyatakan pemboleh ubah tetap ini. Di Jawa, anda menggunakan kata kunci final. Walau bagaimanapun, alat yang diperkenalkan di sini bukan sekadar pemboleh ubah primitif; itu adalah contoh objek sebenar. Contoh objek tidak berubah dan tidak dapat diubah - keadaan dalamannya mungkin tidak dapat diubah. Ini serupa dengan corak singleton, di mana kelas hanya boleh mempunyai satu contoh tunggal; dalam kes ini, bagaimanapun, kelas mungkin hanya mempunyai sekumpulan contoh yang terhad dan ditentukan sebelumnya.

Sebab utama penggunaan pemalar adalah kejelasan dan keselamatan. Contohnya, kod berikut tidak dapat dijelaskan sendiri:

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

Dari kod ini, kita dapat memastikan bahawa warna sedang ditetapkan. Tetapi warna apa yang diwakili oleh 5? Sekiranya kod ini ditulis oleh salah seorang pengaturcara yang jarang berkomentar mengenai kerjanya, kami mungkin mendapat jawapannya di bahagian atas fail. Tetapi kemungkinan besar kita perlu mencari-cari beberapa dokumen reka bentuk lama (jika ada) untuk penjelasan.

Penyelesaian yang lebih jelas adalah memberikan nilai 5 kepada pemboleh ubah dengan nama yang bermakna. Sebagai contoh:

akhir statik awam RED = 5; awam membatalkan someMethod () {setColor (RED); }

Sekarang kita dapat mengetahui dengan segera apa yang berlaku dengan kod tersebut. Warnanya diatur menjadi merah. Ini jauh lebih bersih, tetapi adakah lebih selamat? Bagaimana jika pengekod lain keliru dan menyatakan nilai yang berbeza seperti:

final statik awam RED = 3; final statik awam HIJAU = 5;

Sekarang kita mempunyai dua masalah. Pertama sekali, REDtidak lagi ditetapkan pada nilai yang betul. Kedua, nilai untuk warna merah ditunjukkan oleh pemboleh ubah yang dinamakan GREEN. Mungkin bahagian yang paling menakutkan adalah bahawa kod ini akan disusun dengan baik, dan pepijat mungkin tidak dapat dikesan sehingga produk dihantar.

Kami dapat menyelesaikan masalah ini dengan membuat kelas warna pasti:

warna kelas awam {public static final int RED = 5; final statik awam int HIJAU = 7; }

Kemudian, melalui dokumentasi dan tinjauan kod, kami menggalakkan pengaturcara menggunakannya seperti itu:

awam membatalkan someMethod () {setColor (Color.RED); }

Saya katakan menggalakkan kerana reka bentuk dalam kod penyenaraian itu tidak membenarkan kami memaksa pengekod untuk mematuhi; kod tetap akan disusun walaupun semuanya tidak teratur. Oleh itu, walaupun ini sedikit lebih selamat, ia tidak selamat sepenuhnya. Walaupun pengaturcara harus menggunakan Colorkelas, mereka tidak diharuskan. Pengaturcara dapat menulis dan menyusun kod berikut dengan mudah:

 setColor (3498910); 

Adakah setColorkaedah mengenali sebilangan besar ini menjadi warna? Mungkin tidak. Oleh itu, bagaimana kita dapat melindungi diri daripada pengaturcara nakal ini? Di situlah jenis pemalar datang untuk menyelamatkan.

Kami mulakan dengan mentakrifkan semula tandatangan kaedah:

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

Kini pengaturcara tidak dapat memasukkan nilai integer sewenang-wenangnya. Mereka terpaksa memberikan Colorobjek yang sah . Contoh pelaksanaan ini mungkin kelihatan seperti ini:

awam membatalkan someMethod () {setColor (Warna baru ("Merah")); }

Kami masih menggunakan kod yang bersih dan mudah dibaca, dan kami lebih dekat untuk mencapai keselamatan mutlak. Tetapi kita belum sampai di sana. Pengaturcara masih mempunyai ruang untuk menimbulkan kekacauan dan sewenang-wenangnya dapat membuat warna baru seperti:

awam membatalkan someMethod () {setColor (Warna baru ("Hai, nama saya Ted.")); }

Kami mengelakkan keadaan ini dengan menjadikan Colorkelas tidak berubah dan menyembunyikan contoh dari pengaturcara. Kami menjadikan setiap jenis warna yang berbeza (merah, hijau, biru) sebagai singleton. Ini dicapai dengan menjadikan konstruktor peribadi dan kemudian memperlihatkan pegangan awam kepada senarai contoh yang terhad dan jelas:

kelas awam Warna {warna peribadi () {} warna akhir statik awam MERAH = Warna baru (); warna akhir statik awam HIJAU = Warna baru (); warna akhir statik awam BIRU = Warna baru (); }

Dalam kod ini, kita akhirnya mencapai keselamatan mutlak. Pengaturcara tidak boleh memalsukan warna palsu. Hanya warna yang ditentukan boleh digunakan; jika tidak, program tidak akan disusun. Ini adalah bagaimana pelaksanaan kami sekarang:

awam membatalkan someMethod () {setColor (Color.RED); }

Kegigihan

Baiklah, sekarang kita mempunyai kaedah yang bersih dan selamat untuk menangani jenis yang berterusan. Kita dapat membuat objek dengan atribut warna dan yakin bahawa nilai warna akan selalu berlaku. Tetapi bagaimana jika kita mahu menyimpan objek ini dalam pangkalan data atau menulisnya ke fail? Bagaimana kita menyimpan nilai warna? Kita mesti memetakan jenis ini kepada nilai.

Dalam artikel JavaWorld yang disebutkan di atas, Eric Armstrong menggunakan nilai rentetan. Menggunakan tali memberikan bonus tambahan untuk memberi anda sesuatu yang bermakna untuk dikembalikan dalam toString()kaedah ini, yang menjadikan output debugging sangat jelas.

Walaupun begitu, tali boleh mahal. Bilangan bulat memerlukan 32 bit untuk menyimpan nilainya sementara rentetan memerlukan 16 bit per aksara (kerana sokongan Unicode). Sebagai contoh, nombor 49858712 dapat disimpan dalam 32 bit, tetapi rentetan TURQUOISEmemerlukan 144 bit. Sekiranya anda menyimpan ribuan objek dengan atribut warna, perbezaan bit yang agak kecil ini (antara 32 dan 144 dalam kes ini) dapat bertambah dengan cepat. Oleh itu, mari kita gunakan nilai integer. Apakah jalan keluar untuk masalah ini? Kami akan mengekalkan nilai rentetan, kerana ia penting untuk persembahan, tetapi kami tidak akan menyimpannya.

Versi Java dari 1.1 dan seterusnya dapat membuat siri objek secara automatik, selagi ia melaksanakan Serializableantara muka. Untuk mengelakkan Java menyimpan data asing, anda mesti menyatakan pemboleh ubah tersebut dengan transientkata kunci. Oleh itu, untuk menyimpan nilai integer tanpa menyimpan perwakilan rentetan, kami menyatakan atribut string sebagai sementara. Inilah kelas baru, bersama dengan aksesori ke atribut integer dan rentetan:

warna kelas awam melaksanakan java.io.Serializable {nilai int peribadi; nama String sementara peribadi; warna akhir statik awam MERAH = Warna baru (0, "Merah"); warna akhir statik awam BLUE = Warna baru (1, "Biru"); warna akhir statik awam HIJAU = Warna baru (2, "Hijau"); Warna peribadi (nilai int, Nama rentetan) {this.value = value; ini.nama = nama; } public int getValue () {nilai pulangan; } String toString awam () {nama pengembalian; }}

Sekarang kita dapat menyimpan contoh jenis pemalar dengan cekap Color. Tetapi bagaimana dengan memulihkannya? Itu akan menjadi sedikit rumit. Sebelum kita melangkah lebih jauh, mari kembangkan ini menjadi kerangka yang akan menangani semua perangkap yang disebutkan di atas untuk kita, yang membolehkan kita memberi tumpuan kepada perkara mudah dalam menentukan jenis.

Kerangka jenis malar

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

Terima kasih kepada organisasi hashtable-of-hashtables kami, sangat mudah untuk mendedahkan fungsi penghitungan yang ditawarkan oleh pelaksanaan Eric. Satu-satunya peringatan adalah bahawa penyortiran, yang ditawarkan reka bentuk Eric, tidak dijamin. Sekiranya anda menggunakan Java 2, anda boleh mengganti peta yang disusun dengan hashtables dalam. Tetapi, seperti yang saya nyatakan di awal lajur ini, saya hanya mementingkan JDK versi 1.1 sekarang.

Satu-satunya logik yang diperlukan untuk menghitung jenisnya ialah mengambil jadual dalaman dan mengembalikan senarai elemennya. Sekiranya jadual dalaman tidak ada, kami hanya mengembalikan nol. Inilah keseluruhan kaedah: