Seluruh Section ini telah melatih teknik: kompleksitas, struktur data, sorting, searching, rekursi, greedy, dynamic programming, dan graph. Namun di dunia kerja, algoritma jarang datang sebagai persoalan CLRS yang sudah terdefinisi rapi — algoritma datang sebagai paragraf spesifikasi dari tim produk, lalu tugas kita menerjemahkannya menjadi kode yang benar, mudah dibaca, dan teruji. Bab penutup ini merangkai seluruh bekal menjadi satu alur kerja utuh melalui studi kasus antrean IGD rumah sakit: membaca spesifikasi, memetakan ke struktur data yang tepat, mengimplementasikan dengan record dan PriorityQueue, lalu membuktikan kebenarannya dengan pengujian.

Membaca Spesifikasi dan Memetakan Struktur #

Spesifikasi dari tim produk berbunyi begini: “Pasien IGD dilayani berdasarkan tingkat keparahan (P1 paling prioritas, lalu P2, lalu P3); pasien dengan tingkat yang sama dilayani sesuai urutan kedatangan; pasien baru dapat masuk kapan saja, termasuk saat antrean sedang diproses.” Kalimat seperti itu memuat tiga petunjuk struktural sekaligus.

Pemetaannya berjalan sistematis. Frasa “tingkat keparahan menentukan urutan, bukan urutan datang” menuntut struktur yang mengurutkan berdasarkan kriteria — bukan antrean FIFO biasa — sehingga PriorityQueue menjadi kandidat alami. Frasa “tingkat sama dilayani sesuai urutan kedatangan” berarti comparator perlu dua kriteria berlapis: level triage lebih dulu, nomor kedatangan sebagai pemecah seri. Frasa “pasien baru dapat masuk kapan saja” menegaskan bahwa penyisipan terjadi terus-menerus — dan PriorityQueue memang mendukung penyisipan kapan pun dengan biaya O(log n). Kata benda pada spesifikasi (pasien, level, nomor) menjadi data, sedangkan aturan pembandingnya menjadi Comparator.

Implementasi Antrean IGD #

Implementasi memanfaatkan dua fitur bahasa yang menjaga kode tetap ramping: record untuk data pasien yang tidak perlu berubah, dan Comparator komposabel untuk aturan urutan. Perhatikan bahwa tidak ada class Pasien manual berisi field privat, constructor, dan tiga getter — record menghasilkan semuanya dalam satu baris:

import java.util.Comparator;
import java.util.PriorityQueue;

public class AntreanRumahSakit {

    // Level triage IGD: P1 paling prioritas, disusul P2, lalu P3.
    enum LevelTriage { P1, P2, P3 }

    // Pasien direpresentasikan record: ringkas, immutable, bernilai sama bila datanya sama.
    record Pasien(String nama, LevelTriage level, int nomorKedatangan) { }

    public static void main(String[] args) {
        // Aturan antrean: level triage dulu, lalu nomor kedatangan (FIFO).
        Comparator<Pasien> urutan = Comparator
                .comparing((Pasien p) -> p.level())
                .thenComparingInt(Pasien::nomorKedatangan);

        PriorityQueue<Pasien> antrean = new PriorityQueue<>(urutan);

        antrean.add(new Pasien("Siti", LevelTriage.P3, 1));
        antrean.add(new Pasien("Budi", LevelTriage.P2, 2));
        antrean.add(new Pasien("Dewi", LevelTriage.P1, 3));
        antrean.add(new Pasien("Joko", LevelTriage.P2, 4));
        antrean.add(new Pasien("Rani", LevelTriage.P1, 5));

        System.out.println("=== Antrean IGD: level triage dulu, lalu urutan datang ===");
        System.out.println("Pasien menunggu: " + antrean.size());

        layani(antrean);

        // Pasien P1 datang ketika antrean sedang berjalan.
        antrean.add(new Pasien("Tono", LevelTriage.P1, 6));
        System.out.println();
        System.out.println("Pasien baru masuk : Tono (P1, kedatangan ke-6)");
        System.out.println("Pasien menunggu   : " + antrean.size());

        while (!antrean.isEmpty()) {
            layani(antrean);
        }
    }

    static void layani(PriorityQueue<Pasien> antrean) {
        Pasien pasien = antrean.poll();
        System.out.println("Dilayani : " + pasien.nama() + " (" + pasien.level()
                + ", kedatangan ke-" + pasien.nomorKedatangan() + "), sisa "
                + antrean.size() + " pasien");
    }
}

Output yang Dihasilkan:

text
=== Antrean IGD: level triage dulu, lalu urutan datang ===
Pasien menunggu: 5
Dilayani : Dewi (P1, kedatangan ke-3), sisa 4 pasien

Pasien baru masuk : Tono (P1, kedatangan ke-6)
Pasien menunggu   : 5
Dilayani : Rani (P1, kedatangan ke-5), sisa 4 pasien
Dilayani : Tono (P1, kedatangan ke-6), sisa 3 pasien
Dilayani : Budi (P2, kedatangan ke-2), sisa 2 pasien
Dilayani : Joko (P2, kedatangan ke-4), sisa 1 pasien
Dilayani : Siti (P3, kedatangan ke-1), sisa 0 pasien

Output membuktikan spesifikasi terpenuhi penuh. Dewi (P1, datang ke-3) dilayani sebelum Budi yang sudah lebih dulu mengantri karena levelnya lebih rendah. Tono yang baru datang ketika antrean sedang berjalan langsung melompati Budi dan Joko — keduanya P2 — namun tetap setelah Rani yang sesama P1 tetapi datang lebih awal. Urutan akhir: Dewi, Rani, Tono, Budi, Joko, Siti.

Mari kita bedah bagian-bagian kuncinya:

  1. Baris 13 (record Pasien(String nama, LevelTriage level, int nomorKedatangan)): satu baris ini menggantikan sekitar dua puluh baris class boilerplate — constructor, equals(), hashCode(), dan toString() dihasilkan compiler. Objek pasien bersifat immutable: tidak ada method yang bisa mengubah level atau nomor setelah objek dibuat, sehingga data di dalam antrean tidak mungkin berubah diam-diam.
  2. Baris 19-21 (Comparator.comparing(...).thenComparingInt(...)): aturan spesifikasi diterjemahkan hampir kata per kata — bandingkan level lebih dulu, lalu nomor kedatangan. Enum LevelTriage dibandingkan menurut urutan deklarasinya (P1 < P2 < P3), sehingga urutan enum harus mencerminkan prioritas.
  3. Baris 23 (new PriorityQueue<>(urutan)): comparator diteruskan ke constructor, menjadikan heap biner internal terurut menurut aturan bisnis kita. Operasi add dan poll masing-masing berbiaya O(log n).
  4. Baris 44-48 (method layani): poll() mengambil dan menghapus elemen terkecil menurut comparator. Method ini memisahkan aksi “melayani satu pasien” sehingga logika utama main tetap memetakan alur cerita spesifikasi — bagian yang mudah dibaca ulang saat spesifikasi berubah.

Menguji Tanpa Framework #

Kode yang benar hari ini bisa salah besok bila aturan comparator diubah tanpa sengaja. Praktik profesional menuntut pengujian otomatis; pada Section Testing kita akan memakai JUnit, tetapi prinsip pengujian bisa dibuktikan sekarang dengan harness sederhana berbasis assertion. Program berikut menguji empat sifat penting dari antrean kita:

java UjiAntrean.java
import java.util.Comparator;
import java.util.PriorityQueue;

public class UjiAntrean {

    static int lulus = 0;
    static int gagal = 0;

    // Triase IGD: P1 paling prioritas, disusul P2, lalu P3.
    enum LevelTriage { P1, P2, P3 }

    record Pasien(String nama, LevelTriage level, int nomorKedatangan) { }

    static void periksa(String namaUji, boolean kondisi) {
        if (kondisi) {
            lulus++;
            System.out.println("LULUS  : " + namaUji);
        } else {
            gagal++;
            System.out.println("GAGAL  : " + namaUji);
        }
    }

    static PriorityQueue<Pasien> antreanBaru() {
        Comparator<Pasien> urutan = Comparator
                .comparing((Pasien p) -> p.level())
                .thenComparingInt(Pasien::nomorKedatangan);
        return new PriorityQueue<>(urutan);
    }

    public static void main(String[] args) {
        // Uji 1: level triage menentukan urutan, bukan urutan kedatangan.
        PriorityQueue<Pasien> antrean = antreanBaru();
        antrean.add(new Pasien("Siti", LevelTriage.P3, 1));
        antrean.add(new Pasien("Budi", LevelTriage.P2, 2));
        antrean.add(new Pasien("Dewi", LevelTriage.P1, 3));

        periksa("Pasien P1 dilayani lebih dulu",
                antrean.poll().equals(new Pasien("Dewi", LevelTriage.P1, 3)));
        periksa("Pasien P2 menyusul",
                antrean.poll().equals(new Pasien("Budi", LevelTriage.P2, 2)));
        periksa("Pasien P3 terakhir",
                antrean.poll().equals(new Pasien("Siti", LevelTriage.P3, 1)));
        periksa("Antrean kosong setelah semua dilayani", antrean.isEmpty());

        // Uji 2: level sama berarti yang datang lebih dulu menang (FIFO).
        PriorityQueue<Pasien> selevel = antreanBaru();
        selevel.add(new Pasien("Joko", LevelTriage.P2, 4));
        selevel.add(new Pasien("Budi", LevelTriage.P2, 2));

        periksa("Antrean FIFO untuk level yang sama",
                selevel.poll().nama().equals("Budi"));

        // Uji 3: record Pasien memiliki value semantics.
        Pasien a = new Pasien("Dewi", LevelTriage.P1, 3);
        Pasien b = new Pasien("Dewi", LevelTriage.P1, 3);
        periksa("Record dengan data sama dianggap sama", a.equals(b));
        periksa("hashCode juga sama", a.hashCode() == b.hashCode());

        // Uji 4: nilai field record tidak bisa berubah lewat referensi bersama.
        Pasien asli = new Pasien("Rani", LevelTriage.P1, 5);
        Pasien salinan = asli; // dua nama menunjuk objek yang sama
        periksa("Referensi yang sama tetap objek yang sama", salinan == asli);

        System.out.println();
        System.out.println("Ringkasan: " + lulus + " lulus, " + gagal + " gagal");
    }
}

Output yang Dihasilkan:

text
LULUS  : Pasien P1 dilayani lebih dulu
LULUS  : Pasien P2 menyusul
LULUS  : Pasien P3 terakhir
LULUS  : Antrean kosong setelah semua dilayani
LULUS  : Antrean FIFO untuk level yang sama
LULUS  : Record dengan data sama dianggap sama
LULUS  : hashCode juga sama
LULUS  : Referensi yang sama tetap objek yang sama

Ringkasan: 8 lulus, 0 gagal

Kedelapan pemeriksaan lulus. Setiap pemeriksaan menyasar satu sifat spesifik: urutan antar level, pemecah seri FIFO, value semantics record, hingga kekosongan antrean. Pada baris 34-36, assertion membandingkan hasil poll() dengan record baru yang datanya sama — perbandingan ini hanya bekerja karena record menghasilkan equals() berbasis nilai secara otomatis.

Bedah singkat untuk dua keputusan desain harness ini:

  1. Baris 16-17 (static int lulus, gagal): penghitung terpisah membuat ringkasan akhir menjelaskan kesehatan kode dalam satu baris. Harness yang baik selalu mencetak ringkasan, bukan menghentikan eksekusi pada kegagalan pertama — semua masalah terlihat sekaligus.
  2. Baris 31-36 (method antreanBaru): pabrik antrean segar dipanggil tiap skenario agar pengujian saling bebas — perubahan pada antrean skenario pertama tidak bocor ke skenario kedua. Prinsip ini kelak menjadi anatomi standar @BeforeEach di JUnit.

Tiga Praktik yang Menjaga Kode Awet #

Studi kasus ini memperlihatkan tiga praktik yang berlaku untuk implementasi algoritma apa pun. Generik dan komposabel: PriorityQueue tidak tahu apa-apa tentang rumah sakit — struktur data menerima Comparator dari luar sehingga komponen dan aturan bisnis terpisah bersih. Immutable untuk data: record menjamin data pasien tidak berubah setelah dibuat, menghapus seluruh kelas bug “data dimodifikasi diam-diam dari tempat lain”. Teruji: sifat-sifat penting dijaring assertion yang bisa dijalankan ulang kapan pun, sehingga perbaikan di masa depan langsung tertangkap bila melanggar spesifikasi.

Dengan ini petualangan algoritma dan struktur data kita tuntas. Kode yang efisien dan benar hanyalah setengah cerita — setengah lainnya adalah kode yang tahan terhadap masukan jahat, kebocoran data, dan upaya manipulasi. Section berikutnya, Security, membuka bab baru yang sama pentingnya: bagaimana aplikasi Java kita tetap aman di dunia yang tidak selalu bersahabat.

Referensi #

Berkas Sumber

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

AntreanRumahSakit.java

Unduh berkas
java
import java.util.Comparator;
import java.util.PriorityQueue;

public class AntreanRumahSakit {

    // Level triage IGD: P1 paling prioritas, disusul P2, lalu P3.
    enum LevelTriage { P1, P2, P3 }

    // Pasien direpresentasikan record: ringkas, immutable, bernilai sama bila datanya sama.
    record Pasien(String nama, LevelTriage level, int nomorKedatangan) { }

    public static void main(String[] args) {
        // Aturan antrean: level triage dulu, lalu nomor kedatangan (FIFO).
        Comparator<Pasien> urutan = Comparator
                .comparing((Pasien p) -> p.level())
                .thenComparingInt(Pasien::nomorKedatangan);

        PriorityQueue<Pasien> antrean = new PriorityQueue<>(urutan);

        antrean.add(new Pasien("Siti", LevelTriage.P3, 1));
        antrean.add(new Pasien("Budi", LevelTriage.P2, 2));
        antrean.add(new Pasien("Dewi", LevelTriage.P1, 3));
        antrean.add(new Pasien("Joko", LevelTriage.P2, 4));
        antrean.add(new Pasien("Rani", LevelTriage.P1, 5));

        System.out.println("=== Antrean IGD: level triage dulu, lalu urutan datang ===");
        System.out.println("Pasien menunggu: " + antrean.size());

        layani(antrean);

        // Pasien P1 datang ketika antrean sedang berjalan.
        antrean.add(new Pasien("Tono", LevelTriage.P1, 6));
        System.out.println();
        System.out.println("Pasien baru masuk : Tono (P1, kedatangan ke-6)");
        System.out.println("Pasien menunggu   : " + antrean.size());

        while (!antrean.isEmpty()) {
            layani(antrean);
        }
    }

    static void layani(PriorityQueue<Pasien> antrean) {
        Pasien pasien = antrean.poll();
        System.out.println("Dilayani : " + pasien.nama() + " (" + pasien.level()
                + ", kedatangan ke-" + pasien.nomorKedatangan() + "), sisa "
                + antrean.size() + " pasien");
    }
}

UjiAntrean.java

Unduh berkas
java
import java.util.Comparator;
import java.util.PriorityQueue;

public class UjiAntrean {

    static int lulus = 0;
    static int gagal = 0;

    // Triase IGD: P1 paling prioritas, disusul P2, lalu P3.
    enum LevelTriage { P1, P2, P3 }

    record Pasien(String nama, LevelTriage level, int nomorKedatangan) { }

    static void periksa(String namaUji, boolean kondisi) {
        if (kondisi) {
            lulus++;
            System.out.println("LULUS  : " + namaUji);
        } else {
            gagal++;
            System.out.println("GAGAL  : " + namaUji);
        }
    }

    static PriorityQueue<Pasien> antreanBaru() {
        Comparator<Pasien> urutan = Comparator
                .comparing((Pasien p) -> p.level())
                .thenComparingInt(Pasien::nomorKedatangan);
        return new PriorityQueue<>(urutan);
    }

    public static void main(String[] args) {
        // Uji 1: level triage menentukan urutan, bukan urutan kedatangan.
        PriorityQueue<Pasien> antrean = antreanBaru();
        antrean.add(new Pasien("Siti", LevelTriage.P3, 1));
        antrean.add(new Pasien("Budi", LevelTriage.P2, 2));
        antrean.add(new Pasien("Dewi", LevelTriage.P1, 3));

        periksa("Pasien P1 dilayani lebih dulu",
                antrean.poll().equals(new Pasien("Dewi", LevelTriage.P1, 3)));
        periksa("Pasien P2 menyusul",
                antrean.poll().equals(new Pasien("Budi", LevelTriage.P2, 2)));
        periksa("Pasien P3 terakhir",
                antrean.poll().equals(new Pasien("Siti", LevelTriage.P3, 1)));
        periksa("Antrean kosong setelah semua dilayani", antrean.isEmpty());

        // Uji 2: level sama berarti yang datang lebih dulu menang (FIFO).
        PriorityQueue<Pasien> selevel = antreanBaru();
        selevel.add(new Pasien("Joko", LevelTriage.P2, 4));
        selevel.add(new Pasien("Budi", LevelTriage.P2, 2));

        periksa("Antrean FIFO untuk level yang sama",
                selevel.poll().nama().equals("Budi"));

        // Uji 3: record Pasien memiliki value semantics.
        Pasien a = new Pasien("Dewi", LevelTriage.P1, 3);
        Pasien b = new Pasien("Dewi", LevelTriage.P1, 3);
        periksa("Record dengan data sama dianggap sama", a.equals(b));
        periksa("hashCode juga sama", a.hashCode() == b.hashCode());

        // Uji 4: nilai field record tidak bisa berubah lewat referensi bersama.
        Pasien asli = new Pasien("Rani", LevelTriage.P1, 5);
        Pasien salinan = asli; // dua nama menunjuk objek yang sama
        periksa("Referensi yang sama tetap objek yang sama", salinan == asli);

        System.out.println();
        System.out.println("Ringkasan: " + lulus + " lulus, " + gagal + " gagal");
    }
}