Objek, pewarisan, dan cara kegagalan menjalar
Kapan abstract class layak dipakai, kenapa komposisi biasanya menang, apa arti static sebenarnya, dan bedanya exception yang bisa ditindaklanjuti pemanggil dan yang tidak.
Interface atau abstract class
Interface mendeklarasikan kemampuan; abstract class membagi implementasi. Ambil abstract class hanya kalau ada state nyata atau algoritma nyata untuk dibagi — template method dengan hook, base yang memegang field yang dibutuhkan semua subclass. Selain itu interface, karena sebuah class bisa meng-implement banyak dan meng-extend tepat satu, dan slot satu itu layak dibiarkan kosong.
Default method mengaburkan ini, dan patokannya tetap: kalau kamu sedang menulis abstract class tanpa field dan tanpa konstruktor, yang kamu tulis itu interface.
Komposisi biasanya menang
Pewarisan mengikatmu ke implementasi superclass, bukan cuma kontraknya. Ubah
si induk dan semua subclass berisiko — masalah fragile base class, dan alasan
ArrayList bukan hal yang bagus untuk di-extend.
class AuditedRepo implements Repo { // komposisi private final Repo delegate; public void save(Order o) { audit(o); delegate.save(o); }}Class itu bisa membungkus Repo apa pun, bisa dites dengan fake, dan tidak bisa
dirusak oleh perubahan di dalam delegate. Versi pewarisannya — class AuditedRepo extends JdbcRepo — bisa dirusak perubahan apa pun di JdbcRepo,
termasuk yang cuma menambah pemanggilan internal antara dua method yang kamu
override.
Overloading bukan overriding
- Overloading: nama sama, tipe parameter beda, diselesaikan saat compile dari tipe statisnya. Dua method, tidak berhubungan saat runtime.
- Overriding: signature sama di subtipe, diselesaikan saat runtime dari objek sebenarnya. Satu method, polimorfik.
Konsekuensi yang bikin orang tersandung: variabel List<String> yang memegang
ArrayList memanggil overload yang dipilih untuk List, dan override
yang didefinisikan ArrayList. Tipe menentukan overload; objek menentukan
override.
static milik class
Anggota static ada satu kali, di class-nya, dan bisa dijangkau tanpa instance — jadi dia tidak bisa melihat field instance dan tidak bisa di-override (dia bisa disembunyikan, yang terlihat seperti override padahal bukan). State static yang mutable dibagi oleh setiap thread di proses itu, dan itu membuatnya cara paling sunyi untuk memasukkan race condition.
Dan aturan konstruktor yang sering mengecoh: class tanpa konstruktor
mendapat satu konstruktor tanpa argumen secara implisit. Deklarasikan konstruktor
apa pun, dan yang gratis itu hilang — itulah kenapa menambah konstruktor
ber-argumen bisa merusak kode framework yang memanggil new Thing() secara
reflektif.
Checked atau unchecked itu pertanyaan tentang si pemanggil
Beda mekanisnya: checked exception wajib dideklarasikan atau ditangkap. Pertanyaan desainnya lebih baik: apakah pemanggil bisa melakukan sesuatu yang berguna soal ini?
| Situasi | Jenis |
|---|---|
| Input user yang tidak valid | checked, atau hasil validasi |
| Panggilan remote timeout dan retry mungkin menolong | checked |
| Nilai config wajib tidak ada saat boot | unchecked — gagal cepat |
| Bug: null di tempat yang dijamin non-null | unchecked |
Kalau pemanggil tidak punya respons yang berarti, checked exception hanya memaksa
catch (Exception e) { throw new RuntimeException(e); } di setiap lapisan — tidak
kehilangan apa pun, cuma menambah kebisingan. Membungkus itu wajar — selama
cause-nya dibawa, karena stack trace tanpa penyebab aslinya adalah satu sore
yang hilang.
finally selalu jalan, dan bisa memakan return-mu
int f() { try { return 1; } finally { System.out.println("selalu"); } // dicetak, lalu return 1}finally jalan saat return normal, saat exception, dan saat break/continue.
Dia tidak jalan kalau JVM-nya keluar. Dan return di dalam finally
menggantikan nilai atau exception dari try — menelan kegagalan aslinya dalam
diam. Jangan pernah return dari finally.
Try-with-resources adalah alat yang lebih baik, dan menutup dalam urutan terbalik dari deklarasinya — itu penting kalau resource kedua bergantung pada yang pertama:
try (Connection c = open(); Statement s = c.createStatement()) { ...} // s ditutup dulu, lalu cNullPointerException itu fakta, bukan misteri
Artinya sebuah anggota diakses pada null. Di JVM modern pesannya menyebut
ekspresinya — cukup membantu sehingga mematikan helpful NPE message jarang
sepadan. Perbaikannya hampir selalu bukan null check di titik lemparnya; tapi
menemukan boundary mana yang membiarkan null itu masuk, lalu menghentikannya
di sana.
Lebih jauh soal penanganan error di boundary API: Bertahan di Produksi dengan Java.
Diskusi
Memuat komentar…