Polimorfisme

Memanfaatkan upcasting dan dynamic dispatch untuk merancang program terhadap tipe dasar.

Daftar isi

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.

java Contoh: variabel bertipe induk berisi objek turunan
// tidak-dijalankan
Bentuk b = new Lingkaran(7);   // upcast, implisit
Lingkaran l = (Lingkaran) b;   // downcast, ditulis eksplisit

Arah 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.

java DemoPolimorfisme.java
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;
    }
}
text Keluaran
Menggambar lingkaran
Luas: 153.93804002589985
Menggambar persegi
Luas: 25.0
Menggambar lingkaran
Luas: 12.566370614359172

Tidak 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.

Dynamic dispatch memilih metode menurut tipe objekVariabel btipe statis: BentukObjek Lingkarantipe saat berjalan: Lingkarantabel metode objekluas() ke Lingkaran.luas()nama() ke Lingkaran.nama()menunjukmemilikib.luas()dipilih saat program berjalan
Dynamic dispatch memilih metode menurut tipe objek, bukan tipe variabel

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.

java TotalLuas.java
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;
    }
}
text Keluaran
Total luas: 739.75

Menambahkan 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 menjalankan Lingkaran.luas(). Tipe variabel baru berperan membatasi anggota apa yang boleh dipanggil.
  • Memanggil anggota khas turunan lewat variabel induk. Baris b.jariJari ditolak kompiler karena tipe Bentuk tidak 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#