Pewarisan (extends) terasa alami saat belajar OOP: tinggal turunkan class, tambah sedikit method, selesai. Namun pada sistem yang hidup bertahun-tahun, pewarisan yang dipakai untuk meminjam implementasi justru menjadi sumber bug halus — subclass menempel pada detail internal superclass yang boleh berubah kapan saja. Prinsip composition over Inheritance menawarkan jalan lain: susun objek dari bagian-bagian kecil yang dimiliki dan diatur lewat delegasi, bukan dari class besar yang diwarisi. Chapter ini membuktikan dua jebakan klasik pewarisan, lalu menunjukkan bahwa komposisi menyelesaikan keduanya dengan kode yang lebih aman.
Jebakan Pertama Penghitung Ganda #
Contoh klasiknya berasal dari buku Effective Java: kita ingin Set yang menghitung berapa kali elemen ditambahkan. Versi pewarisan tampak wajar — cukup extends HashSet lalu override add dan addAll:
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class KoleksiDihitungWarisan {
public static void main(String[] args) {
SetDiukur<String> himpunan = new SetDiukur<>();
himpunan.addAll(List.of("Budi", "Siti", "Dewi"));
System.out.println("Jumlah elemen : " + himpunan.size());
System.out.println("Tambah tercatat: " + himpunan.jumlahTambah());
}
}
// Pewarisan dari HashSet: implementasi berbasis detail internal
class SetDiukur<E> extends HashSet<E> {
private int jumlahTambah = 0;
@Override
public boolean add(E e) {
jumlahTambah++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
jumlahTambah += c.size();
return super.addAll(c);
}
int jumlahTambah() {
return jumlahTambah;
}
}Output yang Dihasilkan:
Jumlah elemen : 3
Tambah tercatat: 6Hasilnya mencatat 6 penambahan padahal elemen hanya 3. Mari kita bedah penyebabnya:
- Baris 25-29 (
addAll): override ini menambahc.size()(tiga) ke penghitung, lalu memanggilsuper.addAll(c). - Baris 19-23 (
add): di dalamjava.util.AbstractCollection, methodaddAllbawaan diimplementasikan sebagai loop yang memanggiladd(e)untuk setiap elemen — dan pemanggilan tersebut terikat secara dinamis ke override diSetDiukur. - Akibat gabungan: setiap elemen terhitung dua kali — sekali di
addAlloverride, sekali lagi diaddoverride. KelasHashSetmemang meng-overrideaddAllsecara efisien, tetapi kontrak “implementasiaddAllmemanggiladd” adalah detail internal yang tidak dijamin spesifikasinya.
Perbaikan satu baris (hapus addAll) berhenti bekerja begitu implementasi JDK berubah. Inilah inti masalahnya: subclass mewarisi implementasi, dan implementasi boleh berganti antar versi pustaka.
Komposisi dengan Delegasi #
Versi komposisi tidak lagi mewarisi HashSet. Class pembungkus memiliki sebuah Set dan mendelegasikan setiap operasi ke objek tersebut:
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class KoleksiDihitungKomposisi {
public static void main(String[] args) {
SetDiukur<String> himpunan = new SetDiukur<>(new HashSet<>());
himpunan.addAll(List.of("Budi", "Siti", "Dewi"));
System.out.println("Jumlah elemen : " + himpunan.size());
System.out.println("Tambah tercatat: " + himpunan.jumlahTambah());
}
}
// Komposisi: memiliki Set, mendelegasikan pekerjaan kepadanya
class SetDiukur<E> {
private final Set<E> set;
private int jumlahTambah = 0;
SetDiukur(Set<E> set) {
this.set = set;
}
public boolean add(E e) {
jumlahTambah++;
return set.add(e);
}
public boolean addAll(Collection<? extends E> c) {
int ukuranSebelum = set.size();
boolean berubah = set.addAll(c);
jumlahTambah += set.size() - ukuranSebelum;
return berubah;
}
public int size() {
return set.size();
}
int jumlahTambah() {
return jumlahTambah;
}
}Output yang Dihasilkan:
Jumlah elemen : 3
Tambah tercatat: 3Mari kita bedah struktur delegasi tersebut:
- Baris 17 (
private final Set<E> set): field bertipe interfaceSet, bukan classHashSet. Pembungkus tidak bergantung pada detail implementasi mana pun —TreeSetatauConcurrentHashMap.newKeySet()dapat masuk tanpa perubahan kode. - Baris 20-22 (
SetDiukur(Set<E> set)): dependensi disuntikkan lewat constructor. Pola ini disebut forwarding: pembungkus menerima permintaan, meneruskannya, menambahkan catatannya sendiri. - Baris 29-34 (
addAll): penghitung kini mengukur hasil yang teramati — selisih ukuran sebelum dan sesudah — bukan cara internaladdAllbekerja. Berapa punaddAllmemanggiladddi dalam, penghitungan tetap benar.
Jebakan Kedua Overridable Call di Constructor #
Pewarisan menyimpan jebakan kedua yang berlangsung sebelum subclass selesai dibentuk: constructor superclass yang memanggil method yang dapat di-override. Aturan eksekusi JVM menyatakan constructor super() berjalan lebih dulu, sebelum field subclass diinisialisasi — sehingga override di subclass membaca field yang masih null:
public class PanggilanDiConstructor {
public static void main(String[] args) {
PapanSkor papan = new PapanSkor("Budi");
System.out.println("Nama tersimpan: " + papan.nama());
}
}
class Dasbor {
Dasbor() {
mulai(); // constructor memanggil method yang dapat di-override
}
void mulai() {
System.out.println("Dasbor siap");
}
}
class PapanSkor extends Dasbor {
private final String nama;
PapanSkor(String nama) {
this.nama = nama;
}
String nama() {
return nama;
}
@Override
void mulai() {
System.out.println("Halo " + nama + ", papan dibuat");
}
}Output yang Dihasilkan:
Halo null, papan dibuat
Nama tersimpan: BudiUrutan peristiwanya sebagai berikut:
- Baris 3 (
new PapanSkor("Budi")): JVM mengalokasikan objek, lalu menjalankan constructorDasborterlebih dahulu. - Baris 10 (
mulai()di constructorDasbor): pemanggilan terikat secara dinamis ke overridePapanSkor.mulai()— meski objekPapanSkorbelum selesai dibentuk. - Baris 31 (
"Halo " + nama): fieldnamabelum sempat diisi karena constructorPapanSkorbelum berjalan, sehingga tercetaknull. Baris berikutnya membuktikan field kemudian terisi:Nama tersimpan: Budi.
Kapan Pewarisan Tetap Layak #
Pewarisan tetap tepat saat hubungan benar-benar is-a dan superclass memang dirancang untuk diwarisi — misalnya abstract class tanpa field mutable, atau hierarki tipe seperti sealed interface dengan implementasi terbatas. Komposisi unggul saat kita hanya ingin meminjam perilaku: menghitung, mencatat, membatasi, atau menunda. Aturan praktisnya: wariskan tipe, komposisikan perilaku. Topik berikutnya membawa kita ke disiplin yang lebih luas lagi — menjaga kode tetap terbaca manusia lewat Clean Code.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
KoleksiDihitungKomposisi.java
Unduh berkasimport java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class KoleksiDihitungKomposisi {
public static void main(String[] args) {
SetDiukur<String> himpunan = new SetDiukur<>(new HashSet<>());
himpunan.addAll(List.of("Budi", "Siti", "Dewi"));
System.out.println("Jumlah elemen : " + himpunan.size());
System.out.println("Tambah tercatat: " + himpunan.jumlahTambah());
}
}
// Komposisi: memiliki Set, mendelegasikan pekerjaan kepadanya
class SetDiukur<E> {
private final Set<E> set;
private int jumlahTambah = 0;
SetDiukur(Set<E> set) {
this.set = set;
}
public boolean add(E e) {
jumlahTambah++;
return set.add(e);
}
public boolean addAll(Collection<? extends E> c) {
int ukuranSebelum = set.size();
boolean berubah = set.addAll(c);
jumlahTambah += set.size() - ukuranSebelum;
return berubah;
}
public int size() {
return set.size();
}
int jumlahTambah() {
return jumlahTambah;
}
}
KoleksiDihitungWarisan.java
Unduh berkasimport java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class KoleksiDihitungWarisan {
public static void main(String[] args) {
SetDiukur<String> himpunan = new SetDiukur<>();
himpunan.addAll(List.of("Budi", "Siti", "Dewi"));
System.out.println("Jumlah elemen : " + himpunan.size());
System.out.println("Tambah tercatat: " + himpunan.jumlahTambah());
}
}
// Pewarisan dari HashSet: implementasi berbasis detail internal
class SetDiukur<E> extends HashSet<E> {
private int jumlahTambah = 0;
@Override
public boolean add(E e) {
jumlahTambah++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
jumlahTambah += c.size();
return super.addAll(c);
}
int jumlahTambah() {
return jumlahTambah;
}
}
PanggilanDiConstructor.java
Unduh berkaspublic class PanggilanDiConstructor {
public static void main(String[] args) {
PapanSkor papan = new PapanSkor("Budi");
System.out.println("Nama tersimpan: " + papan.nama());
}
}
class Dasbor {
Dasbor() {
mulai(); // constructor memanggil method yang dapat di-override
}
void mulai() {
System.out.println("Dasbor siap");
}
}
class PapanSkor extends Dasbor {
private final String nama;
PapanSkor(String nama) {
this.nama = nama;
}
String nama() {
return nama;
}
@Override
void mulai() {
System.out.println("Halo " + nama + ", papan dibuat");
}
}