Class pembawa data (data carrier) adalah class yang hidupnya untuk menyimpan sekumpulan nilai: buku dengan judul-penulis-tahun, titik dengan koordinat x-y. Menulis class seperti itu secara klasik menuntut ritual panjang — constructor, getter, equals, hashCode, toString, dan final pada setiap field — sering lebih dari 50 baris untuk logika yang sederhana. Java 16 (JEP 395) memperkenalkan record: satu baris deklarasi yang menghasilkan seluruh ritual tersebut secara otomatis dan benar. Pada Chapter ini kita pelajari anatomi record, compact constructor untuk validasi, bukti bahwa equals otomatis bekerja, dan kapan record justru bukan pilihan yang tepat.

Anatomi Sebuah Record #

Deklarasi record dimulai dari header berisi nama dan komponen — daftar nama dan tipe data yang dibawa. Dari header tersebut, compiler javac menyusun class final dengan field private final untuk tiap komponen, constructor yang mencocokkan komponen, accessor bernama sama dengan komponen (tanpa awalan get), serta equals, hashCode, dan toString yang dihitung dari seluruh komponen. Dengan kata lain, record Buku(String judul, String penulis, int tahunTerbit) adalah ringkasan dari puluhan baris class manual yang setara.

Karena semua field final dan tidak ada setter, record bersifat immutable — turunan langsung dari resep Chapter Immutability. Record juga otomatis final, sehingga tidak bisa diwarisi; dan komponen tidak bisa ditambah subclass, sehingga hierarki pembawa data tidak mungkin terbentuk dari record.

Deklarasi dengan Compact Constructor #

Kode berikut mendefinisikan dua record sekaligus. Buku memakai compact constructor — bentuk constructor tanpa daftar parameter — untuk memvalidasi tahun terbit dan merapikan judul sebelum nilai disimpan. Titik memperlihatkan bahwa record tetap boleh memiliki method tambahan:

java RekapRecord.java
// Record: satu baris deklarasi untuk class pembawa data.
record Buku(String judul, String penulis, int tahunTerbit) {

    // Compact constructor: validasi tanpa menulis parameter dan field manual.
    Buku {
        if (tahunTerbit <= 0) {
            throw new IllegalArgumentException(
                    "tahunTerbit harus positif: " + tahunTerbit);
        }
        judul = judul.trim(); // parameter boleh ditulis ulang sebelum disimpan
    }
}

record Titik(int x, int y) {
    // Body tambahan: method biasa tetap boleh ada di dalam record.
    double jarakKeTitikNol() {
        return Math.sqrt(x * x + y * y);
    }
}

public class RekapRecord {
    public static void main(String[] args) {
        Buku a = new Buku("Java Dasar", "Budi Santoso", 2024);
        Buku b = new Buku("Java Dasar", "Budi Santoso", 2024);
        Buku c = new Buku("Java Lanjutan", "Siti Rahayu", 2025);

        // equals dan hashCode dihasilkan otomatis dari seluruh komponen.
        System.out.println("a.equals(b) : " + a.equals(b));
        System.out.println("a.equals(c) : " + a.equals(c));

        // toString otomatis: format NamaRecord[komponen=nilai, ...].
        System.out.println("a           : " + a);

        // Accessor memakai nama komponen, tanpa awalan "get".
        System.out.println("judul a     : " + a.judul());

        // Validasi compact constructor benar-benar aktif.
        try {
            new Buku("Buku Lama", "Anonim", -1);
        } catch (IllegalArgumentException e) {
            System.out.println("validasi aktif : " + e.getMessage());
        }

        Titik t = new Titik(3, 4);
        System.out.println("titik       : " + t);
        System.out.printf("jarak ke (0,0): %.2f%n", t.jarakKeTitikNol());
    }
}

Output yang Dihasilkan:

text
a.equals(b) : true
a.equals(c) : false
a           : Buku[judul=Java Dasar, penulis=Budi Santoso, tahunTerbit=2024]
judul a     : Java Dasar
validasi aktif : tahunTerbit harus positif: -1
titik       : Titik[x=3, y=4]
jarak ke (0,0): 5.00

Mari kita bedah potongan-potongan pentingnya:

  1. record Buku(String judul, String penulis, int tahunTerbit): tiga komponen ini menjadi tiga field private final, constructor tiga argumen, dan tiga accessor judul(), penulis(), tahunTerbit().
  2. Buku { ... }: compact constructor menuliskan hanya nama record tanpa parameter — parameter dan penugasan field disusun compiler di sekeliling body kita. Kode validasi berjalan setiap kali objek dibuat, terbukti dari IllegalArgumentException pada tahun -1.
  3. judul = judul.trim();: penulisan ulang parameter di compact constructor disalin ke field. Perubahan seperti normalisasi data masuk cocok ditempatkan di sini.
  4. a.equals(b) : true dan a.equals(c) : false: equals otomatis membandingkan seluruh komponen — dua record dengan nilai sama dianggap setara meski objeknya berbeda. hashCode otomatis konsisten dengannya, sehingga record aman langsung dipakai sebagai key HashMap.
  5. a : Buku[judul=Java Dasar, ...]: format toString bawaan mencantumkan setiap komponen — sangat membantu saat men-debug.
  6. t.jarakKeTitikNol(): method tambahan pada Titik menghitung akar 3*3 + 4*4 menjadi 5.00.

Record Bersifat Final #

Sifat final record dijaga compiler. Upaya mewarisi record langsung ditolak saat kompilasi:

// Record bersifat final: tidak bisa diwarisi class lain.
record Titik(int x, int y) {
}

class Segmen extends Titik {
    Segmen(int x, int y) {
        super(x, y);
    }
}

public class PewarisRecord {
    public static void main(String[] args) {
        System.out.println("baris ini tidak pernah tercetak");
    }
}

Compiler javac mengeluarkan pesan yang lugas:

text
error: cannot inherit from final Titik

Pesan ini sama dengan yang muncul ketika mewarisi class final biasa — record memang diperlakukan sebagai class final dengan kewajiban tambahan berupa komponen.

Kapan Tidak Memakai Record #

Record bukan pengganti semua class. Karena komponennya beku, record tidak cocok ketika objek perlu berubah sepanjang hidupnya — keranjang belanja yang diisi bertahap, rekening yang saldonya berubah, atau builder yang menyusun objek langkah demi langkah:

java Contoh class mutable yang tidak cocok menjadi record
class KeranjangBelanja {
    private final List<String> item = new ArrayList<>();

    public void tambah(String barang) {
        item.add(barang);
    }

    public int jumlah() {
        return item.size();
    }
}

Record juga tidak cocok ketika Anda membutuhkan pewarisan field: superclass umum dengan field bersama tidak bisa dibuat dari record, karena record tidak bisa diwarisi maupun mewarisi class biasa. Gunakan class atau abstract class untuk hierarki dengan state bersama, dan gunakan record untuk tipe nilai yang berdiri sendiri.

Record memangkas ceremony class data menjadi satu baris, tetapi hierarki antar record tetap terbuka lebar: siapa pun bisa mengimplementasikan interface yang kita definisikan. Bagaimana jika kita justru ingin menutup hierarki — membatasi tipe yang boleh mengimplementasikan sebuah kontrak? Chapter Sealed Classes membahas permits, kombinasi record dan pattern matching yang saling melengkapi.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

PewarisRecord.java

Unduh berkas
java
// Record bersifat final: tidak bisa diwarisi class lain.
record Titik(int x, int y) {
}

class Segmen extends Titik {
    Segmen(int x, int y) {
        super(x, y);
    }
}

public class PewarisRecord {
    public static void main(String[] args) {
        System.out.println("baris ini tidak pernah tercetak");
    }
}

RekapRecord.java

Unduh berkas
java
// Record: satu baris deklarasi untuk class pembawa data.
record Buku(String judul, String penulis, int tahunTerbit) {

    // Compact constructor: validasi tanpa menulis parameter dan field manual.
    Buku {
        if (tahunTerbit <= 0) {
            throw new IllegalArgumentException(
                    "tahunTerbit harus positif: " + tahunTerbit);
        }
        judul = judul.trim(); // parameter boleh ditulis ulang sebelum disimpan
    }
}

record Titik(int x, int y) {
    // Body tambahan: method biasa tetap boleh ada di dalam record.
    double jarakKeTitikNol() {
        return Math.sqrt(x * x + y * y);
    }
}

public class RekapRecord {
    public static void main(String[] args) {
        Buku a = new Buku("Java Dasar", "Budi Santoso", 2024);
        Buku b = new Buku("Java Dasar", "Budi Santoso", 2024);
        Buku c = new Buku("Java Lanjutan", "Siti Rahayu", 2025);

        // equals dan hashCode dihasilkan otomatis dari seluruh komponen.
        System.out.println("a.equals(b) : " + a.equals(b));
        System.out.println("a.equals(c) : " + a.equals(c));

        // toString otomatis: format NamaRecord[komponen=nilai, ...].
        System.out.println("a           : " + a);

        // Accessor memakai nama komponen, tanpa awalan "get".
        System.out.println("judul a     : " + a.judul());

        // Validasi compact constructor benar-benar aktif.
        try {
            new Buku("Buku Lama", "Anonim", -1);
        } catch (IllegalArgumentException e) {
            System.out.println("validasi aktif : " + e.getMessage());
        }

        Titik t = new Titik(3, 4);
        System.out.println("titik       : " + t);
        System.out.printf("jarak ke (0,0): %.2f%n", t.jarakKeTitikNol());
    }
}