Dukungan kontainer untuk objek di Java 1.0.2

Herbert Spencer menulis, "Sains adalah pengetahuan yang terorganisir." Konsekuensinya mungkin aplikasi adalah objek yang terorganisir. Mari luangkan waktu sejenak untuk beralih ke beberapa aspek Java yang penting untuk mengembangkan aplikasi daripada applet.

Di antara mereka yang pernah mendengar tentang Jawa, sebagian besar telah mempelajari bahasa tersebut melalui pers populer. Pernyataan yang paling sering muncul adalah bahwa Java adalah untuk "memprogram aplikasi kecil, atau applet, yang dapat disematkan pada halaman Web." Meskipun benar, definisi ini hanya menyampaikan satu aspek dari bahasa baru; itu tidak menggambarkan keseluruhan gambar. Mungkin Java dapat lebih baik dijelaskan sebagai bahasa yang dirancang untuk membangun sistem - sistem besar - dari potongan kode yang dapat dieksekusi yang dapat dipahami dengan baik yang dapat digabungkan, secara keseluruhan atau sebagian, untuk menghasilkan keseluruhan yang diinginkan.

Di kolom ini saya akan mulai melihat berbagai alat yang dapat Anda gunakan untuk membangun di Java. Saya akan mendemonstrasikan bagaimana alat-alat ini dapat digabungkan untuk membuat aplikasi yang lebih besar, dan bagaimana, setelah Anda memiliki aplikasi, Anda dapat menggabungkan aplikasi lebih jauh ke dalam sistem yang lebih besar - semua mungkin karena di Java tidak ada perbedaan antara aplikasi lengkap dan subrutin sederhana.

Untuk menyediakan pakan kode sumber untuk kolom ini dan yang lalu, saya memilih untuk membangun juru bahasa BASIC. "Mengapa BASIC?" Anda mungkin bertanya, karena mengira tidak ada yang menggunakan BASIC lagi. Ini tidak sepenuhnya benar. BASIC terus hidup dalam Visual Basic dan dalam bahasa skrip lainnya. Tetapi yang lebih penting, banyak orang telah terpapar padanya dan dapat membuat lompatan konseptual berikut: Jika "aplikasi" diprogram dalam BASIC, dan BASIC dapat ditulis dalam Java, maka aplikasi dapat ditulis dalam Java. BASIC hanyalah bahasa yang ditafsirkan; alat yang akan kami bangun dapat dimodifikasi untuk menggunakan sintaks bahasa apa pun, sehingga konsep inti menjadi fokus artikel ini. Oleh karena itu, apa yang dimulai sebagai aplikasi menjadi komponen dari aplikasi lain - bahkan mungkin applet.

Kelas dan wadah umum

Membangun kelas generik sangat relevan saat membuat aplikasi karena kelas yang menggunakan kembali memberikan pengaruh yang luar biasa dalam mengurangi kompleksitas dan waktu ke pasar. Dalam applet, nilai kelas generik dikurangi dengan kebutuhan memuatnya melalui jaringan. Dampak negatif dari memuat kelas generik melalui jaringan ditunjukkan oleh Sun's Java Workshop (JWS). JWS menambah versi standar dari abstract windowing toolkit (AWT) dengan menggunakan beberapa kelas "bayangan" yang sangat elegan. Keuntungannya adalah applet mudah dikembangkan dan kaya akan fitur; sisi negatifnya adalah memuat kelas-kelas ini dapat memakan banyak waktu pada tautan jaringan yang lambat. Meskipun kerugian ini pada akhirnya akan hilang, apa yang kami temukan adalah bahwa perspektif sistem pada pengembangan kelas sering kali diperlukan untuk mencapai solusi terbaik.

Karena kami mulai melihat sedikit lebih serius pada pengembangan aplikasi, kami akan menganggap kami telah menentukan bahwa kelas generik adalah solusi yang valid.

Java, seperti banyak bahasa tujuan umum, menyediakan beberapa alat untuk membuat kelas generik. Persyaratan yang berbeda akan memerlukan penggunaan

alat yang berbeda. Dalam kolom ini saya akan menggunakan pengembangan containerkelas sebagai contoh karena dapat menampung hampir semua alat yang mungkin ingin digunakan pengguna.

Kontainer: Definisi

Bagi Anda yang belum terbiasa dengan hal-hal yang berorientasi objek, container adalah kelas yang mengatur objek lain. Kontainer umum adalah pohon biner, antrian, daftar, dan tumpukan. Java menyediakan tiga kelas container dengan rilis JDK 1.0.2: java.util.Hashtable, java.util.Stack, dan java.util.Vector.

Penampung memiliki prinsip pengorganisasian dan antarmuka. Tumpukan, misalnya, dapat diatur sebagai "masuk pertama, keluar terakhir" (FILO), dan antarmukanya dapat ditentukan untuk memiliki dua metode - push () dan pop () . Wadah sederhana dapat dianggap memiliki metode standar menambah dan menghapus . Selanjutnya, mereka akan memiliki sarana untuk menghitung seluruh wadah, untuk memeriksa untuk melihat apakah objek kandidat sudah ada dalam wadah dan untuk menguji jumlah elemen yang dipegang oleh wadah.

Kelas container Java mendemonstrasikan beberapa masalah dengan container, terutama container dengan kunci (container yang menggunakan kunci untuk menemukan objek). Wadah tanpa kunci seperti Stack dan Vector hanya memasukkan objek dan menarik objek keluar. Wadah kunci Hashtable menggunakan objek kunci untuk menemukan objek data. Agar fungsi kunci berfungsi, objek kunci harus mendukung metode HashCode yang mengembalikan kode hash unik untuk setiap objek. Kemampuan kunci ini bekerja karenaObjectclass mendefinisikan metode HashCode dan dengan demikian diwarisi oleh semua objek, tetapi tidak selalu seperti yang Anda inginkan. Misalnya, jika Anda meletakkan objek ke dalam wadah tabel hash dan Anda mengindeksnya dengan objek String, metode HashCode default hanya mengembalikan bilangan bulat unik berdasarkan nilai referensi objek. Untuk string, Anda benar-benar ingin kode hash menjadi fungsi dari nilai string, jadi String mengganti HashCode dan menyediakan versinya sendiri. Ini berarti bahwa untuk objek apa pun yang Anda kembangkan, dan ingin disimpan dalam tabel hash menggunakan instance objek sebagai kuncinya, Anda harus mengganti metode HashCode. Ini memastikan bahwa objek yang dibangun secara identik memiliki kode yang sama.

Tapi bagaimana dengan wadah yang disortir? Satu-satunya antarmuka pengurutan yang disediakan di Objectkelas adalah equals () , dan itu dibatasi untuk menyamakan dua objek sebagai memiliki referensi yang sama, tidak memiliki nilai yang sama. Inilah sebabnya, di Java, Anda tidak dapat menulis kode berikut:

 if (someStringObject == "this") maka {... lakukan sesuatu ...} 

Kode di atas membandingkan referensi objek, mencatat bahwa ada dua objek berbeda di sini, dan mengembalikan false. Anda harus menulis kode sebagai berikut:

 if (someStringObject.compareTo ("this") == 0) lalu {... lakukan sesuatu ...} 

Yang terakhir menggunakan tes pengetahuan ini dirumuskan dalam compareTo metode String untuk membandingkan dua benda tali dan kembali indikasi kesetaraan.

Menggunakan alat di dalam kotak

Seperti yang saya sebutkan sebelumnya, pengembang program generik memiliki dua alat utama yang tersedia bagi mereka: pewarisan implementasi (perluasan) dan pewarisan perilaku (penerapan).

Untuk menggunakan implementasi inheritance, Anda memperluas (subclass) kelas yang sudah ada. Dengan ekstensi, semua subclass dari kelas dasar memiliki kemampuan yang sama dengan kelas root. Ini adalah dasar untuk HashCodemetode di Objectkelas. Karena semua objek diwarisi dari java.lang.Objectkelas, semua objek memiliki metode HashCodeyang mengembalikan hash unik untuk objek itu. Namun, jika Anda ingin menggunakan objek Anda sebagai kunci, ingatlah peringatan yang disebutkan sebelumnya tentang penggantian HashCode.

Selain implementasi pewarisan, ada pewarisan perilaku (implementasi), yang dicapai dengan menentukan bahwa suatu objek mengimplementasikan antarmuka Java tertentu. Objek yang mengimplementasikan antarmuka dapat dilemparkan ke referensi objek dari tipe antarmuka itu. Kemudian referensi tersebut dapat digunakan untuk menjalankan metode yang ditentukan oleh antarmuka tersebut. Biasanya, antarmuka digunakan ketika kelas mungkin perlu memproses beberapa objek dari tipe berbeda dengan cara yang umum. Misalnya, Java mendefinisikan antarmuka Runnable yang digunakan oleh kelas thread untuk bekerja dengan kelas di thread mereka sendiri.

Membangun wadah

Untuk mendemonstrasikan pengorbanan dalam menulis kode generik, saya akan memandu Anda melalui desain dan implementasi kelas kontainer yang diurutkan.

Seperti yang saya sebutkan sebelumnya, dalam pengembangan aplikasi tujuan umum, dalam banyak kasus, wadah yang baik akan berguna. Dalam aplikasi contoh saya, saya membutuhkan wadah yang keduanya dikunci , artinya saya ingin mengambil objek yang ada dengan menggunakan kunci sederhana, dan diurutkan sehingga saya bisa mengambil objek yang ada dalam urutan tertentu berdasarkan nilai kunci.

When designing systems, it is important to keep in mind what parts of the system use a particular interface. In the case of containers, there are two critical interfaces -- the container itself and the keys that index the container. User programs use the container to store and organize objects; the containers themselves use the key interfaces to help them organize themselves. When designing containers, we strive to make them easy to use and to store a wide variety of objects (thus increasing their utility). We design the keys to be flexible so that a wide variety of container implementation can use the same key structures.

To solve my behavioral requirements, keying and sorting, I turn to a useful tree data structure called a binary search tree (BST). Binary trees have the useful property of being sorted, so they can be efficiently searched and can be dumped out in sorted order. The actual BST code is an implementation of the algorithms published in the book Introduction to Algorithms, by Thomas Cormen, Charles Leiserson, and Ron Rivest.

java.util.Dictionary

The Java standard classes have taken a first step toward generic keyed containers with the definition of an abstract class named java.util.Dictionary. If you look at the source code that comes with the JDK, you will see that Hashtable is a subclass of Dictionary.

The Dictionary class attempts to define the methods common to all keyed containers. Technically, what is being described could more properly be called a store as there is no required binding between the key and the object it indexes. However, the name is appropriate as nearly everyone understands the basic operation of a dictionary. An alternative name might be KeyedContainer, but that title gets tedious pretty quickly. The point is that the common superclass of a set of generic classes should express the core behavior being factored out by that class. The Dictionary methods are as follows:

size( )

This method returns the number of objects currently being held by the container.
isEmpty( ) This method returns true if the container has no elements.
keys( ) Return the list of keys in the table as an Enumeration.
elements( ) Return the list of contained objects as an Enumeration.
get(Objectk) Get an object, given a particular key k.
put(Objectk,Objecto) Store an object o using key k.
remove(Objectk) Remove an object that is indexed by key k.

By subclassing Dictionary, we use the tool of implementation inheritance to create an object that can be used by a wide variety of clients. These clients need know only how to use a Dictionary, and we can then substitute our new BST or a Hashtable without the client noticing. It is this property of abstracting out the core interface into the superclass that is crucial to reusability, general-purpose function, expressed cleanly.

Basically, Dictionary gives us two groups of behavior, accounting and administration -- accounting in the form of how many objects we've stored and bulk reading of the store, and administration in the form of get, put, and remove.

If you look at the Hashtable class source (it is included with all versions of the JDK in a file named src.zip), you will see that this class extends Dictionary and has two private internal classes, one named HashtableEntry and one named HashtableEnumerator. The implementation is straightforward. When put is called, objects are placed into a HashtableEntry object and stored into a hash table. When get is called, the key passed is hashed and the hashcode is used to locate the desired object in the hash table. These methods keep track of how many objects have been added or removed, and this information is returned in response to a size request. The HashtableEnumerator class is used to return results of the elements method or keys method.

First cut at a generic keyed container

The BinarySearchTree class is an example of a generic container that subclasses Dictionary but uses a different organizing principle. As in the Hashtable class, I've added a couple of classes to support holding the stored objects and keys and for enumerating the table.

The first is BSTNode, which is equivalent to a HashtableEntry. It is defined as shown in the code outline below. You can also look at the source.

class BSTNode { protected BSTNode parent; protected BSTNode left; protected BSTNode right; protected String key; protected Object payload; public BSTNode(String k, Object p) { key = k; payload = p; } protected BSTNode() { super(); } BSTNode successor() { return successor(this); } BSTNode precessor() { return predecessor(this); } BSTNode min() { return min(this); } BSTNode max() { return max(this); } void print(PrintStream p) { print(this, p); } private static BSTNode successor(BSTNode n) { ... } private static BSTNode predecessor(BSTNode n) { ... } private static BSTNode min(BSTNode n) { ... } private static BSTNode max(BSTNode n) { ... } private static void print(BSTNode n, PrintStream p) { ... } } 

Mari kita lihat kode ini untuk memperjelas dua hal. Pertama, ada konstruktor yang dilindungi null, yang ada sehingga subkelas dari kelas ini tidak harus mendeklarasikan konstruktor yang menimpa salah satu konstruktor kelas ini. Kedua, metode penerus , pendahulu , min , maks , dan cetak sangat pendek dan hanya memanggil padanan pribadi yang sama untuk menghemat ruang memori.