Polimorfisme mengacu pada kemampuan beberapa entitas untuk terjadi dalam berbagai bentuk. Ini secara populer diwakili oleh kupu-kupu, yang berubah dari larva menjadi pupa menjadi imago. Polimorfisme juga ada dalam bahasa pemrograman, sebagai teknik pemodelan yang memungkinkan Anda membuat satu antarmuka ke berbagai operan, argumen, dan objek. Polimorfisme Java menghasilkan kode yang lebih ringkas dan lebih mudah dipelihara.
Meskipun tutorial ini berfokus pada polimorfisme subtipe, ada beberapa jenis lain yang harus Anda ketahui. Kami akan mulai dengan gambaran umum dari keempat jenis polimorfisme.
unduh Dapatkan kodenya Unduh kode sumber untuk aplikasi contoh dalam tutorial ini. Dibuat oleh Jeff Friesen untuk JavaWorld.Jenis polimorfisme di Jawa
Ada empat jenis polimorfisme di Jawa:
- Pemaksaan adalah operasi yang melayani banyak tipe melalui konversi tipe implisit. Misalnya, Anda membagi integer dengan integer lain atau nilai floating-point dengan nilai floating-point lain. Jika satu operan adalah integer dan operand lainnya adalah nilai floating-point, kompilator memaksa (secara implisit mengubah) integer menjadi nilai floating-point untuk mencegah kesalahan tipe. (Tidak ada operasi pembagian yang mendukung operand integer dan operand floating-point.) Contoh lain adalah meneruskan referensi objek subclass ke parameter superclass metode. Kompilator memaksa tipe subclass ke tipe superclass untuk membatasi operasi pada superclass.
- Overloading mengacu pada penggunaan simbol operator atau nama metode yang sama dalam konteks yang berbeda. Misalnya, Anda mungkin menggunakan
+
untuk melakukan penambahan integer, penambahan floating-point, atau penggabungan string, bergantung pada jenis operannya. Selain itu, beberapa metode yang memiliki nama yang sama dapat muncul di kelas (melalui deklarasi dan / atau warisan). - Polimorfisme parametrik menetapkan bahwa dalam deklarasi kelas, nama bidang dapat dikaitkan dengan tipe berbeda dan nama metode dapat dikaitkan dengan parameter berbeda dan jenis kembalian. Bidang dan metode kemudian dapat menggunakan tipe yang berbeda di setiap instance kelas (objek). Misalnya, bidang mungkin berjenis
Double
(anggota pustaka kelas standar Java yang membungkusdouble
nilai) dan metode mungkin mengembalikanDouble
dalam satu objek, dan bidang yang sama mungkin berjenisString
dan metode yang sama mungkin mengembalikanString
dalam objek lain . Java mendukung polimorfisme parametrik melalui obat generik, yang akan saya bahas di artikel mendatang. - Subjenis berarti bahwa suatu tipe dapat berfungsi sebagai subtipe tipe lain. Ketika contoh subtipe muncul dalam konteks supertipe, mengeksekusi operasi supertipe pada contoh subtipe menghasilkan versi subtipe dari operasi itu yang dijalankan. Misalnya, pertimbangkan fragmen kode yang menggambar bentuk arbitrer. Anda bisa mengekspresikan kode gambar ini secara lebih ringkas dengan memperkenalkan
Shape
kelas dengandraw()
metode; dengan memperkenalkanCircle
,,Rectangle
dan subkelas lain yang menggantikandraw()
; dengan memperkenalkan array tipeShape
yang elemennya menyimpan referensi keShape
instance subclass; dan dengan memanggilShape
'sdraw()
metode pada setiap contoh. Saat Anda menelepondraw()
, itu adalahCircle
,Rectangle
atauShape
contoh lainnyadraw()
metode yang dipanggil. Kami mengatakan bahwa ada banyak bentukShape
'sdraw()
metode.
Tutorial ini memperkenalkan polimorfisme subtipe. Anda akan belajar tentang upcasting dan late binding, class abstrak (yang tidak dapat dibuat instance-nya), dan metode abstrak (yang tidak dapat dipanggil). Anda juga akan belajar tentang downcasting dan identifikasi jenis runtime, dan Anda akan melihat jenis pengembalian kovarian pertama. Saya akan menyimpan polimorfisme parametrik untuk tutorial mendatang.
Ad-hoc vs polimorfisme universal
Seperti banyak pengembang, saya mengklasifikasikan pemaksaan dan kelebihan beban sebagai polimorfisme ad-hoc, dan parametrik dan subtipe sebagai polimorfisme universal. Meskipun merupakan teknik yang berharga, saya tidak percaya pemaksaan dan pembebanan berlebihan adalah polimorfisme sejati; mereka lebih menyukai konversi tipe dan gula sintaksis.
Polimorfisme subtipe: Upcasting dan late binding
Polimorfisme subtipe bergantung pada upcasting dan late binding. Upcasting adalah bentuk casting di mana Anda melemparkan hierarki warisan dari subtipe ke supertipe. Tidak ada operator cor yang terlibat karena subtipe adalah spesialisasi dari supertipe. Misalnya, Shape s = new Circle();
upcasts from Circle
to Shape
. Ini masuk akal karena lingkaran adalah sejenis bentuk.
Setelah melakukan upcasting Circle
ke Shape
, Anda tidak dapat memanggil Circle
metode -specific, seperti getRadius()
method yang mengembalikan radius lingkaran, karena Circle
metode -specific bukan bagian dari Shape
antarmuka. Kehilangan akses ke fitur subtipe setelah mempersempit subkelas ke superkelasnya tampaknya tidak ada gunanya, tetapi diperlukan untuk mencapai polimorfisme subtipe.
Misalkan Shape
mendeklarasikan sebuah draw()
metode, Circle
subkelasnya menggantikan metode ini, Shape s = new Circle();
baru saja dijalankan, dan baris berikutnya menentukan s.draw();
. Yang draw()
metode ini disebut: Shape
's draw()
metode atau Circle
' s draw()
metode? Kompilator tidak tahu draw()
metode mana yang harus dipanggil. Yang bisa dilakukannya hanyalah memverifikasi bahwa suatu metode ada di superclass, dan memverifikasi bahwa daftar argumen pemanggilan metode dan tipe kembalian cocok dengan deklarasi metode superclass. Namun, kompilator juga menyisipkan instruksi ke dalam kode yang dikompilasi yang, pada waktu proses, mengambil dan menggunakan referensi apa pun yang ada s
untuk memanggil draw()
metode yang benar . Tugas ini dikenal sebagai pengikatan terlambat .
Pengikatan terlambat vs pengikatan awal
Pengikatan terlambat digunakan untuk panggilan ke final
metode non- instance. Untuk semua pemanggilan metode lainnya, kompilator mengetahui metode mana yang akan dipanggil. Ini memasukkan instruksi ke dalam kode yang dikompilasi yang memanggil metode yang terkait dengan tipe variabel dan bukan nilainya. Teknik ini dikenal sebagai pengikatan awal .
Saya telah membuat aplikasi yang mendemonstrasikan polimorfisme subtipe dalam hal upcasting dan late binding. Aplikasi ini terdiri dari Shape
, Circle
, Rectangle
, dan Shapes
kelas, di mana masing-masing kelas disimpan dalam file sumber sendiri. Kode 1 menyajikan tiga kelas pertama.
Daftar 1. Menyatakan hierarki bentuk
class Shape { void draw() { } } class Circle extends Shape { private int x, y, r; Circle(int x, int y, int r) { this.x = x; this.y = y; this.r = r; } // For brevity, I've omitted getX(), getY(), and getRadius() methods. @Override void draw() { System.out.println("Drawing circle (" + x + ", "+ y + ", " + r + ")"); } } class Rectangle extends Shape { private int x, y, w, h; Rectangle(int x, int y, int w, int h) { this.x = x; this.y = y; this.w = w; this.h = h; } // For brevity, I've omitted getX(), getY(), getWidth(), and getHeight() // methods. @Override void draw() { System.out.println("Drawing rectangle (" + x + ", "+ y + ", " + w + "," + h + ")"); } }
Kode 2 menyajikan Shapes
kelas aplikasi yang main()
metodenya menggerakkan aplikasi.
Daftar 2. Upcasting dan pengikatan terlambat dalam subtipe polimorfisme
class Shapes { public static void main(String[] args) { Shape[] shapes = { new Circle(10, 20, 30), new Rectangle(20, 30, 40, 50) }; for (int i = 0; i < shapes.length; i++) shapes[i].draw(); } }
Deklarasi shapes
larik mendemonstrasikan upcasting. The Circle
dan Rectangle
referensi disimpan dalam shapes[0]
dan shapes[1]
dan upcast untuk mengetik Shape
. Masing-masing shapes[0]
dan shapes[1]
dianggap sebagai Shape
contoh: shapes[0]
tidak dianggap sebagai Circle
; shapes[1]
tidak dianggap sebagai Rectangle
.
Pengikatan terlambat ditunjukkan oleh shapes[i].draw();
ekspresi. Ketika i
sama 0
, compiler yang dihasilkan instruksi penyebab Circle
's draw()
metode untuk dipanggil. Ketika i
sama 1
, namun, instruksi ini penyebab Rectangle
's draw()
metode untuk dipanggil. Ini adalah inti dari polimorfisme subtipe.
Dengan asumsi bahwa semua empat sumber file ( Shapes.java
, Shape.java
, Rectangle.java
, dan Circle.java
) terletak di direktori saat ini, kompilasi mereka melalui salah satu baris perintah berikut:
javac *.java javac Shapes.java
Jalankan aplikasi yang dihasilkan:
java Shapes
Anda harus mengamati keluaran berikut:
Drawing circle (10, 20, 30) Drawing rectangle (20, 30, 40, 50)
Kelas dan metode abstrak
Saat mendesain hierarki kelas, Anda akan menemukan bahwa kelas yang berada di dekat bagian atas hierarki ini lebih umum daripada kelas yang berada di bawah. Misalnya, Vehicle
superclass lebih umum daripada Truck
subclass. Demikian pula, Shape
superclass lebih generik daripada subclass Circle
atau Rectangle
.
It doesn't make sense to instantiate a generic class. After all, what would a Vehicle
object describe? Similarly, what kind of shape is represented by a Shape
object? Rather than code an empty draw()
method in Shape
, we can prevent this method from being called and this class from being instantiated by declaring both entities to be abstract.
Java provides the abstract
reserved word to declare a class that cannot be instantiated. The compiler reports an error when you try to instantiate this class. abstract
is also used to declare a method without a body. The draw()
method doesn't need a body because it is unable to draw an abstract shape. Listing 3 demonstrates.
Listing 3. Abstracting the Shape class and its draw() method
abstract class Shape { abstract void draw(); // semicolon is required }
Abstract cautions
The compiler reports an error when you attempt to declare a class abstract
and final
. For example, the compiler complains about abstract final class Shape
because an abstract class cannot be instantiated and a final class cannot be extended. The compiler also reports an error when you declare a method abstract
but don't declare its class abstract
. Removing abstract
from the Shape
class's header in Listing 3 would result in an error, for instance. This would be an error because a non-abstract (concrete) class cannot be instantiated when it contains an abstract method. Finally, when you extend an abstract class, the extending class must override all of the abstract methods, or else the extending class must itself be declared to be abstract; otherwise, the compiler will report an error.
An abstract class can declare fields, constructors, and non-abstract methods in addition to or instead of abstract methods. For example, an abstract Vehicle
class might declare fields describing its make, model, and year. Also, it might declare a constructor to initialize these fields and concrete methods to return their values. Check out Listing 4.
Listing 4. Abstracting a vehicle
abstract class Vehicle { private String make, model; private int year; Vehicle(String make, String model, int year) { this.make = make; this.model = model; this.year = year; } String getMake() { return make; } String getModel() { return model; } int getYear() { return year; } abstract void move(); }
You'll note that Vehicle
declares an abstract move()
method to describe the movement of a vehicle. For example, a car rolls down the road, a boat sails across the water, and a plane flies through the air. Vehicle
's subclasses would override move()
and provide an appropriate description. They would also inherit the methods and their constructors would call Vehicle
's constructor.
Downcasting and RTTI
Moving up the class hierarchy, via upcasting, entails losing access to subtype features. For example, assigning a Circle
object to Shape
variable s
means that you cannot use s
to call Circle
's getRadius()
method. However, it's possible to once again access Circle
's getRadius()
method by performing an explicit cast operation like this one: Circle c = (Circle) s;
.
This assignment is known as downcasting because you are casting down the inheritance hierarchy from a supertype to a subtype (from the Shape
superclass to the Circle
subclass). Although an upcast is always safe (the superclass's interface is a subset of the subclass's interface), a downcast isn't always safe. Listing 5 shows what kind of trouble could ensue if you use downcasting incorrectly.
Listing 5. The problem with downcasting
class Superclass { } class Subclass extends Superclass { void method() { } } public class BadDowncast { public static void main(String[] args) { Superclass superclass = new Superclass(); Subclass subclass = (Subclass) superclass; subclass.method(); } }
Listing 5 presents a class hierarchy consisting of Superclass
and Subclass
, which extends Superclass
. Furthermore, Subclass
declares method()
. A third class named BadDowncast
provides a main()
method that instantiates Superclass
. BadDowncast
then tries to downcast this object to Subclass
and assign the result to variable subclass
.
Dalam hal ini kompilator tidak akan mengeluh karena downcasting dari superclass ke subclass dalam hierarki tipe yang sama adalah legal. Artinya, jika tugas diizinkan, aplikasi akan macet saat mencoba dijalankan subclass.method();
. Dalam hal ini JVM akan mencoba memanggil metode yang tidak ada, karena Superclass
tidak mendeklarasikan method()
. Untungnya, JVM memverifikasi bahwa pemeran legal sebelum melakukan operasi pemeran. Mendeteksi yang Superclass
tidak menyatakan method()
, itu akan melempar ClassCastException
objek. (Saya akan membahas pengecualian di artikel mendatang.)
Susun Daftar 5 sebagai berikut:
javac BadDowncast.java
Jalankan aplikasi yang dihasilkan:
java BadDowncast