Model domain dari Bagian 1 belum punya tempat tinggal: objek Buku dan Anggota dibuat, dicetak, lalu terkumpul menjadi sampah memori begitu variabelnya keluar dari cakupan. Perpustakaan sungguhan menyimpan ratusan judul dan ribuan anggota dalam satu tempat yang hidup sepanjang program berjalan. Pada Bagian ini kita bangun tempat itu — class layanan Perpustakaan yang memegang katalog dan daftar anggota, dilengkapi pencarian kata kunci serta kosakata kegagalan berupa exception domain. Seluruh perilaku diuji dengan collection dari JDK: LinkedHashMap untuk data berkunci, List untuk hasil pencarian.

Katalog sebagai Peta Berurutan #

Katalog punya dua kebutuhan yang tampak bertentangan: pencarian ISBN harus cepat, dan daftar buku harus tampil sesuai urutan pendaftaran. LinkedHashMap memenuhi keduanya sekaligus — struktur data tersebut menggabungkan hash table untuk lompatan langsung ke kunci dengan daftar tautan yang mencatat urutan penyisipan. Karena itu katalog.get(isbn) tidak menyisir seluruh isi, sementara iterasi katalog.values() mengeluarkan buku sesuai urutan masuk.

Penolakan data ganda menjadi tanggung jawab method, bukan pemanggil. Method tambahBuku memakai putIfAbsent: method tersebut mengembalikan nilai lama bila kunci sudah ada, dan nilai lama itulah yang menjadi sinyal pelanggaran. Perilaku sama berlaku pada daftarkan untuk nomor anggota ganda — kunci A-NNN menolak pendaftaran kedua dengan biaya satu operasi peta.

Pencarian Kata Kunci tanpa Memandang Huruf Besar #

Pengunjung mengetik “laskar”, “LASKAR”, atau “Laskar” dan berharap menemukan judul yang sama. Record Buku dari Bagian 1 kini memperoleh satu method pencocokan yang menormalkan seluruh teks ke huruf kecil sebelum dibandingkan — memakai Locale.ROOT agar hasilnya tidak bergantung pada locale sistem:

java Buku.java
import java.util.Locale;

// Model katalog: data buku beku (record) + pencocokan kata kunci pencarian.
public record Buku(String isbn, String judul, String penulis, int tahunTerbit) {

    public Buku {
        if (isbn == null || isbn.isBlank()) {
            throw new IllegalArgumentException("isbn wajib diisi");
        }
        if (judul == null || judul.isBlank()) {
            throw new IllegalArgumentException("judul wajib diisi");
        }
        if (tahunTerbit <= 0) {
            throw new IllegalArgumentException(
                    "tahunTerbit harus positif: " + tahunTerbit);
        }
        isbn = isbn.trim();
        judul = judul.trim();
        penulis = (penulis == null || penulis.isBlank()) ? "Tanpa Nama" : penulis.trim();
    }

    /** Apakah judul, penulis, atau ISBN memuat kata kunci (huruf besar diabaikan)? */
    public boolean cocokKataKunci(String kataKunci) {
        String kunci = kataKunci.toLowerCase(Locale.ROOT);
        return judul().toLowerCase(Locale.ROOT).contains(kunci)
                || penulis().toLowerCase(Locale.ROOT).contains(kunci)
                || isbn().contains(kunci);
    }
}

Method cocokKataKunci memeriksa tiga kolom sekaligus: judul, penulis, dan ISBN. Konsekuensi desainnya: pencarian “sapardi” menemukan Hujan Bulan Juni melalui nama penulis, bukan judul. Validasi pada compact constructor tidak berubah dari Bagian 1, dan record Anggota juga dipakai tanpa perubahan — berkas Anggota.java tetap tersedia di bundle bagian ini.

Kosakata Kegagalan Milik Domain #

Pencarian yang gagal membutuhkan lebih dari null: pemanggil perlu tahu apa yang salah agar kegagalan tersebut dapat ditangani secara berbeda. Kita definisikan hierarki exception domain — satu class induk dan dua class anak, semuanya unchecked karena pelanggaran aturan perpustakaan bagian dari alur domain:

// Induk exception domain: seluruh pelanggaran aturan perpustakaan.
public class PerpustakaanException extends RuntimeException {

    public PerpustakaanException(String pesan) {
        super(pesan);
    }
}
// Buku yang dicari tidak ada di katalog.
public class BukuTidakDitemukanException extends PerpustakaanException {

    public BukuTidakDitemukanException(String pesan) {
        super(pesan);
    }
}
// Nomor anggota yang dicari tidak terdaftar.
public class AnggotaTidakDitemukanException extends PerpustakaanException {

    public AnggotaTidakDitemukanException(String pesan) {
        super(pesan);
    }
}

Induk PerpustakaanException dipakai untuk pelanggaran umum seperti data ganda, sedangkan dua anaknya masing-masing menyebut satu makna kegagalan pencarian. Blok catch (BukuTidakDitemukanException e) kini menangkap tepat satu keadaan, dan tanda tangan method sudah bercerita tanpa dokumentasi tambahan.

Layanan Perpustakaan dalam Satu Class #

Seluruh state berkumpul di class Perpustakaan — katalog, daftar anggota, dan seluruh operasinya. Method pengambil seperti semuaBuku() mengembalikan salinan beku lewat List.copyOf, sehingga pemanggil tidak bisa mengubah isi katalog dari luar tanpa melalui tambahBuku:

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

// Layanan katalog dan keanggotaan: satu objek memegang seluruh data perpustakaan.
public class Perpustakaan {

    // LinkedHashMap: pencarian cepat berdasar kunci DAN urutan sisip terjaga,
    // sehingga daftar buku/anggota selalu tampil sesuai urutan pendaftaran.
    private final Map<String, Buku> katalog = new LinkedHashMap<>();
    private final Map<String, Anggota> anggota = new LinkedHashMap<>();

    public void tambahBuku(Buku buku) {
        Buku sebelumnya = katalog.putIfAbsent(buku.isbn(), buku);
        if (sebelumnya != null) {
            throw new PerpustakaanException(
                    "ISBN sudah terdaftar: " + buku.isbn());
        }
    }

    public void daftarkan(Anggota baru) {
        Anggota sebelumnya = anggota.putIfAbsent(baru.id(), baru);
        if (sebelumnya != null) {
            throw new PerpustakaanException(
                    "ID anggota sudah terdaftar: " + baru.id());
        }
    }

    public Buku cariBukuIsbn(String isbn) {
        Buku buku = katalog.get(isbn);
        if (buku == null) {
            throw new BukuTidakDitemukanException(
                    "ISBN tidak ada di katalog: " + isbn);
        }
        return buku;
    }

    public Anggota cariAnggota(String id) {
        Anggota dicari = anggota.get(id);
        if (dicari == null) {
            throw new AnggotaTidakDitemukanException(
                    "ID anggota tidak terdaftar: " + id);
        }
        return dicari;
    }

    /** Cari buku yang judul, penulis, atau ISBN-nya memuat kata kunci. */
    public List<Buku> cariBuku(String kataKunci) {
        List<Buku> hasil = new ArrayList<>();
        for (Buku buku : katalog.values()) {
            if (buku.cocokKataKunci(kataKunci)) {
                hasil.add(buku);
            }
        }
        return hasil;
    }

    /** Salinan katalog dalam urutan pendaftaran. */
    public List<Buku> semuaBuku() {
        return List.copyOf(katalog.values());
    }

    /** Salinan daftar anggota dalam urutan pendaftaran. */
    public List<Anggota> semuaAnggota() {
        return List.copyOf(anggota.values());
    }

    public int jumlahBuku() {
        return katalog.size();
    }

    public int jumlahAnggota() {
        return anggota.size();
    }
}

Menjalankan Katalog dan Keanggotaan #

Demo berikut mengisi lima judul buku Indonesia, mendaftarkan tiga anggota, lalu menguji pencarian, penolakan data ganda, dan dua exception pencarian:

java LayarDemo.java
import java.time.LocalDate;

// Demo katalog dan keanggotaan: pencarian kata kunci, pendaftaran, penjaga data ganda.
public class LayarDemo {

    public static void main(String[] args) {
        Perpustakaan perpus = new Perpustakaan();
        perpus.tambahBuku(new Buku("978-602-421-123-4", "Laskar Pelangi",
                "Andrea Hirata", 2005));
        perpus.tambahBuku(new Buku("978-979-433-555-1", "Bumi Manusia",
                "Pramoedya Ananta Toer", 1980));
        perpus.tambahBuku(new Buku("978-602-032-556-2", "Hujan Bulan Juni",
                "Sapardi Djoko Damono", 1994));
        perpus.tambahBuku(new Buku("978-602-401-718-3", "Filosofi Teras",
                "Henry Manampiring", 2018));
        perpus.tambahBuku(new Buku("978-602-291-024-4", "Laut Bercerita",
                "Leila S. Chudori", 2017));
        System.out.println("jumlah buku    : " + perpus.jumlahBuku());

        // Pencarian mengabaikan huruf besar-kecil.
        for (Buku buku : perpus.cariBuku("LASKAR")) {
            System.out.println("cari LASKAR   : " + buku.judul());
        }
        for (Buku buku : perpus.cariBuku("sapardi")) {
            System.out.println("cari sapardi  : " + buku.judul());
        }
        System.out.println("cari java     : " + perpus.cariBuku("java").size() + " buku");

        perpus.daftarkan(new Anggota("A-001", "Budi Santoso",
                "[email protected]", LocalDate.of(2026, 1, 2)));
        perpus.daftarkan(new Anggota("A-002", "Siti Rahayu",
                "[email protected]", LocalDate.of(2026, 1, 3)));
        perpus.daftarkan(new Anggota("A-003", "Dewi Lestari",
                "[email protected]", LocalDate.of(2026, 1, 5)));
        System.out.println("anggota       : " + perpus.jumlahAnggota());
        for (Anggota a : perpus.semuaAnggota()) {
            System.out.println("  - " + a.nama() + " (" + a.id() + ")");
        }

        // Penjaga: data ganda ditolak, pencarian yang gagal melempar exception domain.
        try {
            perpus.tambahBuku(new Buku("978-602-421-123-4",
                    "Laskar Pelangi (edisi lama)", "Andrea Hirata", 2005));
        } catch (PerpustakaanException e) {
            System.out.println("buku ganda    : " + e.getMessage());
        }
        try {
            perpus.daftarkan(new Anggota("A-002", "Siti Rahayu",
                    "[email protected]", LocalDate.of(2026, 1, 3)));
        } catch (PerpustakaanException e) {
            System.out.println("anggota ganda : " + e.getMessage());
        }
        try {
            perpus.cariBukuIsbn("978-000-000-000-0");
        } catch (BukuTidakDitemukanException e) {
            System.out.println("isbn tak ada   : " + e.getMessage());
        }
        try {
            perpus.cariAnggota("A-099");
        } catch (AnggotaTidakDitemukanException e) {
            System.out.println("anggota tak ada: " + e.getMessage());
        }

        System.out.println("cari ISBN     : "
                + perpus.cariBukuIsbn("978-602-291-024-4").judul());
    }
}

Output yang Dihasilkan:

text
jumlah buku    : 5
cari LASKAR   : Laskar Pelangi
cari sapardi  : Hujan Bulan Juni
cari java     : 0 buku
anggota       : 3
  - Budi Santoso (A-001)
  - Siti Rahayu (A-002)
  - Dewi Lestari (A-003)
buku ganda    : ISBN sudah terdaftar: 978-602-421-123-4
anggota ganda : ID anggota sudah terdaftar: A-002
isbn tak ada   : ISBN tidak ada di katalog: 978-000-000-000-0
anggota tak ada: ID anggota tidak terdaftar: A-099
cari ISBN     : Laut Bercerita

Mari kita bedah hasilnya:

  1. cari LASKAR dan cari sapardi: pencocokan menemukan Laskar Pelangi lewat judul dan Hujan Bulan Juni lewat nama penulis — huruf besar pada kata kunci tidak berpengaruh.
  2. cari java : 0 buku: kata kunci tanpa hasil menghasilkan List kosong, bukan null, sehingga loop for berjalan nol kali tanpa perlu pemeriksaan khusus.
  3. buku ganda dan anggota ganda: putIfAbsent mengembalikan nilai lama, method melempar PerpustakaanException, dan katalog tetap berisi lima buku — penolakan tidak mengubah data.
  4. isbn tak ada dan anggota tak ada: dua blok catch menangkap jenis exception yang berbeda-beda; pesan pada masing-masing menyebut kunci yang dicari.
  5. cari ISBN: pencarian tepat mengembalikan objek Buku yang sama yang tersimpan di katalog, lalu judul() mengambil komponen record.

Katalog dan keanggotaan kini hidup dalam satu objek layanan dengan penjaga yang aktif. Satu persoalan masih tersisa: objek-objek tersebut belum bergerak — belum ada buku yang keluar dari rak, belum ada jatuh tempo, belum ada denda. Bagian 3 menghidupkan sirkulasi: peminjaman, pengembalian, aturan batas, dan perhitungan denda dengan BigDecimal dan java.time.

Referensi #

Berkas Sumber

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

Anggota.java

Unduh berkas
java
import java.time.LocalDate;

// Keanggotaan: identitas anggota tidak berubah setelah pendaftaran.
public record Anggota(String id, String nama, String email, LocalDate terdaftar) {

    public Anggota {
        if (id == null || !id.matches("A-\\d{3}")) {
            throw new IllegalArgumentException(
                    "id harus berformat A-NNN, diterima: " + id);
        }
        if (nama == null || nama.isBlank()) {
            throw new IllegalArgumentException("nama wajib diisi");
        }
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException(
                    "email harus memuat karakter '@', diterima: " + email);
        }
        nama = nama.trim();
        email = email.trim();
    }
}

AnggotaTidakDitemukanException.java

Unduh berkas
java
// Nomor anggota yang dicari tidak terdaftar.
public class AnggotaTidakDitemukanException extends PerpustakaanException {

    public AnggotaTidakDitemukanException(String pesan) {
        super(pesan);
    }
}

Buku.java

Unduh berkas
java
import java.util.Locale;

// Model katalog: data buku beku (record) + pencocokan kata kunci pencarian.
public record Buku(String isbn, String judul, String penulis, int tahunTerbit) {

    public Buku {
        if (isbn == null || isbn.isBlank()) {
            throw new IllegalArgumentException("isbn wajib diisi");
        }
        if (judul == null || judul.isBlank()) {
            throw new IllegalArgumentException("judul wajib diisi");
        }
        if (tahunTerbit <= 0) {
            throw new IllegalArgumentException(
                    "tahunTerbit harus positif: " + tahunTerbit);
        }
        isbn = isbn.trim();
        judul = judul.trim();
        penulis = (penulis == null || penulis.isBlank()) ? "Tanpa Nama" : penulis.trim();
    }

    /** Apakah judul, penulis, atau ISBN memuat kata kunci (huruf besar diabaikan)? */
    public boolean cocokKataKunci(String kataKunci) {
        String kunci = kataKunci.toLowerCase(Locale.ROOT);
        return judul().toLowerCase(Locale.ROOT).contains(kunci)
                || penulis().toLowerCase(Locale.ROOT).contains(kunci)
                || isbn().contains(kunci);
    }
}

BukuTidakDitemukanException.java

Unduh berkas
java
// Buku yang dicari tidak ada di katalog.
public class BukuTidakDitemukanException extends PerpustakaanException {

    public BukuTidakDitemukanException(String pesan) {
        super(pesan);
    }
}

LayarDemo.java

Unduh berkas
java
import java.time.LocalDate;

// Demo katalog dan keanggotaan: pencarian kata kunci, pendaftaran, penjaga data ganda.
public class LayarDemo {

    public static void main(String[] args) {
        Perpustakaan perpus = new Perpustakaan();
        perpus.tambahBuku(new Buku("978-602-421-123-4", "Laskar Pelangi",
                "Andrea Hirata", 2005));
        perpus.tambahBuku(new Buku("978-979-433-555-1", "Bumi Manusia",
                "Pramoedya Ananta Toer", 1980));
        perpus.tambahBuku(new Buku("978-602-032-556-2", "Hujan Bulan Juni",
                "Sapardi Djoko Damono", 1994));
        perpus.tambahBuku(new Buku("978-602-401-718-3", "Filosofi Teras",
                "Henry Manampiring", 2018));
        perpus.tambahBuku(new Buku("978-602-291-024-4", "Laut Bercerita",
                "Leila S. Chudori", 2017));
        System.out.println("jumlah buku    : " + perpus.jumlahBuku());

        // Pencarian mengabaikan huruf besar-kecil.
        for (Buku buku : perpus.cariBuku("LASKAR")) {
            System.out.println("cari LASKAR   : " + buku.judul());
        }
        for (Buku buku : perpus.cariBuku("sapardi")) {
            System.out.println("cari sapardi  : " + buku.judul());
        }
        System.out.println("cari java     : " + perpus.cariBuku("java").size() + " buku");

        perpus.daftarkan(new Anggota("A-001", "Budi Santoso",
                "[email protected]", LocalDate.of(2026, 1, 2)));
        perpus.daftarkan(new Anggota("A-002", "Siti Rahayu",
                "[email protected]", LocalDate.of(2026, 1, 3)));
        perpus.daftarkan(new Anggota("A-003", "Dewi Lestari",
                "[email protected]", LocalDate.of(2026, 1, 5)));
        System.out.println("anggota       : " + perpus.jumlahAnggota());
        for (Anggota a : perpus.semuaAnggota()) {
            System.out.println("  - " + a.nama() + " (" + a.id() + ")");
        }

        // Penjaga: data ganda ditolak, pencarian yang gagal melempar exception domain.
        try {
            perpus.tambahBuku(new Buku("978-602-421-123-4",
                    "Laskar Pelangi (edisi lama)", "Andrea Hirata", 2005));
        } catch (PerpustakaanException e) {
            System.out.println("buku ganda    : " + e.getMessage());
        }
        try {
            perpus.daftarkan(new Anggota("A-002", "Siti Rahayu",
                    "[email protected]", LocalDate.of(2026, 1, 3)));
        } catch (PerpustakaanException e) {
            System.out.println("anggota ganda : " + e.getMessage());
        }
        try {
            perpus.cariBukuIsbn("978-000-000-000-0");
        } catch (BukuTidakDitemukanException e) {
            System.out.println("isbn tak ada   : " + e.getMessage());
        }
        try {
            perpus.cariAnggota("A-099");
        } catch (AnggotaTidakDitemukanException e) {
            System.out.println("anggota tak ada: " + e.getMessage());
        }

        System.out.println("cari ISBN     : "
                + perpus.cariBukuIsbn("978-602-291-024-4").judul());
    }
}

Perpustakaan.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

// Layanan katalog dan keanggotaan: satu objek memegang seluruh data perpustakaan.
public class Perpustakaan {

    // LinkedHashMap: pencarian cepat berdasar kunci DAN urutan sisip terjaga,
    // sehingga daftar buku/anggota selalu tampil sesuai urutan pendaftaran.
    private final Map<String, Buku> katalog = new LinkedHashMap<>();
    private final Map<String, Anggota> anggota = new LinkedHashMap<>();

    public void tambahBuku(Buku buku) {
        Buku sebelumnya = katalog.putIfAbsent(buku.isbn(), buku);
        if (sebelumnya != null) {
            throw new PerpustakaanException(
                    "ISBN sudah terdaftar: " + buku.isbn());
        }
    }

    public void daftarkan(Anggota baru) {
        Anggota sebelumnya = anggota.putIfAbsent(baru.id(), baru);
        if (sebelumnya != null) {
            throw new PerpustakaanException(
                    "ID anggota sudah terdaftar: " + baru.id());
        }
    }

    public Buku cariBukuIsbn(String isbn) {
        Buku buku = katalog.get(isbn);
        if (buku == null) {
            throw new BukuTidakDitemukanException(
                    "ISBN tidak ada di katalog: " + isbn);
        }
        return buku;
    }

    public Anggota cariAnggota(String id) {
        Anggota dicari = anggota.get(id);
        if (dicari == null) {
            throw new AnggotaTidakDitemukanException(
                    "ID anggota tidak terdaftar: " + id);
        }
        return dicari;
    }

    /** Cari buku yang judul, penulis, atau ISBN-nya memuat kata kunci. */
    public List<Buku> cariBuku(String kataKunci) {
        List<Buku> hasil = new ArrayList<>();
        for (Buku buku : katalog.values()) {
            if (buku.cocokKataKunci(kataKunci)) {
                hasil.add(buku);
            }
        }
        return hasil;
    }

    /** Salinan katalog dalam urutan pendaftaran. */
    public List<Buku> semuaBuku() {
        return List.copyOf(katalog.values());
    }

    /** Salinan daftar anggota dalam urutan pendaftaran. */
    public List<Anggota> semuaAnggota() {
        return List.copyOf(anggota.values());
    }

    public int jumlahBuku() {
        return katalog.size();
    }

    public int jumlahAnggota() {
        return anggota.size();
    }
}

PerpustakaanException.java

Unduh berkas
java
// Induk exception domain: seluruh pelanggaran aturan perpustakaan.
public class PerpustakaanException extends RuntimeException {

    public PerpustakaanException(String pesan) {
        super(pesan);
    }
}