Mengungkap keajaiban di sebalik polimorfisme subjenis

Kata polimorfisme berasal dari bahasa Yunani untuk "banyak bentuk." Sebilangan besar pemaju Java mengaitkan istilah itu dengan kemampuan objek untuk secara ajaib melaksanakan tingkah laku kaedah yang betul pada titik yang sesuai dalam program. Namun, pandangan yang berorientasikan pelaksanaan membawa kepada gambaran ilmu sihir, dan bukannya pemahaman mengenai konsep asas.

Polimorfisme di Jawa adalah polimorfisme subtipe. Memeriksa secara dekat mekanisme yang menghasilkan pelbagai tingkah laku polimorfik memerlukan kita membuang kebimbangan pelaksanaan yang biasa dan berfikir dari segi jenis. Artikel ini menyelidiki perspektif objek berorientasi jenis, dan bagaimana perspektif itu memisahkan tingkah laku apa yang dapat dinyatakan oleh objek dari bagaimana objek sebenarnya menyatakan tingkah laku itu. Dengan membebaskan konsep polimorfisme dari hierarki pelaksanaan, kami juga mengetahui bagaimana antarmuka Java memfasilitasi tingkah laku polimorfik di seluruh kumpulan objek yang sama sekali tidak mempunyai kod pelaksanaan.

Quattro polymorphi

Polimorfisme adalah istilah berorientasikan objek yang luas. Walaupun kita biasanya menyamakan konsep umum dengan subtipe, sebenarnya terdapat empat jenis polimorfisme yang berbeza. Sebelum kita mengkaji polimorfisme subjenis secara terperinci, bahagian berikut menunjukkan gambaran umum polimorfisme dalam bahasa berorientasikan objek.

Luca Cardelli dan Peter Wegner, pengarang "Tentang Memahami Jenis, Abstraksi Data, dan Polimorfisme," (lihat Sumber untuk pautan ke artikel) membahagikan polimorfisme menjadi dua kategori utama - ad hoc dan universal - dan empat jenis: paksaan, kelebihan beban, parametrik, dan kemasukan. Struktur klasifikasi adalah:

| - paksaan | - ad hoc - | | - polimorfisme yang berlebihan - | | - parametrik | - sejagat - | | - kemasukan

Dalam skema umum itu, polimorfisme mewakili keupayaan entiti untuk mempunyai pelbagai bentuk. Polimorfisme sejagat merujuk kepada keseragaman struktur jenis, di mana polimorfisme bertindak terhadap sejumlah jenis yang tidak terbatas yang mempunyai ciri umum. Polimorfisme ad hoc yang kurang tersusun bertindak melebihi sebilangan jenis yang mungkin tidak berkaitan. Keempat-empat jenis tersebut dapat digambarkan sebagai:

  • Paksaan: abstraksi tunggal melayani beberapa jenis melalui penukaran jenis tersirat
  • Beban berlebihan: pengecam tunggal menunjukkan beberapa abstraksi
  • Parametrik: abstraksi beroperasi secara seragam di pelbagai jenis
  • Inklusi: abstraksi beroperasi melalui hubungan inklusi

Saya akan membincangkan secara ringkas setiap ragam sebelum beralih kepada polimorfisme subjenis.

Paksaan

Pemaksaan mewakili penukaran jenis parameter tersirat ke jenis yang diharapkan oleh kaedah atau pengendali, sehingga menghindari kesalahan jenis. Untuk ungkapan berikut, penyusun mesti menentukan sama ada +pengendali binari yang sesuai ada untuk jenis operasi:

 2.0 + 2.0 2.0 + 2 2.0 + "2" 

Ungkapan pertama menambah dua doubleoperan; bahasa Java secara khusus menentukan pengendali sedemikian.

Walau bagaimanapun, ungkapan kedua menambah a doubledan an int; Java tidak menentukan operator yang menerima jenis operasi tersebut. Nasib baik, penyusun secara implisit menukar operasi kedua ke doubledan menggunakan operator yang ditentukan untuk dua doubleoperasi. Itu sangat sesuai untuk pemaju; tanpa penukaran yang tersirat, ralat menyusun masa akan menyebabkan atau pengaturcara perlu membuang jelas yang intke double.

Ungkapan ketiga menambah a doubledan a String. Sekali lagi, bahasa Java tidak menentukan pengendali seperti itu. Oleh itu, penyusun memaksa doubleoperan menjadi a String, dan operator tambah melakukan penggabungan rentetan.

Paksaan juga berlaku semasa pemanggilan kaedah. Katakan kelas Derivedmeluaskan kelas Base, dan kelas Cmempunyai kaedah dengan tandatangan m(Base). Untuk kaedah memohon dalam kod di bawah, penyusun secara implisit menukar derivedpemboleh ubah rujukan, yang mempunyai jenis Derived, ke Basejenis yang ditentukan oleh tandatangan kaedah. Penukaran tersirat itu membolehkan m(Base)kod pelaksanaan kaedah hanya menggunakan jenis operasi yang ditentukan oleh Base:

C c = baru C (); Berasal diturunkan = baru Berasal (); cm (berasal);

Sekali lagi, pemaksaan secara tersirat semasa pemanggilan kaedah menghilangkan pelemparan jenis yang rumit atau kesilapan masa kompilasi yang tidak perlu. Sudah tentu, penyusun masih mengesahkan bahawa semua penukaran jenis menepati hierarki jenis yang ditentukan.

Beban berlebihan

Beban berlebihan membenarkan penggunaan nama pengendali atau kaedah yang sama untuk menunjukkan pelbagai makna program yang berbeza. The +operator yang digunakan dalam bahagian sebelum ini dipamerkan dua bentuk: satu untuk menambah doubleoperan, satu untuk concatenating Stringobjek. Bentuk lain ada untuk menambahkan dua bilangan bulat, dua panjang, dan seterusnya. Kami memanggil operator yang terlalu banyak dan bergantung pada penyusun untuk memilih fungsi yang sesuai berdasarkan konteks program. Seperti yang dinyatakan sebelum ini, jika perlu, penyusun secara implisit menukar jenis operasi agar sesuai dengan tanda tangan pengendali. Walaupun Java menentukan operator yang kelebihan muatan tertentu, ia tidak menyokong kelebihan operator yang ditentukan oleh pengguna.

Java memang membenarkan pemuatan nama kaedah yang ditentukan pengguna. Kelas mungkin mempunyai beberapa kaedah dengan nama yang sama, dengan syarat tandatangan kaedah berbeza. Ini bermaksud sama ada bilangan parameter mesti berbeza atau sekurang-kurangnya satu kedudukan parameter mesti mempunyai jenis yang berbeza. Tanda tangan yang unik membolehkan penyusun membezakan antara kaedah yang mempunyai nama yang sama. Pengkompilasi nama kaedah menggunakan tandatangan unik, dengan berkesan mencipta nama unik. Sehubungan dengan itu, sebarang tingkah laku polimorfik yang nyata akan hilang setelah diperiksa lebih dekat.

Kedua-dua paksaan dan kelebihan beban diklasifikasikan sebagai ad hoc kerana masing-masing memberikan tingkah laku polimorfik hanya dalam pengertian terhad. Walaupun mereka berada di bawah definisi polimorfisme yang luas, varieti ini adalah kemudahan pemaju. Paksaan mengatasi pelempar jenis eksplisit yang rumit atau kesalahan jenis penyusun yang tidak perlu. Beban berlebihan, sebaliknya, menyediakan gula sintaksis, yang membolehkan pemaju menggunakan nama yang sama untuk kaedah yang berbeza.

Parametrik

Polimorfisme parametrik membenarkan penggunaan abstraksi tunggal dalam pelbagai jenis. Sebagai contoh, Listabstraksi, yang mewakili senarai objek homogen, dapat disediakan sebagai modul generik. Anda akan menggunakan semula abstraksi dengan menentukan jenis objek yang terdapat dalam senarai. Oleh kerana jenis parameter dapat menjadi jenis data yang ditentukan pengguna, ada sejumlah penggunaan yang berpotensi tak terbatas untuk abstraksi generik, menjadikan ini jenis polimorfisme yang paling kuat.

Pada pandangan pertama, Listpengabstrakan di atas mungkin menjadi utiliti kelas java.util.List. Walau bagaimanapun, Jawa tidak menyokong polymorphism parametrik benar dengan cara jenis selamat, itulah sebabnya java.util.Listdan java.utilkelas koleksi 's lain ditulis dari segi kelas Java purba, java.lang.Object. (Lihat artikel saya "Antarmuka Primordial?" Untuk lebih jelasnya.) Pewarisan implementasi satu-akar Java menawarkan penyelesaian separa, tetapi bukan kekuatan polimorfisme parametrik sebenarnya. Artikel yang sangat baik Eric Allen, "Lihatlah Kekuatan Polimorfisme Parametrik," menjelaskan perlunya jenis generik di Jawa dan cadangan untuk mengatasi Permintaan Spesifikasi Java Sun # 000014, "Tambahkan Jenis Generik ke Bahasa Pemrograman Java." (Lihat Sumber untuk pautan.)

Kemasukan

Polimorfisme inklusi mencapai tingkah laku polimorfik melalui hubungan inklusi antara jenis atau set nilai. Untuk banyak bahasa berorientasikan objek, termasuk Java, hubungan inklusi adalah hubungan subjenis. Jadi di Jawa, polimorfisme inklusi adalah polimorfisme subjenis.

Seperti disebutkan sebelumnya, ketika para pengembang Java secara umum merujuk pada polimorfisme, mereka selalu bermaksud polimorfisme subjenis. Mendapatkan penghargaan yang kuat terhadap kekuatan polimorfisme subjenis memerlukan melihat mekanisme yang menghasilkan tingkah laku polimorfik dari perspektif berorientasikan jenis. Selebihnya artikel ini mengkaji perspektif itu dengan teliti. Untuk kesimpulan dan kejelasan, saya menggunakan istilah polimorfisme untuk bermaksud polimorfisme subjenis.

Paparan berorientasikan jenis

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.

Walau bagaimanapun, semua tidak sama apabila anda menggunakan pemboleh ubah rujukan derived2dan base. Seperti yang digambarkan dalam Gambar 3, Baserujukan jenis hanya dapat melihat Baseoperasi jenis objek yang mendasari. Jadi walaupun Derived2mempunyai pemetaan untuk kaedah m3()dan m4(), pemboleh ubah basetidak dapat mengakses kaedah tersebut:

Rentetan tmp; // Rujukan turunan2 (Rajah 2) tmp = turunan2.m3 (); // tmp adalah "Derived.m3 ()" tmp = turunan2.m4 (); // tmp adalah "Derived2.m4 ()" // Rujukan asas (Rajah 3) tmp = base.m3 (); // Kesalahan masa kompilasi tmp = base.m4 (); // Kesalahan masa kompilasi

Masa berjalan

Derived2

objek tetap sepenuhnya dapat menerima sama ada

m3()

atau

m4()

kaedah panggilan. Sekatan jenis yang tidak membenarkan percubaan panggilan melalui

Base