Pendalaman Record

Melengkapi record dengan konstruktor compact untuk validasi dan memakai record dengan generics.

Daftar isi

Topik Record pada modul OOP I mempraktikkan deklarasi record beserta konstruktor compact untuk validasi sederhana. Topik ini menuntaskan sisanya, yaitu konstruksi yang lebih lengkap melalui konstruktor tambahan dan pabrik statis, metode turunan yang dihitung dari komponen, serta record yang membawa parameter tipe (generics) sebagaimana kelas generic biasa. Pembahasan ditutup dengan batas-batas yang tetap berlaku dan satu pola penting untuk komponen bertipe koleksi. Setelah selesai, record dapat dirancang sebagai pembawa data yang lengkap, bukan sekadar tumpukan field yang dibungkus singkat.

Konstruktor tambahan dan pabrik statis#

Record secara otomatis memiliki konstruktor kanonik yang parameternya persis daftar komponennya. Konstruktor compact, yang ditulis tanpa daftar parameter, berjalan di tengah konstruksi kanonik dan menjadi tempat pemeriksaan serta penormalan nilai. Selain keduanya, record boleh memiliki konstruktor tambahan sebanyak yang dibutuhkan, dengan satu syarat tegas, yaitu setiap konstruktor tambahan wajib mendelegasikan pekerjaannya ke konstruktor lain melalui this(...), dan rantai itu selalu berujung pada konstruktor kanonik.

java UjiRentang.java
public class UjiRentang {

    public static void main(String[] args) {
        Rentang tugas = Rentang.antara(60, 90);
        System.out.println("Tugas : " + tugas);
        System.out.println("Apakah 75 di dalam? " + tugas.mengandung(75));
        System.out.println("Lebar : " + tugas.lebar());

        Rentang seragam = new Rentang(10);
        System.out.println("Seragam : " + seragam);

        try {
            new Rentang(80, 50);
        } catch (IllegalArgumentException e) {
            System.out.println("Tertangkap: " + e.getMessage());
        }
    }
}

record Rentang(int min, int max) {

    Rentang {
        if (min > max) {
            throw new IllegalArgumentException("min " + min + " melebihi max " + max);
        }
    }

    Rentang(int lebar) {
        this(0, lebar);
    }

    static Rentang antara(int min, int max) {
        return new Rentang(min, max);
    }

    boolean mengandung(int nilai) {
        return nilai >= min && nilai <= max;
    }

    int lebar() {
        return max - min;
    }
}
text Keluaran
Tugas : Rentang[min=60, max=90]
Apakah 75 di dalam? true
Lebar : 30
Seragam : Rentang[min=0, max=10]
Tertangkap: min 80 melebihi max 50

Tiga pintu konstruksi sama-sama melewati pemeriksaan compact, sehingga new Rentang(80, 50) ditolak apa pun pintunya. Konstruktor tambahan Rentang(int lebar) mengisi batas bawah dengan 0 lalu menyerahkan sisanya ke kanonik, sedangkan pabrik statis antara hanya menyediakan nama yang lebih terbaca daripada new Rentang(60, 90). Pabrik semacam ini menjadi kebiasaan yang sehat ketika pembuatan record memiliki beberapa cara yang masing-masing membutuhkan penamaan sendiri.

Metode turunan dan perbandingan bawaan#

Tubuh record boleh memuat metode yang menghitung sesuatu dari komponennya, dan nilai hasil hitungan itu tidak disimpan, melainkan dihitung ulang setiap kali dipanggil. Metode mengandung dan lebar pada contoh sebelumnya bekerja dengan cara ini. Selain metode objek, record juga boleh memuat anggota static, misalnya konstanta atau pabrik, sehingga seluruh pengetahuan tentang data itu tinggal di satu tempat.

Perbandingan bawaan membuat record nyaman dipakai di dalam collections. equals dan hashCode dihitung dari komponen, sehingga dua record yang dibuat terpisah dengan isi sama dianggap sama tanpa satu baris perbandingan yang ditulis manual.

java UjiRecordDiList.java
import java.util.ArrayList;

public class UjiRecordDiList {

    public static void main(String[] args) {
        var antrean = new ArrayList<Pesanan>();
        antrean.add(new Pesanan("Nasi goreng", 2));
        antrean.add(new Pesanan("Es teh", 3));

        Pesanan dicari = new Pesanan("Es teh", 3);
        System.out.println("Posisi : " + antrean.indexOf(dicari));
        System.out.println("Banyak : " + antrean.size());
        System.out.println(antrean);
    }
}

record Pesanan(String menu, int jumlah) {
}
text Keluaran
Posisi : 1
Banyak : 2
[Pesanan[menu=Nasi goreng, jumlah=2], Pesanan[menu=Es teh, jumlah=3]]

Objek dicari tidak pernah dimasukkan ke antrean, tetapi indexOf menemukannya pada posisi 1 karena perbandingan dilakukan atas komponen, bukan atas referensi. Pada kelas biasa, perilaku ini hanya muncul setelah equals dan hashCode ditulis dan dijaga, dan kelupaan yang paling murah ditemukan justru di sini. Record memindahkan pekerjaan itu ke kompiler, sehingga pembawa data seperti Pesanan aman langsung dipakai di List, Set, maupun Map.

Record dengan parameter tipe#

Record dapat dinyatakan dengan parameter tipe sebagaimana kelas generic, dan parameternya ditulis setelah nama record sebelum daftar komponen. Komponen yang bertipe parameter menghasilkan aksesor dengan tipe yang mengikutinya, sehingga nilai yang keluar dari record sudah benar tanpa cast. Pola ini menyambung langsung dengan modul Generics, termasuk batasan tipe pada parameter tersebut.

java UjiKotakGeneric.java
import java.util.List;

public class UjiKotakGeneric {

    public static void main(String[] args) {
        Kotak<String> salam = new Kotak<>("Halo");
        Kotak<Integer> nomor = new Kotak<>(42);
        System.out.println("salam : " + salam.isi() + " ("
                + salam.isi().getClass().getSimpleName() + ")");
        System.out.println("nomor : " + nomor.isi());

        Skala<Double> suhu = new Skala<>(-10.0, 40.0);
        System.out.println("suhu  : " + suhu);

        Kotak<List<String>> arsip = new Kotak<>(List.of("a.txt", "b.txt"));
        System.out.println("arsip : " + arsip.isi().size() + " berkas");
    }
}

record Kotak<T>(T isi) {
}

record Skala<T extends Number>(T minimum, T maksimum) {
}
text Keluaran
salam : Halo (String)
nomor : 42
suhu  : Skala[minimum=-10.0, maksimum=40.0]
arsip : 2 berkas

Record Kotak<T> dipakai untuk tipe apa pun, bahkan untuk tipe generik lain seperti List<String> sebagai komponennya. Record Skala<T extends Number> menunjukkan batasan tipe, yaitu T hanya menerima angka, sehingga kompiler menolak new Skala<>("dingin", "panas") sejak kompilasi. Seperti generics pada umumnya, parameter tipe hanya hidup saat kompilasi dan dihapus setelahnya, sebagaimana dijelaskan pada topik Type Erasure.

Batas-batas yang tetap berlaku#

Kemampuan tambahan di atas tidak mengubah janji struktural record. Record tetap final sehingga tidak dapat diperluas, fieldnya tetap terbatas pada komponen, dan keimutabelannya tetap dangkal terhadap komponen yang bisa berubah. Poin terakhir ini meminta satu pola tambahan ketika komponennya bertipe koleksi, yaitu menormalkan komponen menjadi salinan tak berubah di dalam konstruktor compact.

java Contoh: salinan tak berubah untuk komponen koleksi
// tidak-dijalankan
record Arsip(List<String> berkas) {
    Arsip {
        berkas = List.copyOf(berkas);
    }
}

Tanpa penormalan itu, pemanggil new Arsip(daftarMutable) masih memegang referensi yang sama dan dapat mengubah isi arsip dari luar, sehingga janji keimutabelannya bocor. Dengan List.copyOf, record menyimpan salinan yang tidak menyediakan operasi pengubahan apa pun, dan perubahan pada daftar aslinya tidak lagi terasa di dalam record. Pola yang sama berlaku untuk array dengan penyalinan eksplisit dan untuk kelas yang bisa berubah dengan pembungkus tak berubahnya.

Kesalahan umum#

  • Menulis konstruktor tambahan yang tidak memanggil this(...). Kompilator menolaknya karena satu-satunya jalan masuk nilai komponen adalah konstruktor kanonik. Rantaikan konstruktor tambahan ke kanonik sebagaimana contoh Rentang.
  • Mengira konstruktor compact menambahkan konstruksi baru. Bentuk Suhu { ... } adalah tubuh yang menyatu dengan konstruktor kanonik, bukan konstruktor kedua, sehingga tidak muncul dua kali dalam daftar konstruktor.
  • Menyimpan nilai turunan ke dalam komponen, misalnya menyimpan lebar sebagai komponen ketiga. Nilai yang bisa dihitung dari komponen lain sebaiknya menjadi metode, karena komponen yang redundan dapat saling bertentangan.
  • Menaruh koleksi yang bisa berubah sebagai komponen tanpa List.copyOf. Keimutabelannya dangkal, dan isi record tetap berubah lewat referensi aslinya.
  • Mengharapkan parameter tipe tersedia saat program berjalan. Record generic mengikuti aturan type erasure yang sama dengan kelas generic, sehingga pemeriksaan tipe parameternya tidak dapat dilakukan lewat objeknya.

Ringkasan#

  • Konstruktor tambahan boleh ditambahkan di samping kanonik dengan syarat mendelegasikan melalui this(...), dan pabrik statis memberi nama yang lebih terbaca untuk cara-cara pembuatannya.
  • Metode di dalam tubuh record menghitung nilai dari komponen tanpa menyimpannya, sedangkan equals dan hashCode bawaan menjadikan record nyaman dipakai di collections.
  • Record menerima parameter tipe dan batasan tipe seperti kelas generic, sehingga record Kotak<T>(T isi) dan record Skala<T extends Number>(...) sah dan aksesor mengembalikan tipe yang benar.
  • Keimutabelan komponen koleksi perlu ditegakkan sendiri dengan List.copyOf di konstruktor compact, karena record hanya membekukan referensinya, bukan isinya.

Referensi#