Lihatlah kekuatan polimorfisme parametrik

Misalkan Anda ingin mengimplementasikan kelas daftar di Java. Anda mulai dengan kelas abstrak List,, dan dua subkelas, Emptydan Cons, masing-masing mewakili daftar kosong dan tidak kosong. Karena Anda berencana untuk memperluas fungsionalitas daftar ini, Anda merancang ListVisitorantarmuka, dan menyediakan accept(...)pengait untuk ListVisitorsetiap subkelas Anda. Selain itu, Conskelas Anda memiliki dua bidang, firstdan rest, dengan metode pengakses yang sesuai.

Apa jenis bidang ini? Jelas, restharus tipe List. Jika Anda mengetahui sebelumnya bahwa daftar Anda akan selalu berisi elemen dari kelas tertentu, tugas pengkodean akan jauh lebih mudah pada saat ini. Jika Anda tahu bahwa semua elemen daftar Anda akan menjadi integers, misalnya, Anda dapat menetapkan firstmenjadi tipe integer.

Namun, jika, seperti yang sering terjadi, Anda tidak mengetahui informasi ini sebelumnya, Anda harus memilih superclass paling umum yang memiliki semua kemungkinan elemen yang terdapat dalam daftar Anda, yang biasanya merupakan tipe referensi universal Object. Karenanya, kode Anda untuk daftar elemen tipe yang berbeda-beda memiliki bentuk berikut:

Daftar kelas abstrak {Objek abstrak publik menerima (ListVisitor bahwa); } antarmuka ListVisitor {public Object _case (Kosongkan itu); public Object _case (Kontra itu); } class Empty extends List {Objek publik menerima (ListVisitor that) {return that._case (this); }} class Cons extends List {Objek pribadi pertama; daftar pribadi istirahat; Kontra (Object _first, List _rest) {first = _first; istirahat = _rest; } Objek publik pertama () {kembali pertama;} publik Daftar istirahat () {kembali istirahat;} Objek publik menerima (ListVisitor itu) {mengembalikan itu._case (ini); }}

Meskipun programmer Java sering menggunakan superclass paling tidak umum untuk sebuah field dengan cara ini, pendekatan ini memiliki kelemahan. Misalkan Anda membuat ListVisitoryang menambahkan semua elemen dari daftar Integers dan mengembalikan hasilnya, seperti yang diilustrasikan di bawah ini:

kelas AddVisitor mengimplementasikan ListVisitor {private Integer zero = new Integer (0); public Object _case (Kosongkan itu) {return zero;} public Object _case (Cons that) {return new Integer (((Integer) that.first ()). intValue () + ((Integer) that.rest (). accept (ini)). intValue ()); }}

Perhatikan pemeran eksplisit Integerdalam _case(...)metode kedua . Anda berulang kali melakukan pengujian waktu proses untuk memeriksa properti data; idealnya, kompilator harus melakukan pengujian ini untuk Anda sebagai bagian dari pemeriksaan jenis program. Tetapi karena Anda tidak dijamin bahwa AddVisitorhanya akan diterapkan ke Lists dari Integer, pemeriksa tipe Java tidak dapat mengonfirmasi bahwa Anda, pada kenyataannya, menambahkan dua Integerkecuali ada pemeran.

Anda berpotensi mendapatkan pemeriksaan jenis yang lebih tepat, tetapi hanya dengan mengorbankan polimorfisme dan kode duplikat. Anda dapat, misalnya, membuat Listkelas khusus (dengan subkelas Consdan yang sesuai Empty, serta Visitorantarmuka khusus ) untuk setiap kelas elemen yang Anda simpan di file List. Pada contoh di atas, Anda akan membuat IntegerListkelas yang semua elemennya adalah Integers. Tetapi jika Anda ingin menyimpan, katakanlah, Booleandi tempat lain dalam program ini, Anda harus membuat BooleanListkelas.

Jelasnya, ukuran program yang ditulis menggunakan teknik ini akan meningkat pesat. Ada masalah gaya lebih lanjut, juga; salah satu prinsip penting dari rekayasa perangkat lunak yang baik adalah memiliki satu titik kontrol untuk setiap elemen fungsional program, dan menggandakan kode dengan cara salin dan tempel ini melanggar prinsip tersebut. Melakukannya biasanya menyebabkan biaya pengembangan dan pemeliharaan perangkat lunak yang tinggi. Untuk mengetahui alasannya, pertimbangkan apa yang terjadi ketika bug ditemukan: programmer harus kembali dan memperbaiki bug itu secara terpisah di setiap salinan yang dibuat. Jika programmer lupa untuk mengidentifikasi semua situs yang digandakan, bug baru akan diperkenalkan!

Tetapi, seperti yang diilustrasikan pada contoh di atas, Anda akan merasa sulit untuk secara bersamaan menjaga satu titik kontrol dan menggunakan pemeriksa tipe statis untuk menjamin bahwa kesalahan tertentu tidak akan pernah terjadi saat program dijalankan. Di Java, seperti yang ada saat ini, Anda seringkali tidak punya pilihan selain menggandakan kode jika Anda ingin pemeriksaan tipe statis yang tepat. Yang pasti, Anda tidak akan pernah bisa menghilangkan aspek Java ini sepenuhnya. Postulat tertentu dari teori automata, dibawa ke kesimpulan logisnya, menyiratkan bahwa tidak ada sistem tipe suara yang dapat menentukan secara tepat set input (atau output) yang valid untuk semua metode dalam suatu program. Akibatnya, setiap sistem tipe harus mencapai keseimbangan antara kesederhanaannya sendiri dan ekspresi bahasa yang dihasilkan; sistem tipe Java terlalu condong ke arah kesederhanaan. Pada contoh pertama,sistem tipe yang sedikit lebih ekspresif akan memungkinkan Anda mempertahankan pemeriksaan tipe yang tepat tanpa harus menduplikasi kode.

Sistem tipe ekspresif seperti itu akan menambahkan tipe generik ke bahasa. Tipe generik adalah tipe variabel yang bisa dipakai dengan tipe spesifik yang sesuai untuk setiap instance kelas. Untuk keperluan artikel ini, saya akan mendeklarasikan variabel tipe dalam kurung sudut di atas definisi kelas atau antarmuka. Cakupan variabel tipe kemudian akan terdiri dari isi definisi yang dideklarasikan (tidak termasuk extendsklausa). Dalam lingkup ini, Anda dapat menggunakan variabel tipe di mana pun Anda dapat menggunakan tipe biasa.

Misalnya, dengan tipe generik, Anda dapat menulis ulang Listkelas Anda sebagai berikut:

kelas abstrak Daftar {abstrak publik T menerima (ListVisitor itu); } antarmuka ListVisitor {public T _case (Kosongkan itu); public T _case (Kontra itu); } class Empty extends List {public T accept (ListVisitor that) {return that._case (this); }} class Cons extends List {private T first; daftar pribadi istirahat; Kontra (T _first, List _rest) {first = _first; istirahat = _rest; } public T first () {return first;} public List rest () {return rest;} public T accept (ListVisitor that) {return that._case (this); }}

Sekarang Anda dapat menulis ulang AddVisitoruntuk memanfaatkan tipe generik:

kelas AddVisitor mengimplementasikan ListVisitor {private Integer zero = new Integer (0); public Integer _case (Kosongkan itu) {return zero;} public Integer _case (Cons that) {return new Integer ((that.first ()). intValue () + (that.rest (). accept (this)). intValue ()); }}

Perhatikan bahwa cast eksplisit ke Integertidak lagi diperlukan. Argumen thatke _case(...)metode kedua dideklarasikan Cons, memberi contoh variabel tipe untuk Conskelas dengan Integer. Oleh karena itu, pemeriksa tipe statis dapat membuktikan yang that.first()akan menjadi tipe Integerdan yang that.rest()akan menjadi tipe List. Instansiasi serupa akan dibuat setiap kali instance baru Emptyatau Consdideklarasikan.

Dalam contoh di atas, variabel tipe bisa dibuat dengan sembarang Object. Anda juga bisa memberikan batas atas yang lebih spesifik ke variabel tipe. Dalam kasus seperti itu, Anda dapat menentukan batasan ini pada titik deklarasi variabel tipe dengan sintaks berikut:

  meluas  

Misalnya, jika Anda ingin Lists Anda hanya berisi Comparableobjek, Anda dapat mendefinisikan tiga kelas Anda sebagai berikut:

kelas Daftar {...} kelas Kontra {...} kelas Kosong {...} 

Meskipun menambahkan tipe berparameter ke Java akan memberi Anda manfaat yang ditunjukkan di atas, hal itu tidak akan berguna jika itu berarti mengorbankan kompatibilitas dengan kode lama dalam prosesnya. Untungnya, pengorbanan seperti itu tidak perlu. Dimungkinkan untuk menerjemahkan kode secara otomatis, yang ditulis dalam ekstensi Java yang memiliki tipe generik, ke bytecode untuk JVM yang ada. Beberapa kompiler sudah melakukannya - kompiler Pizza dan GJ, yang ditulis oleh Martin Odersky, adalah contoh yang sangat bagus. Pizza adalah bahasa eksperimental yang menambahkan beberapa fitur baru ke Java, beberapa di antaranya dimasukkan ke dalam Java 1.2; GJ adalah penerus Pizza yang hanya menambahkan jenis umum. Karena ini adalah satu-satunya fitur yang ditambahkan, compiler GJ dapat menghasilkan bytecode yang berfungsi lancar dengan kode lama. Ini mengkompilasi sumber ke bytecode melaluitype erasure, yang menggantikan setiap instance dari setiap variabel jenis dengan batas atas variabel tersebut. Ini juga memungkinkan variabel tipe dideklarasikan untuk metode tertentu, daripada untuk seluruh kelas. GJ menggunakan sintaks yang sama untuk tipe generik yang saya gunakan di artikel ini.

Pekerjaan dalam proses

Di Rice University, grup teknologi bahasa pemrograman tempat saya bekerja menerapkan kompiler untuk versi GJ yang kompatibel ke atas, yang disebut NextGen. Bahasa NextGen dikembangkan bersama oleh Profesor Robert Cartwright dari departemen ilmu komputer Rice dan Guy Steele dari Sun Microsystems; itu menambahkan kemampuan untuk melakukan pemeriksaan runtime variabel tipe ke GJ.

Solusi potensial lain untuk masalah ini, yang disebut PolyJ, dikembangkan di MIT. Itu diperpanjang di Cornell. PolyJ menggunakan sintaks yang sedikit berbeda dari GJ / NextGen. Ini juga sedikit berbeda dalam penggunaan tipe generik. Misalnya, itu tidak mendukung parameterisasi tipe metode individu, dan saat ini, tidak mendukung kelas dalam. Tapi tidak seperti GJ atau NextGen, itu memungkinkan variabel tipe untuk dipakai dengan tipe primitif. Selain itu, seperti NextGen, PolyJ mendukung operasi runtime pada tipe generik.

Sun telah merilis Java Specification Request (JSR) untuk menambahkan tipe generik ke bahasa tersebut. Tidak mengherankan, salah satu tujuan utama yang terdaftar untuk setiap pengiriman adalah pemeliharaan kompatibilitas dengan perpustakaan kelas yang ada. Ketika tipe generik ditambahkan ke Java, kemungkinan salah satu proposal yang dibahas di atas akan berfungsi sebagai prototipe.

Ada beberapa programmer yang menentang penambahan tipe generik dalam bentuk apapun, meskipun memiliki kelebihan. Saya akan merujuk pada dua argumen umum dari lawan seperti argumen "template yang jahat" dan argumen "itu tidak berorientasi objek", dan membahas masing-masing secara bergantian.

Apakah template itu jahat?

C ++ menggunakan templateuntuk memberikan bentuk tipe generik. Template telah mendapatkan reputasi buruk di antara beberapa developer C ++ karena definisinya bukan tipe yang diperiksa dalam bentuk berparameter. Sebagai gantinya, kode direplikasi di setiap contoh, dan setiap replikasi diperiksa secara terpisah. Masalah dengan pendekatan ini adalah bahwa kesalahan jenis mungkin ada dalam kode asli yang tidak muncul di salah satu contoh awal. Kesalahan ini dapat muncul dengan sendirinya nanti jika revisi atau ekstensi program memperkenalkan instansiasi baru. Bayangkan kekecewaan seorang pengembang yang menggunakan kelas yang sudah ada yang jenisnya diperiksa ketika dikompilasi sendiri, tetapi tidak setelah ia menambahkan subkelas baru yang benar-benar sah! Lebih buruk lagi, jika template tidak dikompilasi ulang bersama dengan kelas-kelas baru, kesalahan seperti itu tidak akan terdeteksi, tetapi justru akan merusak program yang sedang dijalankan.

Karena masalah ini, beberapa orang tidak setuju untuk mengembalikan template, mengharapkan kelemahan template di C ++ untuk diterapkan ke sistem tipe generik di Java. Analogi ini menyesatkan, karena fondasi semantik Java dan C ++ sangat berbeda. C ++ adalah bahasa yang tidak aman, di mana pemeriksaan tipe statis adalah proses heuristik tanpa dasar matematika. Sebaliknya, Java adalah bahasa yang aman, di mana pemeriksa tipe statis secara harfiah membuktikan bahwa kesalahan tertentu tidak dapat terjadi ketika kode dijalankan. Akibatnya, program C ++ yang melibatkan templat mengalami banyak sekali masalah keamanan yang tidak dapat terjadi di Java.

Selain itu, semua proposal yang menonjol untuk Java generik melakukan pemeriksaan tipe statis eksplisit dari kelas berparameter, daripada hanya melakukannya di setiap pembuatan instance kelas. Jika Anda khawatir bahwa pemeriksaan eksplisit seperti itu akan memperlambat pemeriksaan jenis, yakinlah bahwa, pada kenyataannya, yang terjadi adalah sebaliknya: karena pemeriksa jenis hanya membuat satu pass di atas kode berparameter, sebagai lawan dari pass untuk setiap instantiasi dari tipe berparameter, proses pengecekan tipe dipercepat. Karena alasan ini, banyak penolakan terhadap template C ++ tidak berlaku untuk proposal tipe generik untuk Java. Faktanya, jika Anda melihat lebih jauh dari apa yang telah banyak digunakan di industri, ada banyak bahasa yang kurang populer tetapi dirancang dengan sangat baik, seperti Objective Caml dan Eiffel, yang mendukung tipe berparameter untuk keuntungan besar.

Apakah sistem tipe generik berorientasi objek?

Akhirnya, beberapa programmer menolak sistem tipe generik dengan alasan bahwa, karena sistem seperti itu pada awalnya dikembangkan untuk bahasa fungsional, mereka tidak berorientasi objek. Keberatan ini palsu. Tipe generik sangat cocok dengan kerangka kerja berorientasi objek, seperti yang ditunjukkan oleh contoh dan diskusi di atas. Tapi saya menduga bahwa keberatan ini berakar pada kurangnya pemahaman tentang bagaimana mengintegrasikan tipe generik dengan polimorfisme pewarisan Java. Faktanya, integrasi seperti itu dimungkinkan, dan merupakan dasar untuk implementasi NextGen kami.