Kapan Sebaiknya Pakai Record, Class, atau Lombok di Java
Pilih record kalau objeknya hanya membawa data dan tidak akan berubah setelah dibuat. Pilih class biasa kalau ada validasi, warisan, atau perilaku yang perlu dikendalikan sendiri. Lombok masuk akal ketika kamu sudah terlanjur memakai class dan ingin memotong kode boilerplate tanpa mengubah struktur desain. Sisanya soal detail yang sering baru terasa saat kode mulai tumbuh.
Batas mendasar dari record di Java
Record menghasilkan kelas final dengan field final, aksesor bernama sama dengan komponennya, serta implementasi equals, hashCode, dan toString yang diturunkan dari semua komponen. Karena final, record tidak bisa diwarisi. Karena field-nya final, isinya tidak bisa diganti setelah konstruktor selesai.
Yang sering terlewat: record tetap bisa punya konstruktor kanonik yang dipadatkan. Di situ kamu boleh memvalidasi argumen, menyalin koleksi agar tidak berbagi referensi dengan pemanggil, atau menormalkan nilai.
public record Rentang(int awal, int akhir) {
public Rentang {
if (awal > akhir) {
throw new IllegalArgumentException("awal melebihi akhir");
}
}
public int panjang() {
return akhir - awal;
}
}
Kode di atas menunjukkan bahwa record bisa memegang invarian. Yang tidak bisa dilakukan record adalah mengubah field setelah objek jadi, dan itu yang membuatnya aman dipakai sebagai kunci map atau elemen set.
Kapan memakai record atau class di Java untuk DTO dan nilai
Record paling berguna pada lapisan perpindahan data: parameter query, hasil pemetaan baris database, nilai konfigurasi yang dibaca sekali, atau pasangan kunci-nilai yang dipakai sebagai identitas di koleksi. Semua kasus itu berbagi sifat: objek dibuat lengkap, tidak diubah, lalu dibaca.
Ada satu jebakan yang layak diwaspadai. Record yang memuat List atau Map tidak otomatis menjadi immutable, karena isi koleksi aslinya masih bisa diubah dari luar. Kalau objek itu akan disimpan lama atau dilempar antar-thread, salin koleksinya di konstruktor.
public record Pesanan(String id, List<Item> item) {
public Pesanan {
id = Objects.requireNonNull(id);
item = List.copyOf(item);
}
}
Sebaliknya, class biasa lebih cocok untuk entitas yang punya siklus hidup: objek yang dibuat kosong lalu diisi bertahap oleh lapisan ORM, atau objek yang statusnya berubah mengikuti alur bisnis. Di sini mutabilitas bukan cacat, melainkan bagian dari cara kerja kerangka kerja yang kamu pakai.
Lombok: apa yang dihemat dan apa yang disembunyikan
Lombok menghasilkan kode yang sama seperti yang kamu tulis tangan: getter, setter, konstruktor, equals, dan hashCode. Bedanya, kode itu tidak muncul di berkas sumber sehingga pembaca harus mencari anotasi untuk mengetahui perilaku kelas.
Untuk kelas data yang pendek dan stabil, penghematannya nyata. Untuk kelas domain yang punya banyak aturan, anotasi seperti @Data justru berbahaya karena sekaligus membuat setter untuk semua field dan menghasilkan equals berbasis seluruh field, termasuk field yang seharusnya tidak ikut menentukan identitas.
| Aspek | Record | Class biasa | Class + Lombok |
|---|---|---|---|
| Mutabilitas | Tidak bisa diubah | Bebas diatur | Bebas diatur |
| Warisan | Tidak bisa diwarisi | Bisa | Bisa |
| Validasi di konstruktor | Bisa, lewat konstruktor padat | Bisa | Bisa |
| Kode boilerplate | Dihasilkan compiler | Ditulis tangan | Dihasilkan anotasi |
| Ketergantungan alat | Tidak ada | Tidak ada | Perlu plugin dan processor |
| Cocok untuk entitas ORM | Umumnya tidak | Ya | Ya, dengan hati-hati |
Record dan Jackson, JPA, serta lapisan web
Jackson mendukung record sebagai tipe deserialisasi selama nama komponen bisa dipetakan dari JSON. Ini membuat record nyaman untuk badan permintaan dan respons di lapisan web. Kamu juga bisa menambahkan anotasi validasi pada komponen record, dan validator akan membacanya dari aksesor.
Untuk JPA situasinya berbeda. Spesifikasi JPA mengharuskan entitas berupa kelas dengan konstruktor tanpa argumen dan field yang bisa diakses lewat aksesor atau akses field. Record tidak memenuhi syarat itu sebagai entitas. Kamu masih bisa memakai record untuk hasil proyeksi query, misalnya konstruktor ekspresi di JPQL atau pemetaan manual dari Tuple, dan itu sering menghasilkan kode yang lebih bersih daripada DTO dengan setter.
Di Spring, record langsung dikenali sebagai kandidat injeksi konstruktor. Controller dan service yang seluruh dependensinya tetap bisa ditulis sebagai record tanpa anotasi tambahan, dan itu memaksa semua dependensi diisi pada saat pembuatan objek.
Warisan dan polimorfisme: di mana record berhenti
Kalau hierarki tipe diperlukan, record bukan pilihan. Misalnya kamu punya beberapa jenis pesan yang harus diperlakukan seragam oleh satu pemroses. Di sini class atau interface dengan implementasi class lebih tepat, karena record tidak bisa menjadi superclass.
Ada jalan tengah: interface sebagai tipe kontrak, lalu record sebagai implementasi tiap varian. Pemroses bekerja pada interface, dan tiap varian tetap ringkas karena tidak perlu menulis equals atau hashCode. Pola ini juga membuat pencocokan tipe dengan switch berpola lebih mudah dibaca daripada rangkaian instanceof.
Keputusan praktis di codebase campuran
Pada proyek yang sudah berjalan lama, mengganti semua DTO menjadi record tidak memberi manfaat sepadan dengan risikonya. Seri refaktor berikut lebih masuk akal dijalankan bertahap:
- Tandai kelas yang seluruh field-nya final dan tidak punya setter. Itu kandidat terkuat untuk record.
- Periksa apakah kelas itu diwarisi atau mewarisi. Kalau ya, singkirkan dari daftar.
- Periksa apakah kelas itu dipakai ORM sebagai entitas. Kalau ya, biarkan sebagai class.
- Ganti satu per satu, jalankan pengujian, lalu hapus anotasi Lombok yang tidak lagi terpakai di kelas tersebut.
- Untuk kelas yang tetap butuh setter, ganti
@Datadengan@Getterdan setter eksplisit hanya pada field yang memang boleh berubah.
Satu hal yang sering luput: equals pada record membandingkan semua komponen. Kalau salah satu komponen berupa array, perbandingan memakai identitas referensi array, bukan isinya. Untuk kasus seperti itu, timpa equals dan hashCode secara eksplisit, atau bungkus array dalam tipe yang punya semantik nilai.
Untuk konfigurasi yang dibaca dari berkas, record dengan konstruktor padat memberi keuntungan nyata: nilai yang tidak valid gagal saat aplikasi mulai, bukan saat dipakai berjam-jam kemudian. Kegagalan dini seperti ini lebih mudah dilacak daripada NullPointerException yang muncul di tengah alur permintaan.
Ringkasan
Record untuk data yang tidak berubah dan tidak berwarisi, class biasa untuk entitas yang punya siklus hidup atau hierarki, dan Lombok untuk memangkas boilerplate pada kelas yang strukturnya sudah tepat. Ketiganya bisa hidup berdampingan dalam satu proyek asalkan batasnya jelas dan setiap pilihan punya alasan yang bisa dijelaskan ke rekan satu tim.
Pertanyaan yang Sering Diajukan
Apakah record bisa dipakai sebagai entitas JPA?
Tidak. JPA membutuhkan kelas dengan konstruktor tanpa argumen dan field yang bisa diakses kerangka kerja, sementara record bersifat final dan komponennya tidak bisa diubah. Pakai record untuk hasil proyeksi query, bukan untuk entitas yang dipetakan ke tabel.
Apakah record lebih cepat daripada class biasa?
Dari sisi eksekusi, perbedaannya kecil dan biasanya tidak menentukan. Keuntungan utamanya ada pada kejelasan maksud dan keamanan saat objek dipakai bersama, bukan pada kecepatan.
Apakah Lombok masih perlu kalau sudah ada record?
Masih, untuk kelas yang butuh mutabilitas, warisan, atau anotasi kerangka kerja. Yang perlu dihindari adalah memakai @Data secara borongan pada kelas domain yang punya aturan identitas dan validasi sendiri.
