Sebelum Maven atau Gradle menyentuh proyek Anda, ada satu alat yang bekerja paling awal: compiler javac. Compiler di dalam JDK ini menerjemahkan berkas .java menjadi berkas .class berisi bytecode — format yang dibaca JVM. Memahami kompilasi manual memberi fondasi untuk memahami apa yang diotomatiskan build tool: mencari berkas sumber, menyusun classpath, mengelola output, dan membaca pesan error. Pada chapter ini kita mengompilasi program multi-file secara manual, mengatur struktur package, dan mengenali opsi javac yang paling sering dipakai.

Dari Kode Sumber ke Bytecode #

Compiler javac membaca berkas .java, memeriksa tipe setiap ekspresi, lalu menulis satu berkas .class untuk setiap class atau interface. Bytecode bersifat netral terhadap sistem operasi dan prosesor, sehingga berkas yang dikompilasi di Linux dapat dijalankan di Windows tanpa kompilasi ulang. Pemeriksaan tipe terjadi sepenuhnya saat kompilasi, sehingga pemanggilan method yang tidak ada tertangkap sebelum program berjalan.

Untuk program beberapa berkas, javac perlu melihat semua berkas yang saling merujuk. Jika class AplikasiPenjualan memakai class Penjualan, kedua berkas harus tersedia bagi compiler sebagai argumen baris perintah atau lewat classpath.

Kompilasi Serentak Banyak Berkas #

Kita mulai dengan aplikasi kasir kecil berisi dua berkas pada direktori yang sama. Berkas pertama mendefinisikan class data:

java Penjualan.java
public class Penjualan {
    private final String namaBarang;
    private final int jumlah;
    private final long hargaSatuan;

    public Penjualan(String namaBarang, int jumlah, long hargaSatuan) {
        this.namaBarang = namaBarang;
        this.jumlah = jumlah;
        this.hargaSatuan = hargaSatuan;
    }

    public long hitungTotal() {
        return (long) jumlah * hargaSatuan;
    }

    public String ringkas() {
        return String.format("%-14s x%-2d Rp%d", namaBarang, jumlah, hitungTotal());
    }
}

Berkas kedua berisi titik masuk main dan memakai class di atas:

public class AplikasiPenjualan {
    public static void main(String[] args) {
        Penjualan[] daftar = {
            new Penjualan("Kopi Arabika", 2, 65000),
            new Penjualan("Gula Pasir", 1, 16000),
            new Penjualan("Susu Kental", 3, 21000)
        };

        long totalBelanja = 0;
        for (Penjualan p : daftar) {
            System.out.println(p.ringkas());
            totalBelanja += p.hitungTotal();
        }
        System.out.println("------------------------------");
        System.out.println("Total belanja: Rp" + totalBelanja);
    }
}

Output yang Dihasilkan:

text
Kopi Arabika   x2  Rp130000
Gula Pasir     x1  Rp16000
Susu Kental    x3  Rp63000
------------------------------
Total belanja: Rp209000

Mari kita telaah kode di atas dari sisi kompilasinya:

  1. Baris 1 (public class Penjualan {): class publik wajib berada pada berkas bernama sama, sehingga berkas ini harus bernama Penjualan.java; aturan ini membantu javac menemukan class yang dirujuk berkas lain.
  2. Baris 13-14 (public long hitungTotal()): cast eksplisit (long) menjaga hasil perkalian int * long pada tipe 64-bit.
  3. Baris 17-19 (public String ringkas()): compiler memeriksa kecocokan argumen String.format dengan penentu formatnya.
  4. Baris 3-6 (Penjualan[] daftar = {...}): javac mencatat referensi ke class Penjualan lalu mencari berkasnya pada daftar kompilasi.
  5. Baris 9-14 (for (Penjualan p : daftar)): pemanggilan p.ringkas() dan p.hitungTotal() diverifikasi terhadap deklarasi method di Penjualan.java.
  6. Baris 17 ("Total belanja: Rp" + totalBelanja): penggabungan string dikompilasi menjadi instruksi StringBuilder — transformasi kecil compiler di balik layar.

Kedua berkas dikompilasi dengan satu perintah serentak:

bash
javac AplikasiPenjualan.java Penjualan.java
ls *.class
java AplikasiPenjualan

Output yang Dihasilkan:

text
AplikasiPenjualan.class
Penjualan.class
Kopi Arabika   x2  Rp130000
Gula Pasir     x1  Rp16000
Susu Kental    x3  Rp63000
------------------------------
Total belanja: Rp209000

Compiler menulis dua berkas .class ke direktori kerja, lalu JVM menjalankan class AplikasiPenjualan dari classpath bawaan — direktori itu sendiri.

Opsi -d dan Struktur package #

Pada proyek nyata, class hidup di dalam package dan berkas sumber tersusun dalam direktori src: package toko berarti subdirektori src/toko. Opsi -d menyuruh javac menulis hasil kompilasi ke direktori output tersendiri sambil menirukan struktur package:

bash
javac -d out src/toko/AplikasiKasir.java src/toko/Laporan.java src/util/FormatAngka.java
ls out out/toko out/util
java -cp out toko.AplikasiKasir

Output yang Dihasilkan:

text
out:
toko
util

out/toko:
AplikasiKasir.class
Laporan.class

out/util:
FormatAngka.class
Kopi Arabika   x2  Rp130.000
Gula Pasir     x1  Rp16.000
Susu Kental    x3  Rp63.000
------------------------------
Total belanja: Rp209.000

Perhatikan bahwa out berisi subdirektori toko dan util, bukan berkas class yang menumpuk di satu tempat. Struktur ini menjadi dasar konvensi direktori Maven pada chapter berikutnya.

Classpath dengan Entri Berganda #

Classpath adalah daftar lokasi tempat javac dan JVM mencari berkas .class. Ketika hasil kompilasi tersebar di beberapa direktori, kita menuliskannya sebagai satu classpath berpemisah titik dua pada Linux dan macOS. Skenario berikut mengompilasi package util lebih dulu, lalu package toko yang bergantung padanya:

bash
javac -d out-util src/util/FormatAngka.java
javac -d out-toko -cp out-util src/toko/AplikasiKasir.java src/toko/Laporan.java
java -cp out-toko:out-util toko.AplikasiKasir

Output yang Dihasilkan:

text
Kopi Arabika   x2  Rp130.000
Gula Pasir     x1  Rp16.000
Susu Kental    x3  Rp63.000
------------------------------
Total belanja: Rp209.000

Flag -cp pada javac mengarahkan pencarian class saat kompilasi, sedangkan -cp pada java mengarahkan pencarian saat eksekusi — keduanya berdiri sendiri. Jika satu entri terlupakan, JVM gagal memuat class yang dibutuhkan:

bash
java -cp out-toko toko.AplikasiKasir

Output yang Dihasilkan:

text
Exception in thread "main" java.lang.NoClassDefFoundError: util/FormatAngka
        at toko.Laporan.ringkas(Laporan.java:20)
        at toko.AplikasiKasir.main(AplikasiKasir.java:13)
Caused by: java.lang.ClassNotFoundException: util.FormatAngka

Source-File Launcher #

Sejak Java 11 dan diperluas di Java 22 lewat JEP 458, perintah java dapat menjalankan berkas sumber secara langsung tanpa langkah javac eksplisit:

bash
java AplikasiPenjualan.java

Output yang Dihasilkan:

text
Kopi Arabika   x2  Rp130000
Gula Pasir     x1  Rp16000
Susu Kental    x3  Rp63000
------------------------------
Total belanja: Rp209000

Mekanismenya: JVM mengompilasi berkas sumber dan berkas lain yang dirujuknya ke memori, lalu langsung mengeksekusinya — tidak ada berkas .class yang tertinggal di disk. Mode ini cocok untuk skrip dan eksperimen kecil. Untuk proyek yang didistribusikan, kompilasi eksplisit tetap menjadi jalur resmi karena kita mengendalikan output dan opsi compiler.

Menargetkan Versi Lain dengan –release #

Compiler JDK 25 secara bawaan menghasilkan bytecode versi 25 yang hanya berjalan di JVM 25 ke atas. Opsi --release menghasilkan bytecode untuk versi Java lebih lama sekaligus membatasi API yang boleh dipakai ke API versi tersebut:

bash
javac --release 21 -d out-release src/toko/AplikasiKasir.java src/toko/Laporan.java src/util/FormatAngka.java
javap -verbose out-release/toko/Laporan.class | grep "major version"
javap -verbose out/toko/Laporan.class | grep "major version"

Output yang Dihasilkan:

text
  major version: 65
  major version: 69

Bytecode major version: 65 sesuai Java 21, sedangkan 69 sesuai Java 25. Jika kode memakai API yang belum ada di versi target, compiler menolaknya dengan pesan error — lebih aman daripada sekadar menurunkan nomor versi bytecode tanpa pemeriksaan API.

Memeriksa Peringatan dengan -Xlint #

Secara bawaan javac hanya menyampaikan error, bukan peringatan rinci. Opsi -Xlint:all menyalakan seluruh kategori pemeriksaan, termasuk rawtypes dan unchecked yang sering muncul pada kode lawas:

java Contoh raw type tanpa generics
List daftar = new ArrayList();   // raw type: tanpa parameter tipe
daftar.add("Budi");              // unchecked: tidak ada pemeriksaan tipe

Kompilasi kode tersebut menghasilkan keluaran berikut:

bash
javac -Xlint:all DaftarMentah.java

Output yang Dihasilkan:

text
DaftarMentah.java:6: warning: [rawtypes] found raw type: List
        List daftar = new ArrayList();
        ^
  missing type arguments for generic class List<E>
DaftarMentah.java:7: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
        daftar.add("Budi");
                  ^
4 warnings

Tanpa -Xlint, compiler hanya mencatat ringkas uses unchecked or unsafe operations. Peringatan seperti ini biasanya diperbaiki dengan menambahkan generics — List<String> — sehingga pemeriksaan tipe kembali dikerjakan compiler.

Membaca Pesan Error Compiler #

Pesan error javac selalu memuat nama berkas, nomor baris, dan cuplikan kode dengan penanda posisi. Error paling awal yang akan Anda temui adalah ketidakcocokan nama class publik dengan nama berkas — LaporanSalah.java yang berisi public class Kasir:

bash
javac LaporanSalah.java

Output yang Dihasilkan:

text
LaporanSalah.java:1: error: class Kasir is public, should be declared in a file named Kasir.java
public class Kasir {
       ^
1 error

Pola membacanya: cari LaporanSalah.java:1 untuk berkas dan barisnya, lalu baca kalimat error untuk perbaikannya — ganti nama berkas menjadi Kasir.java atau ubah nama classnya. Kebiasaan ini membuat kita cepat menemukan penyebab error tanpa menebak.

Kompilasi manual dengan javac memberi kendali penuh, tetapi jumlah berkas proyek nyata tumbuh cepat dan classpath menjadi panjang. Kita butuh cara membungkus hasil kompilasi menjadi satu berkas rapi untuk didistribusikan — itulah pekerjaan format JAR pada chapter berikutnya.

Referensi #

Berkas Sumber

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

AplikasiPenjualan.java

Unduh berkas
java
public class AplikasiPenjualan {
    public static void main(String[] args) {
        Penjualan[] daftar = {
            new Penjualan("Kopi Arabika", 2, 65000),
            new Penjualan("Gula Pasir", 1, 16000),
            new Penjualan("Susu Kental", 3, 21000)
        };

        long totalBelanja = 0;
        for (Penjualan p : daftar) {
            System.out.println(p.ringkas());
            totalBelanja += p.hitungTotal();
        }
        System.out.println("------------------------------");
        System.out.println("Total belanja: Rp" + totalBelanja);
    }
}

Penjualan.java

Unduh berkas
java
public class Penjualan {
    private final String namaBarang;
    private final int jumlah;
    private final long hargaSatuan;

    public Penjualan(String namaBarang, int jumlah, long hargaSatuan) {
        this.namaBarang = namaBarang;
        this.jumlah = jumlah;
        this.hargaSatuan = hargaSatuan;
    }

    public long hitungTotal() {
        return (long) jumlah * hargaSatuan;
    }

    public String ringkas() {
        return String.format("%-14s x%-2d Rp%d", namaBarang, jumlah, hitungTotal());
    }
}