Tampilan
Kenapa penting
Di aplikasi nyata kamu sering punya beberapa jenis data yang mirip tapi tidak sama: produk fisik dan produk digital, notifikasi email dan SMS, akun tabungan dan akun giro. Inheritance memungkinkan class turunan mewarisi field dan method class induk, lalu menambah atau mengganti perilaku yang berbeda. Framework seperti Spring dan hierarki exception di Java (M03) dibangun di atas konsep ini, jadi kamu perlu bisa membacanya. Tapi inheritance juga mudah disalahgunakan — hierarki yang terlalu dalam membuat kode kaku. Bab ini membahas cara memakainya dengan benar dan kapan sebaiknya dihindari.
Konsep inti
extends dan super(...)
Class turunan (subclass) ditulis dengan extends. Ia mewarisi semua method dan field class induk (superclass), tetapi field private milik induk tetap hanya bisa diakses lewat method induk. Constructor tidak diwarisi: class turunan memanggil constructor induk dengan super(...).
java
class Product {
private final String name;
private final long price;
Product(String name, long price) {
this.name = name;
this.price = price;
}
String name() {
return name;
}
long price() {
return price;
}
String describe() {
return name + " - Rp" + price;
}
}
class PhysicalProduct extends Product {
private final int weightGrams;
PhysicalProduct(String name, long price, int weightGrams) {
super(name, price); // panggil constructor induk
this.weightGrams = weightGrams;
}
int weightGrams() {
return weightGrams;
}
@Override
String describe() {
return super.describe() + " (" + weightGrams + " g)";
}
}
class DigitalProduct extends Product {
private final String downloadUrl;
DigitalProduct(String name, long price, String downloadUrl) {
super(name, price);
this.downloadUrl = downloadUrl;
}
@Override
String describe() {
return super.describe() + " [unduh: " + downloadUrl + "]";
}
}
public class InheritanceDemo {
public static void main(String[] args) {
PhysicalProduct mug = new PhysicalProduct("Mug", 45_000, 350);
DigitalProduct ebook = new DigitalProduct("E-book Java", 99_000, "/files/java.pdf");
System.out.println(mug.describe());
System.out.println(ebook.describe());
System.out.println(mug.name() + " berat " + mug.weightGrams() + " g");
}
}Output:
text
Mug - Rp45000 (350 g)
E-book Java - Rp99000 [unduh: /files/java.pdf]
Mug berat 350 gmug.name() dipanggil padahal PhysicalProduct tidak menulis method name — method itu diwarisi dari Product. Java hanya mengizinkan satu class induk per class (extends hanya satu nama); cara menggabungkan beberapa "kemampuan" dibahas di bab Interface.
Override, @Override, dan super.method()
Override berarti class turunan menulis ulang method induk dengan nama dan parameter yang sama. Saat mug.describe() dipanggil, versi PhysicalProduct yang jalan. Di dalamnya, super.describe() memanggil versi induk, sehingga kamu cukup menambahkan bagian yang berbeda alih-alih menyalin seluruh logika.
Selalu tulis anotasi @Override. Anotasi ini meminta compiler memastikan method itu benar-benar meng-override sesuatu. Salah ketik sekecil tostring (huruf s kecil) langsung ketahuan:
text
OverrideTypo.java:2: error: method does not override or implement a method from a supertype
@Override
^
1 errorTanpa @Override, kode itu lolos kompilasi dan kamu baru sadar ada bug saat melihat output yang aneh. Ini berbeda dengan overloading di M01: overloading = nama sama, parameter berbeda; override = nama dan parameter sama, di class turunan.
Urutan constructor
Constructor induk selalu dijalankan sebelum constructor turunan. Kalau kamu tidak menulis super(...), compiler otomatis menyisipkan super() tanpa argumen.
java
class Base {
Base() {
System.out.println("1. constructor Base");
}
}
class Middle extends Base {
Middle() {
super(); // boleh ditulis eksplisit; kalau tidak, compiler menambahkannya
System.out.println("2. constructor Middle");
}
}
class Leaf extends Middle {
Leaf() {
System.out.println("3. constructor Leaf");
}
}
public class ConstructorOrder {
public static void main(String[] args) {
new Leaf();
}
}Output: 1. constructor Base, 2. constructor Middle, 3. constructor Leaf. Akibatnya, bila induk hanya punya constructor berparameter (seperti Product), turunannya wajib memanggil super(...) dengan argumen yang sesuai.
Flexible constructor bodies (Java 25)
Dulu super(...) wajib menjadi baris pertama constructor. Akibatnya validasi argumen baru bisa dilakukan setelah constructor induk selesai bekerja. Java 25 memfinalkan JEP 513 (Flexible Constructor Bodies): kamu boleh menulis statement sebelum super(...), misalnya untuk menolak argumen yang tidak valid lebih awal.
java
class Account {
private final String owner;
Account(String owner) {
System.out.println("constructor Account untuk " + owner);
this.owner = owner;
}
String owner() {
return owner;
}
}
class SavingsAccount extends Account {
private final int interestPercent;
SavingsAccount(String owner, int interestPercent) {
if (interestPercent < 0 || interestPercent > 20) { // Java 25: boleh sebelum super(...)
throw new IllegalArgumentException("Bunga tidak valid: " + interestPercent);
}
super(owner);
this.interestPercent = interestPercent;
}
int interestPercent() {
return interestPercent;
}
}
public class FlexibleConstructor {
public static void main(String[] args) {
SavingsAccount ok = new SavingsAccount("Sari", 5);
System.out.println(ok.owner() + " " + ok.interestPercent() + "%");
new SavingsAccount("Budi", 50); // gagal SEBELUM constructor Account dijalankan
}
}Output-nya constructor Account untuk Sari, lalu Sari 5%, lalu program berhenti dengan IllegalArgumentException: Bunga tidak valid: 50 — tanpa mencetak constructor Account untuk Budi, karena validasi gagal sebelum constructor induk sempat jalan.
Batasannya: sebelum super(...), object belum siap, jadi kamu tidak boleh memakai this — membaca field, memanggil method instance, atau mengoper this ke tempat lain. Mencobanya menghasilkan error seperti cannot reference log(String) before supertype constructor has been called. Memeriksa parameter, memanggil method static, dan menyiapkan variabel lokal boleh.
protected dan class Object
Selain private dan public dari bab Encapsulation, ada protected: bisa diakses class turunan (dan class lain di package yang sama). Pakai seperlunya — lebih aman field tetap private, lalu sediakan method protected bila turunan butuh membacanya, seperti recipient() di contoh backend di bawah.
Setiap class yang tidak menulis extends otomatis menjadi turunan java.lang.Object. Dari sana semua object mewarisi method seperti toString(), equals(), dan hashCode(). Versi bawaan toString() hanya mencetak nama class dan kode hash, misalnya Customer@1b6d3586 (angkanya berbeda di setiap mesin) — tidak berguna di log. Karena itu class data biasanya meng-override toString():
java
// Potongan
@Override
public String toString() {
return "Invoice{number=" + number + "}";
}Method toString() milik Object bersifat public, jadi versi override-nya juga harus public. Override equals dan hashCode punya aturan khusus yang penting untuk collection — dibahas di M03.
final method dan final class
final pada method mencegah method itu di-override; final pada class mencegah class itu diturunkan sama sekali. Compiler menolak keduanya dengan pesan yang jelas:
text
FinalErrors.java:14: error: cannot inherit from final String
class MyString extends String {
^
FinalErrors.java:9: error: fee(long) in CheapGateway cannot override fee(long) in PaymentGateway
long fee(long amount) {
^
overridden method is final
2 errorsGunakan final untuk langkah yang tidak boleh diubah turunan (misalnya urutan proses) dan untuk class immutable seperti di bab sebelumnya.
Kapan inheritance tepat? Composition over inheritance
Pakai inheritance hanya untuk relasi is-a yang stabil: PhysicalProduct adalah Product. Untuk relasi has-a atau "memakai" — pesanan punya pelanggan, service memakai pengirim email — gunakan composition: simpan object lain sebagai field dan panggil method-nya.
java
// Potongan: composition — OrderService MEMAKAI EmailSender, bukan turunan EmailSender
class OrderService {
private final EmailSender emailSender;
OrderService(EmailSender emailSender) {
this.emailSender = emailSender;
}
void confirm(String orderId, String email) {
emailSender.send(email, "Pesanan " + orderId + " dikonfirmasi");
}
}Hierarki inheritance yang dalam membuat perubahan di class induk merembet ke semua turunan dan sulit diuji. Composition lebih lentur: bagian yang dipakai bisa diganti tanpa mengubah hierarki. Prinsip ini dikenal sebagai "composition over inheritance" dan akan sering kamu temui saat memakai Spring, yang menyusun aplikasi dari object-object kecil yang saling dipakai.
Contoh di dunia backend
Layanan notifikasi mengirim pesan lewat beberapa kanal. Semua notifikasi punya penerima dan pesan; yang berbeda adalah nama kanal dan cara pesan dirender. Class induk menetapkan urutan pengiriman dalam method final, sedangkan turunan hanya meng-override bagian yang berbeda:
java
class Notification {
private final String recipient;
private final String message;
Notification(String recipient, String message) {
if (recipient == null || recipient.isBlank()) {
throw new IllegalArgumentException("Penerima wajib diisi");
}
this.recipient = recipient;
this.message = message;
}
protected String recipient() {
return recipient;
}
protected String message() {
return message;
}
String channel() {
return "LOG";
}
String render() {
return message;
}
// final: urutan langkah pengiriman sama untuk semua turunan
final String send() {
return "[" + channel() + "] ke " + recipient + ": " + render();
}
}
class EmailNotification extends Notification {
private final String subject;
EmailNotification(String email, String subject, String message) {
if (!email.contains("@")) {
throw new IllegalArgumentException("Email tidak valid: " + email);
}
super(email, message);
this.subject = subject;
}
@Override
String channel() {
return "EMAIL";
}
@Override
String render() {
return "Subjek: " + subject + " | " + super.render();
}
}
class SmsNotification extends Notification {
private static final int MAX_LENGTH = 30;
SmsNotification(String phone, String message) {
super(phone, message);
}
@Override
String channel() {
return "SMS";
}
@Override
String render() {
String text = message();
return text.length() <= MAX_LENGTH ? text : text.substring(0, MAX_LENGTH - 3) + "...";
}
}
public class NotificationDemo {
public static void main(String[] args) {
Notification audit = new Notification("ops-team", "Stok kopi di bawah 10");
EmailNotification email = new EmailNotification("budi@contoh.com", "Pesanan dikirim",
"Pesanan INV-001 sedang dalam perjalanan");
SmsNotification sms = new SmsNotification("081234567890",
"Kode OTP kamu 482913, berlaku 5 menit. Jangan bagikan.");
System.out.println(audit.send());
System.out.println(email.send());
System.out.println(sms.send());
}
}Output:
text
[LOG] ke ops-team: Stok kopi di bawah 10
[EMAIL] ke budi@contoh.com: Subjek: Pesanan dikirim | Pesanan INV-001 sedang dalam perjalanan
[SMS] ke 081234567890: Kode OTP kamu 482913, berla...Perhatikan: send() didefinisikan sekali di induk, tetapi memanggil channel() dan render() versi turunan. Pola "induk menetapkan kerangka, turunan mengisi detail" ini disebut template method. EmailNotification memakai flexible constructor bodies untuk menolak email tanpa @ sebelum constructor induk dipanggil, dan SmsNotification membaca pesan lewat method protected message() karena field induk tetap private. Bagaimana satu variabel bertipe Notification bisa menunjuk object EmailNotification dibahas di bab Polymorphism.
Pertanyaan interview
1. Kenapa Java tidak mengizinkan satu class meng-extends lebih dari satu class?
Untuk menghindari ambiguitas ketika dua induk punya method atau field dengan nama sama (dikenal sebagai diamond problem): implementasi mana yang harus diwarisi? Java memilih satu induk class saja, dan menyediakan interface untuk menggabungkan beberapa kemampuan.
2. Apa maksud "composition over inheritance"?
Utamakan menyusun class dari object lain yang disimpan sebagai field (has-a) daripada menurunkan class (is-a). Composition membuat class lebih longgar keterikatannya, mudah diganti bagiannya, dan mudah diuji. Inheritance tetap dipakai untuk relasi is-a yang jelas dan stabil.
3. Bagaimana urutan constructor pada hierarki class, dan apa yang berubah di Java 25?
Constructor induk selalu selesai lebih dulu sebelum isi constructor turunan dijalankan; bila super(...) tidak ditulis, compiler menyisipkan super(). Sejak Java 25 (JEP 513) boleh ada statement sebelum super(...) — misalnya validasi argumen — asalkan tidak memakai this.
Jebakan umum
- Lupa
@Override. Salah ketik nama atau parameter membuat method baru, bukan override, dan bug-nya diam-diam. - Override
toString()tanpapublic. Method milikObjectitupublic; mempersempit akses saat override ditolak compiler. - Memakai
thissebelumsuper(...). Walau Java 25 mengizinkan statement di awal constructor, object belum siap — hanya parameter, variabel lokal, dan methodstaticyang boleh dipakai. - Membuat field induk
protectedataupublicdemi kemudahan. Turunan jadi bergantung pada detail internal induk. Pertahankanprivatedan sediakan method. - Inheritance untuk relasi has-a.
class Order extends ArrayListatauclass OrderService extends EmailSendermewarisi puluhan method yang tidak relevan. Pakai composition. - Hierarki terlalu dalam. Lebih dari dua-tiga tingkat biasanya tanda desain perlu dipecah.
Ringkasan
extendsmewarisi method & field induk; constructor induk dipanggil dengansuper(...)dan selalu jalan lebih dulu.- Override mengganti perilaku method induk; selalu tulis
@Override, dan pakaisuper.method()untuk memakai ulang. - Semua class turunan
Object; overridetoString()agar object mudah dibaca di log. - Java 25 (JEP 513) mengizinkan validasi sebelum
super(...), tanpa memakaithis. finalmethod tidak bisa di-override,finalclass tidak bisa diturunkan;protecteddipakai seperlunya.- Inheritance untuk relasi is-a yang stabil; selebihnya utamakan composition.