Tampilan
Kenapa penting
Sebagian besar kode backend memindahkan data: request JSON masuk, diubah menjadi object, divalidasi, disimpan, lalu dikirim balik sebagai response. Dulu setiap bentuk data butuh class dengan constructor, getter, equals, hashCode, dan toString yang ditulis manual — puluhan baris yang mudah salah. Record memangkas semua itu menjadi satu baris. Di sisi lain, banyak nilai di bisnis hanya boleh berasal dari daftar tetap: status pesanan, metode pembayaran, zona pengiriman. Enum membuat daftar itu aman: compiler menolak nilai yang tidak ada, dan aturan bisnis bisa ditaruh langsung di dalamnya.
Konsep inti
Record: class untuk data
Record adalah class khusus yang dideklarasikan cukup dengan daftar komponen. Compiler membuatkan field private final, constructor, accessor, serta equals, hashCode, dan toString yang berbasis isi.
java
record ProductDto(long id, String name, long price) {}
public class RecordBasics {
public static void main(String[] args) {
ProductDto kopi = new ProductDto(1L, "Kopi Gayo", 85_000);
ProductDto sama = new ProductDto(1L, "Kopi Gayo", 85_000);
System.out.println(kopi.name()); // accessor: name(), bukan getName()
System.out.println(kopi.price());
System.out.println(kopi); // toString() otomatis
System.out.println(kopi.equals(sama)); // equals() otomatis membandingkan isi
System.out.println(kopi == sama); // tetap dua object berbeda
System.out.println(kopi.hashCode() == sama.hashCode());
}
}Output:
text
Kopi Gayo
85000
ProductDto[id=1, name=Kopi Gayo, price=85000]
true
false
truePerhatikan bahwa accessor record memakai nama komponen persis (name()), bukan getName(). Record juga tidak punya setter: semua field final. Mencoba mengubahnya, misalnya price = price - 1000; di dalam method record, ditolak compiler dengan pesan cannot assign a value to final variable price. Untuk "mengubah" record, buat object baru dengan nilai yang berbeda.
Record secara implisit final (tidak bisa di-extend) dan tidak boleh punya field instance tambahan di luar komponennya. Record tetap boleh mengimplementasikan interface, punya method biasa, dan punya method static.
Compact constructor: validasi dan normalisasi
Data yang masuk ke sistem perlu diperiksa. Record punya bentuk constructor khusus — compact constructor — yang tidak menulis daftar parameter. Di dalamnya kamu bisa memvalidasi atau menormalisasi parameter sebelum disimpan ke field.
java
record EmailAddress(String value) {
EmailAddress { // compact constructor
value = value.strip().toLowerCase(); // normalisasi sebelum disimpan
if (!value.contains("@")) {
throw new IllegalArgumentException("Email tidak valid: " + value);
}
}
static EmailAddress of(String raw) { // static factory
return new EmailAddress(raw);
}
String domain() { // method tambahan
return value.substring(value.indexOf('@') + 1);
}
}
public class EmailDemo {
public static void main(String[] args) {
EmailAddress email = EmailAddress.of(" Budi@Contoh.COM ");
System.out.println(email.value());
System.out.println(email.domain());
System.out.println(email.equals(EmailAddress.of("budi@contoh.com")));
EmailAddress.of("bukan-email"); // berhenti dengan IllegalArgumentException
}
}Tiga baris pertama mencetak budi@contoh.com, contoh.com, dan true. Baris terakhir menghentikan program dengan IllegalArgumentException: Email tidak valid: bukan-email — cara menangani exception dibahas di M03. Polanya penting: object yang tidak valid tidak pernah bisa dibuat, jadi kode lain yang menerima EmailAddress tidak perlu memeriksa ulang. Record kecil seperti ini disebut value object.
Record hanya immutable secara dangkal
Field record final, tetapi final hanya mengunci referensi, bukan isi object yang ditunjuk. Bila komponennya berupa array (atau object lain yang bisa diubah), isinya tetap bisa diubah dari luar.
java
import java.util.Arrays;
record Tags(String[] values) {}
record SafeTags(String[] values) {
SafeTags {
values = values.clone(); // salinan defensif saat dibuat
}
@Override
public String[] values() {
return values.clone(); // salinan defensif saat dibaca
}
}
public class ShallowCopy {
public static void main(String[] args) {
String[] raw = {"promo", "baru"};
Tags tags = new Tags(raw);
raw[0] = "DIUBAH";
System.out.println(Arrays.toString(tags.values()));
String[] raw2 = {"promo", "baru"};
SafeTags safe = new SafeTags(raw2);
raw2[0] = "DIUBAH";
safe.values()[1] = "DIUBAH JUGA";
System.out.println(Arrays.toString(safe.values()));
}
}Output: [DIUBAH, baru] lalu [promo, baru]. Tags ikut berubah karena menyimpan array yang sama dengan raw. SafeTags membuat salinan defensif saat dibuat dan saat dibaca, sehingga benar-benar tidak bisa diubah dari luar. Di M03 kamu akan memakai List.copyOf(...) untuk tujuan yang sama.
Kapan record, kapan class biasa?
| Pakai record bila… | Pakai class biasa bila… |
|---|---|
| object hanya membawa data (DTO request/response, event) | object punya state yang berubah (saldo rekening, keranjang) |
| dua object dengan isi sama dianggap sama | identitas penting walau isinya sama |
| nilainya kecil dan immutable (email, kode SKU, koordinat) | butuh pewarisan, atau field yang tidak menjadi bagian data |
Entity JPA yang dipetakan ke tabel database — dibahas di level Spring Boot — juga memakai class biasa, karena JPA butuh constructor kosong dan field yang bisa diubah.
Enum: himpunan nilai yang tetap
Enum adalah tipe dengan daftar object yang sudah pasti. Setiap konstanta adalah object tunggal, sehingga aman dibandingkan dengan ==.
java
enum PaymentMethod { BANK_TRANSFER, EWALLET, CREDIT_CARD }
public class EnumBasics {
public static void main(String[] args) {
PaymentMethod method = PaymentMethod.EWALLET;
System.out.println(method); // EWALLET
System.out.println(method.name()); // EWALLET
System.out.println(method.ordinal()); // 1 — posisi dalam deklarasi
for (PaymentMethod m : PaymentMethod.values()) {
System.out.print(m + " ");
}
System.out.println();
PaymentMethod fromRequest = PaymentMethod.valueOf("CREDIT_CARD");
System.out.println(fromRequest == PaymentMethod.CREDIT_CARD); // enum aman dibandingkan dengan ==
}
}values()mengembalikan array semua konstanta sesuai urutan deklarasi.valueOf("NAMA")mencari konstanta dari namanya — peka huruf besar/kecil.valueOf("ewallet")menghentikan program denganIllegalArgumentException: No enum constant PaymentMethod.ewallet.name()mengembalikan nama konstanta;ordinal()mengembalikan posisinya (mulai 0).
Dibanding konstanta String seperti "EWALLET", enum mencegah salah ketik sejak kompilasi: PaymentMethod.EWALET langsung ditolak compiler, sedangkan "EWALET" baru ketahuan saat program berjalan — atau tidak pernah.
Enum dengan field, constructor, dan method
Enum bisa punya data dan perilaku sendiri. Constructor enum selalu private dan dipanggil sekali untuk tiap konstanta.
java
enum ShippingZone {
JAWA("Pulau Jawa", 10_000, 2),
SUMATERA("Sumatera", 18_000, 4),
PAPUA("Papua", 45_000, 7);
private final String displayName;
private final long ratePerKg;
private final int etaDays;
ShippingZone(String displayName, long ratePerKg, int etaDays) { // constructor enum selalu private
this.displayName = displayName;
this.ratePerKg = ratePerKg;
this.etaDays = etaDays;
}
long cost(int weightKg) {
return ratePerKg * weightKg;
}
String summary(int weightKg) {
return displayName + ": Rp" + cost(weightKg) + ", " + etaDays + " hari";
}
}
public class ShippingZoneDemo {
static boolean needsInsurance(ShippingZone zone) {
return switch (zone) { // exhaustive: semua konstanta ditangani, tanpa default
case JAWA -> false;
case SUMATERA, PAPUA -> true;
};
}
public static void main(String[] args) {
for (ShippingZone zone : ShippingZone.values()) {
System.out.println(zone.summary(3) + " | asuransi: " + needsInsurance(zone));
}
}
}Output:
text
Pulau Jawa: Rp30000, 2 hari | asuransi: false
Sumatera: Rp54000, 4 hari | asuransi: true
Papua: Rp135000, 7 hari | asuransi: trueSwitch expression atas enum tidak butuh default bila semua konstanta sudah ditangani. Ini justru lebih aman: kalau besok ada zona baru KALIMANTAN, compiler menolak needsInsurance dengan pesan the switch expression does not cover all possible input values — kamu dipaksa memutuskan aturannya, bukan diam-diam jatuh ke default.
Contoh di dunia backend
Endpoint "buat pesanan" menerima request, memvalidasinya, lalu mengembalikan response. Record dan enum cocok untuk keduanya. Saat memakai Spring Boot nanti, JSON dari klien otomatis diubah menjadi record request, dan record response otomatis diubah menjadi JSON.
java
enum PaymentMethod { BANK_TRANSFER, EWALLET, CREDIT_CARD }
// DTO request: data yang dikirim klien (Spring nanti mengisi record ini dari JSON)
record CreateOrderRequest(String customerEmail, String sku, int quantity, String paymentMethod) {
CreateOrderRequest {
if (quantity < 1) {
throw new IllegalArgumentException("quantity minimal 1");
}
customerEmail = customerEmail.strip().toLowerCase();
}
}
// DTO response: data yang dikirim balik ke klien
record OrderResponse(String orderId, String sku, int quantity, long total, PaymentMethod paymentMethod, String status) {}
public class OrderApi {
static OrderResponse createOrder(CreateOrderRequest request, long unitPrice) {
PaymentMethod method = PaymentMethod.valueOf(request.paymentMethod().strip().toUpperCase());
long total = unitPrice * request.quantity();
return new OrderResponse("ORD-1001", request.sku(), request.quantity(), total, method, "NEW");
}
public static void main(String[] args) {
var request = new CreateOrderRequest(" Sari@Contoh.com ", "KOPI-250G", 2, "ewallet");
System.out.println(request);
System.out.println(createOrder(request, 85_000));
}
}Output:
text
CreateOrderRequest[customerEmail=sari@contoh.com, sku=KOPI-250G, quantity=2, paymentMethod=ewallet]
OrderResponse[orderId=ORD-1001, sku=KOPI-250G, quantity=2, total=170000, paymentMethod=EWALLET, status=NEW]Request dan response sengaja dipisah: klien mengirim paymentMethod sebagai teks bebas, sedangkan di dalam sistem nilainya sudah menjadi enum yang pasti valid. Field status di sini masih String — di latihan bab ini kamu membangun enum OrderStatus lengkap dengan aturan transisinya, sehingga status yang mustahil seperti "dari Dikirim kembali ke Baru" tidak bisa terjadi.
Pertanyaan interview
1. Apa itu record dan apa bedanya dengan class biasa?
Record adalah class untuk data immutable: dari daftar komponen, compiler membuat field private final, constructor, accessor, equals, hashCode, dan toString. Record tidak bisa di-extend dan tidak punya setter. Cocok untuk DTO dan value object; class biasa tetap dipakai untuk object dengan state yang berubah atau entity JPA.
2. Apakah record benar-benar immutable?
Hanya secara dangkal. Field-nya final, tetapi bila komponennya object yang bisa diubah (array, list), isinya tetap bisa berubah. Gunakan salinan defensif di compact constructor dan accessor, atau pakai tipe yang immutable.
3. Kenapa tidak boleh menyimpan ordinal() enum ke database?
ordinal() bergantung pada urutan deklarasi. Menyisipkan atau mengurutkan ulang konstanta menggeser angkanya sehingga data lama terbaca sebagai nilai yang salah. Simpan name() (di JPA: @Enumerated(EnumType.STRING)) atau kode eksplisit yang disimpan sebagai field enum.
Jebakan umum
- Menyimpan
ordinal()ke database atau API. Urutan konstanta bisa berubah; simpanname()atau kode eksplisit. valueOfdengan input mentah.valueOfpeka huruf besar/kecil dan berhenti dengan exception untuk nama yang tidak ada. Normalisasi input dulu, dan siapkan penanganan nilai yang tidak dikenal.- Menganggap record immutable sepenuhnya. Array atau object yang bisa diubah di dalam record tetap bisa berubah — buat salinan defensif.
- Menambah
defaultdi switch atas enum "untuk jaga-jaga". Tanpadefault, compiler memberi tahu saat ada konstanta baru yang belum ditangani; dengandefault, kasus baru diam-diam ikut cabang yang mungkin salah. - Memakai record untuk object yang perlu berubah, misalnya keranjang belanja yang item-nya bertambah. Pakai class biasa dengan encapsulation.
- Mencari getter
getName()di record. Accessor record bernamaname().
Ringkasan
recordmembuat class data immutable dalam satu baris: fieldprivate final, constructor, accessor,equals,hashCode, dantoStringotomatis.- Compact constructor memvalidasi dan menormalisasi data, sehingga object yang tidak valid tidak pernah tercipta.
- Record immutable secara dangkal — buat salinan defensif untuk komponen yang bisa diubah.
enummewakili himpunan nilai tetap; konstantanya aman dibandingkan dengan==dan bisa punya field serta method.- Switch expression atas enum tidak butuh
defaultbila semua konstanta ditangani — compiler membantu saat ada konstanta baru. - Jangan simpan
ordinal(); simpanname()atau kode eksplisit.