Ungkapkan keajaiban di balik polimorfisme subtipe

Kata polimorfisme berasal dari bahasa Yunani yang berarti "banyak bentuk". Sebagian besar pengembang Java mengaitkan istilah dengan kemampuan objek untuk secara ajaib mengeksekusi perilaku metode yang benar pada titik yang sesuai dalam sebuah program. Namun, tampilan berorientasi implementasi tersebut mengarah ke gambar sihir, bukan pemahaman tentang konsep dasar.

Polimorfisme di Jawa selalu merupakan polimorfisme subtipe. Memeriksa dengan cermat mekanisme yang menghasilkan variasi perilaku polimorfik tersebut mengharuskan kita membuang masalah penerapan yang biasa dan berpikir dalam istilah jenis. Artikel ini menyelidiki perspektif berorientasi tipe objek, dan bagaimana perspektif itu memisahkan perilaku apa yang dapat diekspresikan oleh objek dari bagaimana objek mengekspresikan perilaku tersebut. Dengan membebaskan konsep polimorfisme kita dari hierarki implementasi, kita juga menemukan bagaimana antarmuka Java memfasilitasi perilaku polimorfik di seluruh grup objek yang tidak berbagi kode implementasi sama sekali.

Quattro polymorphi

Polimorfisme adalah istilah berorientasi objek yang luas. Meskipun kita biasanya menyamakan konsep umum dengan variasi subtipe, sebenarnya ada empat jenis polimorfisme. Sebelum kita memeriksa polimorfisme subtipe secara rinci, bagian berikut menyajikan gambaran umum polimorfisme dalam bahasa berorientasi objek.

Luca Cardelli dan Peter Wegner, penulis "Tentang Jenis Pemahaman, Abstraksi Data, dan Polimorfisme", (lihat Sumber daya untuk tautan ke artikel) membagi polimorfisme menjadi dua kategori utama - ad hoc dan universal - dan empat jenis: paksaan, beban berlebih, parametrik, dan inklusi. Struktur klasifikasinya adalah:

| - paksaan | - ad hoc - | | - polimorfisme yang membebani - | | - parametrik | - universal - | | - inklusi

Dalam skema umum tersebut, polimorfisme merepresentasikan kapasitas suatu entitas untuk memiliki banyak bentuk. Polimorfisme universal mengacu pada keseragaman tipe struktur, di mana polimorfisme bekerja pada tipe tak terbatas yang memiliki fitur umum. Polimorfisme ad hoc yang kurang terstruktur bekerja atas sejumlah jenis yang mungkin tidak terkait. Keempat varietas tersebut dapat digambarkan sebagai:

  • Pemaksaan: abstraksi tunggal melayani beberapa jenis melalui konversi jenis implisit
  • Overloading: satu pengenal menunjukkan beberapa abstraksi
  • Parametrik: abstraksi beroperasi secara seragam di berbagai jenis
  • Inklusi: abstraksi beroperasi melalui relasi inklusi

Saya akan membahas secara singkat setiap varietas sebelum beralih secara khusus ke subtipe polimorfisme.

Paksaan

Pemaksaan mewakili konversi tipe parameter implisit ke tipe yang diharapkan oleh metode atau operator, sehingga menghindari kesalahan tipe. Untuk ekspresi berikut, kompilator harus menentukan apakah +operator biner yang sesuai ada untuk jenis operan:

 2.0 + 2.0 2.0 + 2 2.0 + "2" 

Ekspresi pertama menambahkan dua doubleoperan; bahasa Java secara khusus mendefinisikan operator tersebut.

Namun, ekspresi kedua menambahkan a doubledan an int; Java tidak mendefinisikan operator yang menerima tipe operand tersebut. Untungnya, kompilator secara implisit mengubah operan kedua menjadi doubledan menggunakan operator yang ditentukan untuk dua doubleoperan. Itu sangat nyaman bagi pengembang; tanpa konversi implisit, kesalahan waktu kompilasi akan terjadi atau programmer harus secara eksplisit mentransmisikan intke double.

Ekspresi ketiga menambahkan a doubledan a String. Sekali lagi, bahasa Java tidak mendefinisikan operator seperti itu. Jadi kompilator memaksa doubleoperan ke a String, dan operator plus melakukan penggabungan string.

Pemaksaan juga terjadi pada pemanggilan metode. Misalkan kelas Derivedmemperluas kelas Base, dan kelas Cmemiliki metode dengan tanda tangan m(Base). Untuk pemanggilan metode dalam kode di bawah ini, compiler secara implisit mengonversi derivedvariabel referensi, yang memiliki tipe Derived, ke Basetipe yang ditentukan oleh tanda tangan metode. Konversi implisit tersebut memungkinkan m(Base)kode implementasi metode untuk hanya menggunakan jenis operasi yang ditentukan oleh Base:

C c = C baru (); Turunan turunan = turunan baru (); cm (diturunkan);

Sekali lagi, pemaksaan implisit selama pemanggilan metode meniadakan tipe cast yang rumit atau kesalahan waktu kompilasi yang tidak perlu. Tentu saja, kompilator masih memverifikasi bahwa semua jenis konversi sesuai dengan hierarki jenis yang ditentukan.

Kelebihan beban

Overloading mengizinkan penggunaan operator yang sama atau nama metode untuk menunjukkan beberapa arti program yang berbeda. The +operator yang digunakan dalam bagian sebelumnya dipamerkan dua bentuk: satu untuk menambahkan doubleoperan, satu untuk concatenating Stringobjek. Bentuk lain ada untuk menambahkan dua bilangan bulat, dua panjang, dan seterusnya. Kami menyebut operator kelebihan beban dan mengandalkan kompiler untuk memilih fungsionalitas yang sesuai berdasarkan konteks program. Seperti disebutkan sebelumnya, jika perlu, kompilator secara implisit mengubah tipe operan agar cocok dengan tanda tangan operator. Meskipun Java menetapkan operator tertentu yang kelebihan beban, Java tidak mendukung kelebihan beban yang ditentukan pengguna oleh operator.

Java mengizinkan overloading nama metode yang ditentukan pengguna. Suatu kelas dapat memiliki beberapa metode dengan nama yang sama, asalkan tanda tangan metode berbeda. Artinya, jumlah parameter harus berbeda atau setidaknya satu posisi parameter harus memiliki jenis yang berbeda. Tanda tangan unik memungkinkan penyusun untuk membedakan antara metode yang memiliki nama yang sama. Kompilator mengacaukan nama metode menggunakan tanda tangan unik, secara efektif membuat nama unik. Oleh karena itu, setiap perilaku polimorfik yang terlihat menguap setelah pemeriksaan lebih dekat.

Baik pemaksaan maupun pemuatan berlebih diklasifikasikan sebagai ad hoc karena masing-masing memberikan perilaku polimorfik hanya dalam arti yang terbatas. Meskipun mereka termasuk dalam definisi polimorfisme yang luas, varietas ini terutama merupakan kemudahan pengembang. Coercion meniadakan cast tipe eksplisit yang merepotkan atau error tipe compiler yang tidak perlu. Sebaliknya, kelebihan muatan memberikan gula sintaksis, memungkinkan pengembang menggunakan nama yang sama untuk metode yang berbeda.

Parametrik

Polimorfisme parametrik memungkinkan penggunaan abstraksi tunggal di berbagai jenis. Misalnya, Listabstraksi, yang mewakili daftar objek homogen, dapat diberikan sebagai modul umum. Anda akan menggunakan kembali abstraksi dengan menentukan jenis objek yang terdapat dalam daftar. Karena tipe berparameter dapat berupa tipe data yang ditentukan pengguna, ada potensi penggunaan tak terbatas untuk abstraksi generik, menjadikannya jenis polimorfisme yang paling kuat.

Sekilas, Listabstraksi di atas mungkin tampak seperti kegunaan kelas java.util.List. Namun, Java tidak mendukung polimorfisme parametrik yang sebenarnya dengan cara yang aman-jenis, itulah sebabnya java.util.Listdan java.utilkelas koleksi lainnya ditulis dalam istilah kelas Java primordial java.lang.Object,. (Lihat artikel saya "A Primordial Interface?" Untuk lebih jelasnya.) Warisan implementasi berakar tunggal Java menawarkan solusi parsial, tetapi bukan kekuatan sebenarnya dari polimorfisme parametrik. Artikel Eric Allen yang sangat bagus, "Lihatlah Kekuatan Polimorfisme Parametrik," menjelaskan kebutuhan akan tipe generik di Java dan proposal untuk menjawab Permintaan Spesifikasi Java Sun # 000014, "Tambahkan Tipe Generik ke Bahasa Pemrograman Java." (Lihat Sumberdaya untuk link.)

Penyertaan

Polimorfisme inklusi mencapai perilaku polimorfik melalui relasi inklusi antara tipe atau kumpulan nilai. Untuk banyak bahasa berorientasi objek, termasuk Java, relasi inklusi adalah relasi subtipe. Jadi di Jawa, polimorfisme inklusi adalah polimorfisme subtipe.

Seperti disebutkan sebelumnya, ketika pengembang Java secara umum mengacu pada polimorfisme, mereka selalu berarti polimorfisme subtipe. Memperoleh apresiasi yang kuat dari kekuatan subtipe polimorfisme memerlukan melihat mekanisme yang menghasilkan perilaku polimorfik dari perspektif berorientasi tipe. Sisa artikel ini membahas perspektif itu dengan cermat. Untuk singkatnya dan kejelasan, saya menggunakan istilah polimorfisme yang berarti polimorfisme subtipe.

Tampilan berorientasi tipe

The UML class diagram in Figure 1 shows the simple type and class hierarchy used to illustrate the mechanics of polymorphism. The model depicts five types, four classes, and one interface. Although the model is called a class diagram, I think of it as a type diagram. As detailed in "Thanks Type and Gentle Class," every Java class and interface declares a user-defined data type. So from an implementation-independent view (i.e., a type-oriented view) each of the five rectangles in the figure represents a type. From an implementation point of view, four of those types are defined using class constructs, and one is defined using an interface.

The following code defines and implements each user-defined data type. I purposely keep the implementation as simple as possible:

/* Base.java */ public class Base { public String m1() { return "Base.m1()"; } public String m2( String s ) { return "Base.m2( " + s + " )"; } } /* IType.java */ interface IType { String m2( String s ); String m3(); } /* Derived.java */ public class Derived extends Base implements IType { public String m1() { return "Derived.m1()"; } public String m3() { return "Derived.m3()"; } } /* Derived2.java */ public class Derived2 extends Derived { public String m2( String s ) { return "Derived2.m2( " + s + " )"; } public String m4() { return "Derived2.m4()"; } } /* Separate.java */ public class Separate implements IType { public String m1() { return "Separate.m1()"; } public String m2( String s ) { return "Separate.m2( " + s + " )"; } public String m3() { return "Separate.m3()"; } } 

Using these type declarations and class definitions, Figure 2 depicts a conceptual view of the Java statement:

Derived2 derived2 = new Derived2(); 

The above statement declares an explicitly typed reference variable, derived2, and attaches that reference to a newly created Derived2 class object. The top panel in Figure 2 depicts the Derived2 reference as a set of portholes, through which the underlying Derived2 object can be viewed. There is one hole for each Derived2 type operation. The actual Derived2 object maps each Derived2 operation to appropriate implementation code, as prescribed by the implementation hierarchy defined in the above code. For example, the Derived2 object maps m1() to implementation code defined in class Derived. Furthermore, that implementation code overrides the m1() method in class Base. A Derived2 reference variable cannot access the overridden m1() implementation in class Base. That does not mean that the actual implementation code in class Derived can't use the Base class implementation via super.m1(). But as far as the reference variable derived2 is concerned, that code is inaccessible. The mappings of the other Derived2 operations similarly show the implementation code executed for each type operation.

Now that you have a Derived2 object, you can reference it with any variable that conforms to type Derived2. The type hierarchy in Figure 1's UML diagram reveals that Derived, Base, and IType are all super types of Derived2. So, for example, a Base reference can be attached to the object. Figure 3 depicts the conceptual view of the following Java statement:

Base base = derived2; 

There is absolutely no change to the underlying Derived2 object or any of the operation mappings, though methods m3() and m4() are no longer accessible through the Base reference. Calling m1() or m2(String) using either variable derived2 or base results in execution of the same implementation code:

String tmp; // Derived2 reference (Figure 2) tmp = derived2.m1(); // tmp is "Derived.m1()" tmp = derived2.m2( "Hello" ); // tmp is "Derived2.m2( Hello )" // Base reference (Figure 3) tmp = base.m1(); // tmp is "Derived.m1()" tmp = base.m2( "Hello" ); // tmp is "Derived2.m2( Hello )" 

Realizing identical behavior through both references makes sense because the Derived2 object does not know what calls each method. The object only knows that when called upon, it follows the marching orders defined by the implementation hierarchy. Those orders stipulate that for method m1(), the Derived2 object executes the code in class Derived, and for method m2(String), it executes the code in class Derived2. The action performed by the underlying object does not depend on the reference variable's type.

Namun, semua tidak sama bila Anda menggunakan variabel referensi derived2dan base. Seperti yang digambarkan pada Gambar 3, Basereferensi tipe hanya dapat melihat Baseoperasi tipe dari objek yang mendasari. Jadi meskipun Derived2memiliki pemetaan untuk metode m3()dan m4(), variabel basetidak dapat mengakses metode tersebut:

String tmp; // Referensi turunan2 (Gambar 2) tmp = turunan2.m3 (); // tmp adalah "Derived.m3 ()" tmp = turunan2.m4 (); // tmp adalah "Derived2.m4 ()" // Referensi dasar (Gambar 3) tmp = base.m3 (); // Kesalahan waktu kompilasi tmp = base.m4 (); // Kesalahan waktu kompilasi

Waktu proses

Derived2

objek tetap sepenuhnya mampu menerima baik

m3()

atau

m4()

panggilan metode. Pembatasan jenis yang melarang panggilan yang dicoba melalui

Base