Pemrograman Dart
Polymorphism, @override, dan Perilaku Objek (toString, ==)
Pelajari cara mengubah perilaku method kelas induk dengan @override, memanfaatkan polimorfisme untuk menangani beragam subclass secara seragam, serta menyesuaikan toString dan operator == dari kelas Object.

Pada materi sebelumnya, kita telah mempelajari bagaimana extends memungkinkan sebuah subclass mewarisi properti dan method dari superclass. Namun, dalam kenyataan, setiap kelas turunan sering kali memiliki cara unik dalam menjalankan sebuah aksi yang namanya sama. Sebagai contoh, pembayaran melalui transfer bank dan dompet digital sama-sama memiliki aksi prosesPembayaran(), tetapi langkah verifikasi serta potongan biayanya berbeda. Di sinilah Polymorphism (polimorfisme) dan Method Overriding berperan: kita dapat menyeragamkan cara pemanggilan method melalui satu tipe induk, sembari membiarkan setiap objek turunan mengeksekusi implementasi spesifiknya masing-masing.
Yang akan dipelajari
- Memahami konsep method overriding menggunakan anotasi
@overridedan kata kuncisuper. - Menerapkan polimorfisme (many forms) untuk mengelola berbagai subclass melalui satu referensi superclass.
- Mengenal
Objectsebagai akar dari seluruh kelas di Dart. - Meng-override
toString()agar objek menampilkan teks yang informatif saat dicetak. - Meng-override
operator ==danhashCodeuntuk membandingkan kesamaan nilai antar-objek (value equality).
Konsep Method Overriding dan Anotasi @override
Method overriding adalah teknik ketika subclass menulis ulang implementasi sebuah method (atau getter/setter) yang sudah didefinisikan di superclass. Agar pendefinisian ulang ini sah dan aman di Dart, kita perlu mengikuti aturan berikut:
- Nama dan jumlah parameter harus sama dengan method yang ada di kelas induk.
- Tipe nilai kembalian (return type) harus sama atau merupakan subtipe dari tipe kembalian method induk.
- Tambahkan anotasi
@overridetepat di atas deklarasi method. Meskipun secara teknis kode tetap bisa berjalan tanpa@override, anotasi ini sangat dianjurkan oleh Effective Dart karena memberi tahu compiler untuk memeriksa apakah method tersebut benar-benar ada di kelas induk. Jika kita salah ketik nama method (misalnyahitungbiayapadahal di indukhitungBiaya), analyzer VS Code langsung memberikan peringatan.
Berikut perbandingan antara pewarisan method biasa, overriding penuh, dan overriding yang masih memperluas perilaku induk:
| Pendekatan di Subclass | Apa yang Dilakukan? | Kapan Digunakan? |
|---|---|---|
| Tidak menulis ulang method | Memakai implementasi superclass apa adanya | Perilaku subclass sama persis dengan induk |
@override tanpa super.namaMethod() |
Mengganti total perilaku superclass | Rumus atau alur di subclass benar-benar berbeda |
@override dengan super.namaMethod() |
Menjalankan logika induk terlebih dahulu, lalu menambah logika khusus subclass | Ingin mempertahankan langkah dasar induk dan menambah detail |
Mari kita lihat penerapannya pada sistem pengiriman barang:
class Pengiriman {
final String resi;
final double beratKg;
Pengiriman({required this.resi, required this.beratKg});
double hitungOngkir() {
// Tarif dasar reguler: Rp10.000 per kg
return beratKg * 10000;
}
String ringkasan() {
return '[$resi] Berat: ${beratKg}kg | Ongkir: Rp${hitungOngkir().toStringAsFixed(0)}';
}
}
class PengirimanKilat extends Pengiriman {
final double biayaAsuransi;
PengirimanKilat({
required super.resi,
required super.beratKg,
required this.biayaAsuransi,
});
@override
double hitungOngkir() {
// Tarif kilat: Rp18.000 per kg + biaya asuransi
return (beratKg * 18000) + biayaAsuransi;
}
@override
String ringkasan() {
// Memanggil ringkasan() milik induk lalu menambahkan penanda KILAT
return '${super.ringkasan()} (LAYANAN KILAT)';
}
}
void main() {
final reguler = Pengiriman(resi: 'REG-001', beratKg: 2.5);
final kilat = PengirimanKilat(
resi: 'EXP-002',
beratKg: 2.5,
biayaAsuransi: 5000,
);
print(reguler.ringkasan());
print(kilat.ringkasan());
}
Output:
[REG-001] Berat: 2.5kg | Ongkir: Rp25000
[EXP-002] Berat: 2.5kg | Ongkir: Rp50000 (LAYANAN KILAT)
Perhatikan bagaimana di dalam Pengiriman.ringkasan(), pemanggilan hitungOngkir() secara otomatis menjalankan versi PengirimanKilat.hitungOngkir() ketika objek yang dibuat adalah PengirimanKilat. Mekanisme ini disebut dynamic dispatch (penentuan method saat waktu jalan berdasarkan tipe asli objek di memori).
Memahami Polymorphism (Polimorfisme)
Secara harfiah, polymorphism berarti "banyak bentuk". Dalam OOP, polimorfisme memungkinkan sebuah variabel, parameter fungsi, atau koleksi (List) yang bertipe superclass untuk menyimpan objek dari berbagai subclass turunannya.
Bayangkan kita sedang membangun sistem kasir yang mendukung pembayaran via Transfer Bank, E-Wallet, dan Kartu Kredit. Tanpa polimorfisme, kita harus membuat List terpisah untuk tiap metode pembayaran dan menulis percabangan if-else yang panjang. Dengan polimorfisme, cukup satu List<MetodePembayaran>:
class MetodePembayaran {
final String namaPemilik;
MetodePembayaran(this.namaPemilik);
double hitungTotalBayar(double nominalBelanja) {
return nominalBelanja;
}
void cetakStruk(double nominalBelanja) {
final total = hitungTotalBayar(nominalBelanja);
print('Pemilik: $namaPemilik | Total dibebankan: Rp${total.toStringAsFixed(0)}');
}
}
class PembayaranTransferBank extends MetodePembayaran {
final String namaBank;
PembayaranTransferBank(super.namaPemilik, {required this.namaBank});
@override
double hitungTotalBayar(double nominalBelanja) {
// Biaya admin tetap Rp2.500
return nominalBelanja + 2500;
}
@override
void cetakStruk(double nominalBelanja) {
print('--- Transfer Bank ($namaBank) ---');
super.cetakStruk(nominalBelanja);
}
}
class PembayaranEWallet extends MetodePembayaran {
final double persenCashback;
PembayaranEWallet(super.namaPemilik, {this.persenCashback = 0.05});
@override
double hitungTotalBayar(double nominalBelanja) {
// Potongan langsung dari promosi dompet digital
final potongan = nominalBelanja * persenCashback;
return nominalBelanja - potongan;
}
@override
void cetakStruk(double nominalBelanja) {
print('--- Dompet Digital (Hemat ${(persenCashback * 100).toStringAsFixed(0)}%) ---');
super.cetakStruk(nominalBelanja);
}
}
class PembayaranKartuKredit extends MetodePembayaran {
PembayaranKartuKredit(super.namaPemilik);
@override
double hitungTotalBayar(double nominalBelanja) {
// Biaya layanan 2% dari transaksi
return nominalBelanja * 1.02;
}
@override
void cetakStruk(double nominalBelanja) {
print('--- Kartu Kredit (Layanan 2%) ---');
super.cetakStruk(nominalBelanja);
}
}
void prosesSemuaTransaksi(List<MetodePembayaran> daftarTransaksi, double nominal) {
for (final metode in daftarTransaksi) {
// Satu baris pemanggilan yang sama, tetapi perilakunya menyesuaikan objek asli!
metode.cetakStruk(nominal);
}
}
void main() {
final antreanKasir = <MetodePembayaran>[
PembayaranTransferBank('Budi Santoso', namaBank: 'Bank Nusantara'),
PembayaranEWallet('Siti Aminah', persenCashback: 0.10),
PembayaranKartuKredit('Andi Wijaya'),
];
prosesSemuaTransaksi(antreanKasir, 100000);
}
Output:
--- Transfer Bank (Bank Nusantara) ---
Pemilik: Budi Santoso | Total dibebankan: Rp102500
--- Dompet Digital (Hemat 10%) ---
Pemilik: Siti Aminah | Total dibebankan: Rp90000
--- Kartu Kredit (Layanan 2%) ---
Pemilik: Andi Wijaya | Total dibebankan: Rp102000
Fungsi prosesSemuaTransaksi sama sekali tidak perlu tahu detail perhitungan tiap metode pembayaran. Jika bulan depan perusahaan menambahkan PembayaranQRIS extends MetodePembayaran, fungsi prosesSemuaTransaksi tidak perlu diubah sedikit pun.
Semua Kelas di Dart Mewarisi Object
Di Dart, setiap kelas yang kita buat—bahkan yang tidak menuliskan extends apa pun—secara otomatis merupakan turunan langsung atau tidak langsung dari kelas bawaan bernama Object. Artinya, deklarasi class Produk {} sebenarnya setara dengan class Produk extends Object {}.
Karena mewarisi Object, setiap objek di Dart pasti memiliki method dan properti bawaan berikut:
Anggota dari Object |
Fungsi Bawaan | Mengapa Sering Di-override? |
|---|---|---|
String toString() |
Mengembalikan teks "Instance of 'NamaKelas'" |
Agar saat di-print(objek), yang tampil adalah isi data objek yang mudah dibaca |
bool operator ==(Object other) |
Memeriksa apakah dua variabel menunjuk ke alamat memori yang sama persis (referential equality) | Agar dua objek dengan isi data yang sama dianggap bernilai sama (value equality) |
int get hashCode |
Menghasilkan angka kode hash berdasarkan identitas memori objek | Wajib di-override bersama operator == agar objek bekerja benar di dalam Set dan kunci Map |
Meng-override toString()
Pernahkah Anda mencetak sebuah objek langsung dengan print(produk) lalu hanya mendapatkan tulisan Instance of 'Produk'? Itu adalah hasil implementasi standar toString() dari kelas Object. Mari kita ubah perilakunya dengan @override:
class Buku {
final String isbn;
final String judul;
final int tahunTerbit;
const Buku({
required this.isbn,
required this.judul,
required this.tahunTerbit,
});
@override
String toString() {
return 'Buku(isbn: $isbn, judul: "$judul", tahun: $tahunTerbit)';
}
}
void main() {
const bukuDart = Buku(
isbn: '978-602-01',
judul: 'Pemrograman Dart Modern',
tahunTerbit: 2026,
);
// print() secara otomatis memanggil bukuDart.toString()
print(bukuDart);
}
Output:
Buku(isbn: 978-602-01, judul: "Pemrograman Dart Modern", tahun: 2026)
Meng-override operator == dan hashCode
Secara bawaan, tanda == pada dua objek hanya bernilai true jika keduanya merupakan instansiasi di alamat memori yang sama (atau keduanya const dengan argumen identik). Jika kita membuat dua objek non-konstan dengan data yang sama persis, Dart akan menganggapnya berbeda (false).
Agar dua objek Koordinat atau Mahasiswa dianggap sama ketika nilai field-nya sama, kita harus meng-override operator == sekaligus getter hashCode (gunakan Object.hash(...) yang sudah disediakan Dart SDK):
class Mahasiswa {
final String nim;
final String nama;
Mahasiswa({required this.nim, required this.nama});
@override
bool operator ==(Object other) {
// 1. Jika menunjuk ke objek memori yang sama, pasti true
if (identical(this, other)) return true;
// 2. Pastikan tipe other adalah Mahasiswa dan nilai field kuncinya sama
return other is Mahasiswa && other.nim == nim && other.nama == nama;
}
@override
int get hashCode => Object.hash(nim, nama);
@override
String toString() => 'Mahasiswa($nim - $nama)';
}
void main() {
final mhs1 = Mahasiswa(nim: '24001', nama: 'Rina Permata');
final mhs2 = Mahasiswa(nim: '24001', nama: 'Rina Permata');
final mhs3 = Mahasiswa(nim: '24002', nama: 'Dedi Kurniawan');
print('Apakah mhs1 == mhs2? ${mhs1 == mhs2}');
print('Apakah mhs1 == mhs3? ${mhs1 == mhs3}');
// Karena == dan hashCode sudah di-override, Set otomatis menolak duplikat nilai!
final daftarHadir = <Mahasiswa>{mhs1, mhs2, mhs3};
print('Jumlah mahasiswa unik di Set: ${daftarHadir.length}');
print(daftarHadir);
}
Output:
Apakah mhs1 == mhs2? true
Apakah mhs1 == mhs3? false
Jumlah mahasiswa unik di Set: 2
{Mahasiswa(24001 - Rina Permata), Mahasiswa(24002 - Dedi Kurniawan)}
Coba sendiri
Buka proyek latihan Dart Anda di VS Code, lalu buat file bin/uji_polimorfisme.dart:
- Salin kode contoh
Mahasiswadi atas dan jalankan menggunakan perintahdart run bin/uji_polimorfisme.dartdi terminal VS Code. - Sekarang, coba beri komentar (
//) pada blok@override bool operator ==dan@override int get hashCode, lalu jalankan ulangdart run bin/uji_polimorfisme.dart. - Amati perbedaannya:
mhs1 == mhs2akan berubah menjadifalse, dan ukuranSet(daftarHadir.length) menjadi3karena Dart kembali membandingkan alamat memori, bukan isi datanya!
Kesalahan yang sering terjadi
| Kesalahan Pemula | Mengapa Bermasalah? | Solusi yang Benar |
|---|---|---|
Mengubah method induk di subclass tanpa menulis @override |
Jika salah eja nama method (misal toSting()), Dart menganggapnya method baru, bukan override dari toString() |
Selalu tulis @override di atas method, getter, atau setter yang menimpa milik induk |
Memanggil this.namaMethod() di dalam method yang di-override saat bermaksud memanggil versi induk |
Mengakibatkan infinite recursion (Stack Overflow) karena method memanggil dirinya sendiri tanpa henti |
Gunakan super.namaMethod() untuk merujuk ke implementasi milik superclass |
Meng-override operator == tetapi lupa meng-override hashCode |
Objek dengan nilai == yang true tetap dianggap berbeda saat dimasukkan ke Set atau dijadikan key pada Map |
Selalu pasangkan @override bool operator == dengan @override int get hashCode => Object.hash(...) |
| Mengubah tipe parameter pada method yang di-override menjadi lebih sempit | Melanggar kontrak polimorfisme sehingga kode gagal dikompilasi | Pertahankan tanda tangan parameter (signature) yang sama dengan superclass |
Kaitannya dengan Flutter / Dunia Nyata: Hampir setiap layar yang Anda bangun di Flutter menggunakan
@overridedan polimorfisme! Ketika Anda membuatclass BerandaPage extends StatelessWidget, Anda wajib menulis@override Widget build(BuildContext context). Mesin perender Flutter menyimpan seluruh komponen antarmuka sebagai koleksi bertipeWidget(polimorfisme), lalu memanggil.build()pada masing-masing komponen tanpa perlu memusingkan apakah itu tombol, teks, atau kartu produk buatan Anda. Selain itu, meng-override==danhashCodesangat krusial pada manajemen state Flutter (seperti BLoC atau Riverpod) agar antarmuka hanya digambar ulang ketika data di dalam state benar-benar berubah.
Latihan
- Notifikasi Multi-Saluran: Buat kelas induk
Notifikasidengan propertipenerimadanpesan, serta methodString kirim(). Buat dua kelas turunan:NotifikasiEmail(menambahkan propertisubjek) danNotifikasiWhatsApp(menambahkan propertinomorHp). Override methodkirim()di masing-masing kelas turunan, lalu masukkan keduanya ke dalam satuList<Notifikasi>dan cetak hasil pengiriman lewat perulanganfor-in. - Override dengan
super: Buat kelasPegawaidengan propertinamadangajiPokok, serta methoddouble hitungGaji()yang mengembalikangajiPokok. Buat kelas turunanManajerdengan tambahan propertitunjanganJabatan. Di dalamManajer, overridehitungGaji()dengan memanggilsuper.hitungGaji() + tunjanganJabatan, serta overridetoString()agar menampilkan nama dan total gajinya. - Kesamaan Nilai Produk (
==&hashCode): Buat kelasVoucherDiskondengan propertikodeVoucher(String) dannominalPotongan(int). OverridetoString(),operator ==, danhashCodesehingga dua objekVoucherDiskondianggap sama apabilakodeVoucher-nya sama (tidak peduli huruf besar/kecil, gunakan.toUpperCase()). Uji dengan memasukkan dua objek berkode'hemat50'dan'HEMAT50'ke dalam sebuahSet<VoucherDiskon>.
Kunci jawaban dan pembahasan
Latihan 1
class Notifikasi {
final String penerima;
final String pesan;
Notifikasi({required this.penerima, required this.pesan});
String kirim() {
return 'Mengirim pesan umum ke $penerima: "$pesan"';
}
}
class NotifikasiEmail extends Notifikasi {
final String subjek;
NotifikasiEmail({
required super.penerima,
required super.pesan,
required this.subjek,
});
@override
String kirim() {
return '[EMAIL] Ke: $penerima | Subjek: $subjek | Isi: "$pesan"';
}
}
class NotifikasiWhatsApp extends Notifikasi {
final String nomorHp;
NotifikasiWhatsApp({
required super.penerima,
required super.pesan,
required this.nomorHp,
});
@override
String kirim() {
return '[WHATSAPP] Ke: $penerima ($nomorHp) -> "$pesan"';
}
}
void main() {
final antreanNotifikasi = <Notifikasi>[
NotifikasiEmail(
penerima: 'rina@contoh.id',
subjek: 'Tagihan Bulanan',
pesan: 'Tagihan bulan ini telah terbit.',
),
NotifikasiWhatsApp(
penerima: 'Rina',
nomorHp: '+628123456789',
pesan: 'Kode OTP Anda adalah 8842.',
),
];
for (final notif in antreanNotifikasi) {
print(notif.kirim());
}
}
Pembahasan: Kita menampung NotifikasiEmail dan NotifikasiWhatsApp di dalam satu List<Notifikasi>. Saat notif.kirim() dipanggil di dalam perulangan, Dart secara polimorfik memilih method kirim() milik kelas asli masing-masing objek.
Latihan 2
class Pegawai {
final String nama;
final double gajiPokok;
Pegawai({required this.nama, required this.gajiPokok});
double hitungGaji() => gajiPokok;
@override
String toString() {
return 'Pegawai: $nama | Total Gaji: Rp${hitungGaji().toStringAsFixed(0)}';
}
}
class Manajer extends Pegawai {
final double tunjanganJabatan;
Manajer({
required super.nama,
required super.gajiPokok,
required this.tunjanganJabatan,
});
@override
double hitungGaji() {
return super.hitungGaji() + tunjanganJabatan;
}
@override
String toString() {
return 'Manajer: $nama | Total Gaji: Rp${hitungGaji().toStringAsFixed(0)} (Termasuk Tunjangan)';
}
}
void main() {
final staf = Pegawai(nama: 'Dimas', gajiPokok: 5000000);
final manajer = Manajer(
nama: 'Lestari',
gajiPokok: 8000000,
tunjanganJabatan: 3500000,
);
print(staf);
print(manajer);
}
Pembahasan: Dengan memanggil super.hitungGaji() di dalam Manajer.hitungGaji(), kita tidak menduplikasi logika perhitungan dasar dari Pegawai. Apabila suatu saat rumus gaji pokok di kelas Pegawai berubah (misalnya dikurangi pajak dasar), kelas Manajer otomatis mengikuti perhitungan baru tersebut.
Latihan 3
class VoucherDiskon {
final String kodeVoucher;
final int nominalPotongan;
VoucherDiskon({
required this.kodeVoucher,
required this.nominalPotongan,
});
String get _kodeNormal => kodeVoucher.trim().toUpperCase();
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is VoucherDiskon && other._kodeNormal == _kodeNormal;
}
@override
int get hashCode => _kodeNormal.hashCode;
@override
String toString() => 'Voucher($_kodeNormal: Rp$nominalPotongan)';
}
void main() {
final v1 = VoucherDiskon(kodeVoucher: 'hemat50', nominalPotongan: 50000);
final v2 = VoucherDiskon(kodeVoucher: 'HEMAT50', nominalPotongan: 50000);
print('Apakah v1 == v2? ${v1 == v2}');
final dompetVoucher = <VoucherDiskon>{v1, v2};
print('Isi dompet voucher: $dompetVoucher');
}
Pembahasan: Kita membuat private getter _kodeNormal yang menormalisasi teks kode menjadi huruf kapital. Baik operator == maupun hashCode sama-sama bergantung pada _kodeNormal sehingga kontrak kesamaan objek terjaga: dua objek yang bernilai true saat dibandingkan dengan == pasti menghasilkan hashCode yang identik.
Diskusi (0)
Kami menyimpan nama (opsional), komentar, dan hash teknis (bukan alamat IP asli) untuk mencegah spam. Cookie fungsional dipakai agar Anda dapat mengedit atau menghapus komentar sendiri dari browser ini. Untuk permintaan penghapusan, gunakan halaman kontak.
Belum ada komentar. Mulai diskusinya.