Tampilan
Kenapa penting
Polymorphism adalah alasan utama OOP berguna di backend. Dengan polymorphism, satu baris kode seperti gateway.charge(invoice, amount) bisa menjalankan perilaku berbeda — memanggil payment gateway sungguhan di production, atau implementasi palsu di unit test — tanpa mengubah baris itu. Kemampuan ini yang membuat aplikasi besar bisa dikembangkan bertahap: menambah metode pembayaran, mengganti penyedia email, atau menguji logika bisnis tanpa jaringan. Framework seperti Spring sepenuhnya bergantung pada ide ini.
Konsep inti
Dynamic dispatch
Saat kamu memanggil method instance yang di-override, JVM memilih versi milik object yang sebenarnya (tipe runtime), bukan milik tipe variabelnya. Pemilihan ini terjadi saat program berjalan, karena itu disebut dynamic dispatch.
java
public class DispatchDemo {
public static void main(String[] args) {
PaymentMethod[] methods = {new BankTransfer(), new CreditCard(), new EWallet()};
long amount = 200_000;
for (PaymentMethod method : methods) {
// Satu baris pemanggilan, tiga perilaku berbeda: dipilih saat program berjalan.
System.out.println(method.name() + ": biaya Rp" + method.fee(amount));
}
}
}
interface PaymentMethod {
String name();
long fee(long amount);
}
class BankTransfer implements PaymentMethod {
@Override
public String name() {
return "Transfer bank";
}
@Override
public long fee(long amount) {
return 4_000; // biaya tetap
}
}
class CreditCard implements PaymentMethod {
@Override
public String name() {
return "Kartu kredit";
}
@Override
public long fee(long amount) {
return amount * 25 / 1000; // 2,5%
}
}
class EWallet implements PaymentMethod {
@Override
public String name() {
return "E-wallet";
}
@Override
public long fee(long amount) {
return amount / 100; // 1%
}
}Output:
text
Transfer bank: biaya Rp4000
Kartu kredit: biaya Rp5000
E-wallet: biaya Rp2000Loop di main tidak tahu ada berapa jenis metode pembayaran, dan tidak butuh if/switch per jenis. Menambah QrisPayment cukup dengan class baru — kode pemanggil tidak berubah. Bandingkan dengan alternatifnya: rantai if (type.equals("CARD")) ... else if ... yang harus diubah setiap kali ada jenis baru.
Tipe deklarasi vs tipe runtime
Setiap variabel referensi punya dua "tipe":
- Tipe deklarasi (static type) — tipe yang ditulis di deklarasi variabel. Compiler memakai tipe ini untuk menentukan method apa saja yang boleh dipanggil.
- Tipe runtime — class object yang sebenarnya. JVM memakai tipe ini untuk menentukan versi mana dari method yang di-override yang dijalankan.
Upcasting — menyimpan object ke variabel bertipe induk/interface — terjadi otomatis dan selalu aman: PaymentMethod method = new CreditCard();. Konsekuensinya, method khusus CreditCard tidak terlihat lewat variabel itu. Untuk memakainya, kamu perlu downcasting: memeriksa tipe runtime lalu mengubah tipe deklarasi.
java
public class CastingDemo {
public static void main(String[] args) {
PaymentMethod method = new CreditCard(); // upcasting: otomatis
// method.installments(3); // error: cannot find symbol
if (method instanceof CreditCard card) { // cek tipe runtime + downcasting
System.out.println(card.installments(200_000, 3));
}
Object value = "INV-001";
System.out.println(value instanceof String);
System.out.println(value instanceof Integer);
}
}
interface PaymentMethod {
long fee(long amount);
}
class CreditCard implements PaymentMethod {
@Override
public long fee(long amount) {
return amount * 25 / 1000;
}
long installments(long amount, int months) {
return (amount + fee(amount)) / months;
}
}Output: 68333, true, false. Bentuk method instanceof CreditCard card disebut pattern matching instanceof (final sejak Java 16): bila cek berhasil, variabel card langsung tersedia dengan tipe CreditCard. Cast manual gaya lama (CreditCard) method tetap ada, tetapi bila tipe runtime-nya salah, program berhenti:
text
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
at Cce.main(Cce.java:4)Downcasting yang sering muncul adalah tanda desain bisa diperbaiki: bila kamu terus-menerus bertanya "object ini jenis apa?", mungkin perilaku itu sebaiknya menjadi method di interface. Untuk kasus di mana memeriksa jenis memang wajar — misalnya hasil pembayaran yang jenisnya terbatas — Java modern punya sealed type dan pattern matching switch (bab Sealed & Pattern Matching).
Overloading vs overriding
Keduanya memakai nama method yang sama, tetapi dipilih pada waktu yang berbeda:
| Aspek | Overloading (M01) | Overriding |
|---|---|---|
| Di mana | dalam satu class (atau warisannya) | subclass/implementasi vs induk/interface |
| Yang berbeda | daftar parameter | implementasinya; signature sama |
| Dipilih oleh | compiler, berdasarkan tipe deklarasi | JVM, berdasarkan tipe runtime |
| Waktu pemilihan | saat kompilasi | saat program berjalan |
java
public class OverloadOverride {
public static void main(String[] args) {
AuditLog log = new AuditLog();
Object payload = "order dibuat"; // tipe deklarasi Object, isi String
log.record(payload); // overload dipilih saat kompilasi
log.record("order dibuat");
Notifier notifier = new SmsNotifier(); // override dipilih saat runtime
notifier.send("halo");
}
}
class AuditLog {
void record(Object payload) {
System.out.println("record(Object)");
}
void record(String payload) {
System.out.println("record(String)");
}
}
class Notifier {
void send(String message) {
System.out.println("Notifier: " + message);
}
}
class SmsNotifier extends Notifier {
@Override
void send(String message) {
System.out.println("SMS: " + message);
}
}Output:
text
record(Object)
record(String)
SMS: haloBaris pertama sering mengejutkan: walaupun isinya String, compiler hanya melihat tipe deklarasi Object. Ingat juga bahwa method static dan field tidak ikut polymorphism. Method static dengan nama sama di subclass hanya "menyembunyikan" (hiding) milik induknya, dan yang dipanggil ditentukan tipe deklarasi.
Program ke interface
Prinsip desain yang paling sering kamu dengar di tim backend: bergantunglah pada interface, bukan implementasi. Artinya:
- tipe field, parameter, dan nilai kembalian memakai interface (
PaymentGateway, bukanHttpPaymentGateway); - object implementasi dibuat di satu tempat lalu diberikan ke class yang membutuhkannya, biasanya lewat constructor;
- class pemakai tidak tahu dan tidak peduli implementasi mana yang ia terima.
Hasilnya, implementasi bisa diganti tanpa menyentuh class pemakai, dan unit test bisa memberikan implementasi palsu.
Contoh di dunia backend
CheckoutService menagih pembayaran lewat PaymentGateway. Di production ia menerima implementasi yang memanggil API sungguhan; di test ia menerima FakePaymentGateway yang hasilnya bisa diatur dan mencatat berapa kali dipanggil.
java
public class CheckoutDemo {
public static void main(String[] args) {
// Production: implementasi yang memanggil API payment gateway sungguhan.
CheckoutService live = new CheckoutService(new HttpPaymentGateway("https://api.pay.example"));
System.out.println(live.checkout("INV-001", 250_000));
// Test: implementasi palsu — cepat, tanpa jaringan, hasil bisa diatur.
FakePaymentGateway fake = new FakePaymentGateway(false);
CheckoutService underTest = new CheckoutService(fake);
System.out.println(underTest.checkout("INV-002", 90_000));
System.out.println("Dipanggil " + fake.calls() + " kali, terakhir " + fake.lastInvoice());
System.out.println(underTest.checkout("INV-003", 0));
System.out.println("Dipanggil " + fake.calls() + " kali");
}
}
interface PaymentGateway {
boolean charge(String invoiceId, long amount);
}
class HttpPaymentGateway implements PaymentGateway {
private final String baseUrl;
HttpPaymentGateway(String baseUrl) {
this.baseUrl = baseUrl;
}
@Override
public boolean charge(String invoiceId, long amount) {
// Di aplikasi nyata: kirim request HTTP ke baseUrl lalu baca responsnya.
System.out.println("POST " + baseUrl + "/charges " + invoiceId + " " + amount);
return true;
}
}
class FakePaymentGateway implements PaymentGateway {
private final boolean result;
private int calls;
private String lastInvoice = "-";
FakePaymentGateway(boolean result) {
this.result = result;
}
@Override
public boolean charge(String invoiceId, long amount) {
calls++;
lastInvoice = invoiceId;
return result;
}
int calls() {
return calls;
}
String lastInvoice() {
return lastInvoice;
}
}
class CheckoutService {
private final PaymentGateway gateway; // bergantung pada kontrak, bukan implementasi
CheckoutService(PaymentGateway gateway) {
this.gateway = gateway;
}
String checkout(String invoiceId, long amount) {
if (amount <= 0) {
return invoiceId + ": DITOLAK (nominal tidak valid)";
}
boolean paid = gateway.charge(invoiceId, amount);
return invoiceId + ": " + (paid ? "LUNAS" : "GAGAL BAYAR");
}
}Output:
text
POST https://api.pay.example/charges INV-001 250000
INV-001: LUNAS
INV-002: GAGAL BAYAR
Dipanggil 1 kali, terakhir INV-002
INV-003: DITOLAK (nominal tidak valid)
Dipanggil 1 kaliPerhatikan baris terakhir: untuk nominal tidak valid, gateway tidak dipanggil — dan test bisa membuktikannya lewat fake.calls(). Memberikan dependency lewat constructor seperti ini disebut constructor injection; di Spring Boot, framework yang membuat dan menyuntikkan object-object itu untukmu. Library seperti Mockito (dibahas di materi testing) membuat implementasi palsu secara otomatis, tetapi prinsipnya sama persis.
Pertanyaan interview
1. Apa itu polymorphism di Java?
Kemampuan satu variabel bertipe induk atau interface merujuk ke object dari berbagai class, sehingga satu pemanggilan method menjalankan perilaku yang berbeda sesuai object sebenarnya. Di Java, ini terwujud lewat overriding dan dynamic dispatch: versi method dipilih berdasarkan tipe runtime saat program berjalan.
2. Apa beda overloading dan overriding, dan kapan masing-masing dipilih?
Overloading: nama sama, daftar parameter berbeda, dipilih compiler saat kompilasi berdasarkan tipe deklarasi argumen. Overriding: subclass/implementasi mengganti method dengan signature sama, dipilih JVM saat runtime berdasarkan tipe object. Method static dan field tidak di-override, hanya disembunyikan.
3. Kenapa sebaiknya "program ke interface"? Apa hubungannya dengan testing?
Class yang bergantung pada interface tidak terikat ke satu implementasi, sehingga implementasi bisa diganti (misalnya pindah penyedia pembayaran) tanpa mengubah class itu. Dalam test, kita memberi implementasi palsu atau mock lewat constructor, sehingga logika bisnis bisa diuji cepat tanpa database atau jaringan.
Jebakan umum
- Rantai
instanceof/ifper jenis object untuk memilih perilaku. Pindahkan perilaku itu ke method yang di-override; tambah jenis baru cukup dengan class baru. - Cast tanpa cek →
ClassCastException. Pakaiif (x instanceof Tipe t). - Mengira overload dipilih dari isi object. Overload ditentukan tipe deklarasi saat kompilasi.
- Mengira method
staticikut polymorphism.staticdipilih dari tipe deklarasi; jangan "meng-override"-nya. - Lupa
@Override. Salah ketik nama atau parameter membuat method baru (overload), bukan override — dan bug-nya diam-diam. Anotasi@Overridemembuat compiler menangkapnya. - Membuat dependency di dalam class (
new HttpPaymentGateway(...)di dalam service) sehingga tidak bisa diganti saat test. Terima dependency lewat constructor.
Ringkasan
- Dynamic dispatch: method instance yang di-override dipilih dari tipe runtime object, saat program berjalan.
- Tipe deklarasi menentukan method apa yang boleh dipanggil; tipe runtime menentukan versi mana yang jalan.
- Upcasting otomatis dan aman; downcasting gunakan pattern
instanceofagar tidakClassCastException. - Overloading dipilih saat kompilasi, overriding saat runtime;
staticdan field tidak polymorphic. - Bergantung pada interface + constructor injection membuat kode mudah diganti dan dites.