Polimorfisme (polymorphism) adalah kemampuan satu pemanggilan metode menghasilkan perilaku yang berbeda sesuai tipe objeknya yang sebenarnya. Dengan upcast ke tipe dasar (base type), sebuah variabel boleh memegang objek dari kelas induk maupun salah satu turunannya, sedangkan dynamic dispatch memilih versi metode yang berjalan pada saat program berjalan. Topik ini mempraktikkan keduanya, lalu menutup dengan cara merancang program terhadap tipe dasar agar penambahan jenis baru tidak menular ke seluruh kode.
Upcast ke tipe dasar#
Upcast adalah penugasan objek turunan ke variabel bertipe induknya, dan kompiler mengerjakannya tanpa notasi tambahan karena arahnya aman. Variabel dan objek membawa tipe yang berbeda sejak baris itu, yaitu variabel bertipe Bentuk sedangkan objek yang dirujuknya bertipe Lingkaran. Kompiler mengizinkan hanya anggota yang dikenal oleh tipe variabel, sehingga metode milik Lingkaran saja tidak dapat dipanggil dari variabel Bentuk tanpa cast balik.
// tidak-dijalankan
Bentuk b = new Lingkaran(7); // upcast, implisit
Lingkaran l = (Lingkaran) b; // downcast, ditulis eksplisitArah baliknya, downcast, ditulis eksplisit seperti pada baris kedua karena kompiler tidak menjamin isi variabelnya memang bertipe itu. Pemeriksaan yang aman untuk downcast dibahas pada topik instanceof, sedangkan bagian ini cukup menetapkan arah yang mudah, yaitu naik ke induk tanpa cast dan turun ke anak dengan cast yang disertai alasan.
Dynamic dispatch saat program berjalan#
Pemanggilan metode instance lewat variabel induk tidak melihat tipe variabelnya, melainkan tipe objek yang dirujuknya saat itu, mekanisme yang disebut dynamic dispatch. JVM mencari versi metode yang paling spesifik pada kelas objeknya, sehingga perilaku mengikuti objek, bukan wadahnya. Program berikut menaruh tiga objek dua jenis berbeda ke dalam satu array Bentuk[], lalu memanggil metode yang sama pada setiap elemennya.
public class DemoPolimorfisme {
public static void main(String[] args) {
Bentuk[] daftar = {
new Lingkaran(7),
new Persegi(5),
new Lingkaran(2)
};
for (Bentuk b : daftar) {
b.gambar();
System.out.println("Luas: " + b.luas());
}
}
}
class Bentuk {
String nama() {
return "bentuk";
}
double luas() {
return 0;
}
void gambar() {
System.out.println("Menggambar " + nama());
}
}
class Lingkaran extends Bentuk {
double jariJari;
Lingkaran(double jariJari) {
this.jariJari = jariJari;
}
@Override
String nama() {
return "lingkaran";
}
@Override
double luas() {
return Math.PI * jariJari * jariJari;
}
}
class Persegi extends Bentuk {
double sisi;
Persegi(double sisi) {
this.sisi = sisi;
}
@Override
String nama() {
return "persegi";
}
@Override
double luas() {
return sisi * sisi;
}
}Menggambar lingkaran
Luas: 153.93804002589985
Menggambar persegi
Luas: 25.0
Menggambar lingkaran
Luas: 12.566370614359172Tidak ada satu pun if yang memeriksa jenis bentuk, tetapi setiap elemen menjawab dengan rumusnya sendiri. Baris b.luas() pada kode sumber selalu sama, sedangkan metode yang berjalan berganti sesuai isi array, yaitu dua kali versi Lingkaran dan satu kali versi Persegi. Perhatikan pula gambar() di induk memanggil nama(), dan versi nama() milik turunan yang berjalan, karena dynamic dispatch berlaku pada setiap pemanggilan metode instance, termasuk yang terjadi dari dalam badan induk.
Diagram berikut memperlihatkan pemisahan itu secara ringkas, yaitu tipe variabel yang terlihat kompiler dan tipe objek yang dilihat JVM saat metode dipilih.
Merancang program terhadap tipe dasar#
Keuntungan terbesar muncul ketika bagian yang berulang ditulis hanya terhadap tipe dasar. Metode totalLuas di bawah ini menerima Bentuk[] tanpa mengetahui jenis isinya, sehingga jenis baru cukup ditambahkan sebagai turunan Bentuk tanpa menyentuh metodenya sama sekali.
public class TotalLuas {
public static void main(String[] args) {
Bentuk[] lahan = {
new Persegi(10),
new Lingkaran(14),
new Segitiga(6, 8)
};
System.out.printf("Total luas: %.2f%n", totalLuas(lahan));
}
static double totalLuas(Bentuk[] daftar) {
double total = 0;
for (Bentuk b : daftar) {
total = total + b.luas();
}
return total;
}
}
class Bentuk {
double luas() {
return 0;
}
}
class Lingkaran extends Bentuk {
double jariJari;
Lingkaran(double jariJari) {
this.jariJari = jariJari;
}
@Override
double luas() {
return Math.PI * jariJari * jariJari;
}
}
class Persegi extends Bentuk {
double sisi;
Persegi(double sisi) {
this.sisi = sisi;
}
@Override
double luas() {
return sisi * sisi;
}
}
class Segitiga extends Bentuk {
double alas;
double tinggi;
Segitiga(double alas, double tinggi) {
this.alas = alas;
this.tinggi = tinggi;
}
@Override
double luas() {
return alas * tinggi / 2;
}
}Total luas: 739.75Menambahkan Segitiga tidak mengubah satu baris pun di totalLuas, karena metode itu hanya bergantung pada janji luas() milik Bentuk. Rancangan semacam ini memindahkan pertumbuhan program ke arah penambahan kelas turunan, bukan penambahan cabang percabangan di banyak tempat. Kebalikannya, kode yang penuh if (b instanceof Lingkaran) menyebarkan pengetahuan tentang jenis ke banyak lokasi dan harus disunting di setiap lokasi itu setiap kali jenis baru datang.
Batas kelas dasar yang masih konkret#
Kelas Bentuk pada contoh di atas menyimpan luas() yang selalu mengembalikan 0, nilai yang tidak bermakna untuk objek Bentuk apa pun. Default semacam itu hanya berfungsi menutupi kelalaian turunan yang lupa mengoverride, karena luas 0 terasa sah bagi kompiler dan diam-diam merusak hasil hitung. Kelas dasar yang objeknya memang tidak perlu dibuat dan hanya berdiri sebagai kerangka turunannya menandai situasi klasik ketika kontrak sebaiknya ditegakkan tanpa isi, misalnya lewat kelas abstrak. Pola yang sama berlaku pada nama() generik yang lebih baik diwajibkan daripada di-default-kan.
Kesalahan umum#
- Mengira tipe variabel menentukan metode yang berjalan. Untuk metode instance, yang menentukan adalah tipe objeknya, sehingga
Bentuk b = new Lingkaran(7)tetap menjalankanLingkaran.luas(). Tipe variabel baru berperan membatasi anggota apa yang boleh dipanggil. - Memanggil anggota khas turunan lewat variabel induk. Baris
b.jariJariditolak kompiler karena tipeBentuktidak mengenalnya. Lakukan downcast dengan pemeriksaan, atau tanggalkan kebutuhannya dengan menambah metode di tipe dasar. - Mengira field ikut mengikuti dynamic dispatch. Akses field diselesaikan kompiler menurut tipe variabel, sehingga field dengan nama sama di induk dan turunan tidak saling menggantikan seperti metode. Hindari field publik bernama sama pada satu hierarki.
Ringkasan#
- Upcast dari turunan ke induk berjalan implisit, sedangkan arah baliknya wajib cast dan sebaiknya diperiksa lebih dahulu.
- Dynamic dispatch memilih metode instance menurut tipe objek saat program berjalan, sehingga satu panggilan menghasilkan banyak perilaku.
- Program yang ditulis terhadap tipe dasar tumbuh dengan menambah kelas turunan, tanpa menyentuh kode pemakainya.
Referensi#
- The Java Tutorials: Polymorphism — Oracle — Pengenalan resmi polimorfisme melalui kelas induk dan turunannya.
- JLS SE 21 §15.12: Method Invocation Expressions — Oracle — Aturan pemilihan metode instance saat pemanggilan, dasar dynamic dispatch.
- The Java Tutorials: Using the Keyword super — Oracle — Hubungan metode induk dan turunan yang menyusun perilaku berlapis.