Checkstyle menyapa kita setelah kode tertulis; SpotBugs menyapa setelah kode terkompilasi. Error Prone menyapa lebih awal dari keduanya: sebagai plugin bagi compiler javac itu sendiri, ia memeriksa kode pada saat kompilasi berlangsung dan melaporkan temuan sebagai galat atau peringatan kompilator. Artinya, pola seperti exception yang dibuat tetapi tidak pernah dilempar dapat ditolak bahkan sebelum berkas .class dihasilkan — siklus umpan balik sependek mungkin. Alat ini dikembangkan dan dipakai secara luas di Google untuk menjuta-juta baris kode, dan katalog pemeriksaannya tumbuh dari bug nyata yang berulang. Pada Chapter ini kita akan memahami mekanisme plugin compiler, menjalankan dua kasus klasiknya, dan melihat cara memasangnya ke build.

Pemeriksaan di Dalam Compiler #

Compiler javac memproses kode melalui tahapan yang jelas: parsing ke pohon sintaks (AST), resolusi nama dan tipe, lalu pembangkitan bytecode. Error Prone mengaitkan diri sebagai plugin pada tahap-tahapan itu melalui opsi -Xplugin:ErrorProne, lalu mendaftarkan pemeriksa yang berjalan pada AST setiap method yang dikompilasi. Ketika sebuah pemeriksa mengenali pola bermasalah, Error Prone memunculkan diagnostik dengan nama pemeriksaan — misalnya DeadException — pada baris yang bersangkutan, dengan tingkat keparahan yang dapat dikonfigurasi sebagai galat (menggagalkan kompilasi) atau peringatan.

Keuntungan posisi ini nyata: tidak ada alat terpisah yang harus dipasang, temuan muncul di editor yang sama dengan error kompilasi biasa, dan perbaikannya terjadi sebelum kode pernah berjalan. Batasnya juga jelas — pemeriksaan hanya melihat satu unit kompilasi dan informasi tipe yang tersedia, sehingga analisis lintas modul dan aliran data yang dalam tetap ranah SpotBugs.

Kasus Dead Exception #

Bug paling ringan namun paling memalukan: exception sudah disiapkan dengan pesan yang matang, tetapi kata kunci throw lupa ditulis. Program berjalan normal, validasi diam-diam tidak melakukan apa-apa. Program berikut memperagakan akibatnya:

public class ExceptionMati {
    public static void main(String[] args) {
        validasiUmur(-5);
        System.out.println("Program selesai tanpa satu pun pesan galat.");
    }

    static void validasiUmur(int umur) {
        if (umur < 0) {
            // BUG: exception dibuat, tetapi tidak pernah dilempar
            new IllegalArgumentException("umur tidak boleh negatif: " + umur);
        }
        System.out.println("umur diterima: " + umur);
    }
}

Output yang Dihasilkan:

text
umur diterima: -5
Program selesai tanpa satu pun pesan galat.

Mari kita bedah apa yang sebenarnya terjadi:

  1. Baris new IllegalArgumentException(...): ekspresi ini membuat objek exception di heap — lengkap dengan pesannya — lalu langsung membuang referensinya. Tanpa throw, tidak ada mekanisme yang membawa objek itu ke mana pun.
  2. Keluaran umur diterima: -5: cabang if dieksekusi, objek exception dibuat, tetapi validasi tidak menolak nilai negatif. Method melanjutkan seolah semuanya benar.
  3. Pemeriksaan DeadException pada Error Prone: pola “objek exception dibuat tanpa throw” dikenali pada AST, sehingga kelalaian ini ditandai saat kompilasi — tepat pada baris yang salah, sebelum program sempat menipu siapa pun dengan kelakuannya yang tenang.
  4. Varian sejenis: hasil String.format(...) yang dibuang, atau new Thread(...) tanpa start(), ditangkap pemeriksaan keluarga yang sama (DeadThread, UnusedException).

Perbaikannya satu kata: tambahkan throw sebelum new. Pola penekanan (suppress) untuk kasus yang memang disengaja tersedia lewat anotasi @SuppressWarnings("DeadException") pada scope terkecil yang membutuhkan.

Kasus List remove yang Ambigu #

List memiliki dua overload remove: remove(int indeks) dan remove(Object nilai). Pada List<Integer>, pemanggilan remove(1) terikat ke overload indeks — meskipun niat programmer sering kali membuang nilai 1. Program berikut memperagakan jebakan dan penulisannya yang eksplisit:

import java.util.ArrayList;
import java.util.List;

public class HimpunanRemove {
    public static void main(String[] args) {
        List<Integer> angka = new ArrayList<>(List.of(10, 20, 30));

        // Overload remove(int indeks) terpilih, bukan remove(Object nilai)
        angka.remove(1);
        System.out.println("setelah remove(1)                 : " + angka);

        // Niat membuang nilai 30: bungkus eksplisit agar overload Object terpilih
        angka.remove(Integer.valueOf(30));
        System.out.println("setelah remove(Integer.valueOf(30)): " + angka);
    }
}

Output yang Dihasilkan:

text
setelah remove(1)                 : [10, 30]
setelah remove(Integer.valueOf(30)): [10]

Poin bedah untuk program tersebut:

  1. angka.remove(1): compiler memilih overload remove(int) karena literal 1 bertipe int dan cocok persis tanpa boxing. Elemen indeks 1 — nilai 20 — terbuang, terlihat dari keluaran [10, 30].
  2. angka.remove(Integer.valueOf(30)): argumen bertipe Integer (object), sehingga overload remove(Object) terpilih dan nilai 30 dibuang — keluaran [10].
  3. Pemeriksaan ListRemoveAmbiguous: Error Prone menandai pemanggilan remove yang maknanya mudah tertukar pada tipe elemen numerik, mendorong penulisan eksplisit remove(Integer.valueOf(...)) atau removeIf(n -> n == 30) yang tidak ambigu.

Kasus Penggabungan String dalam Loop #

Pola ketiga yang menjadi temuan klasik analisis statik: menggabungkan string dengan operator + di dalam loop ketat:

java
String laporan = "";
for (int i = 0; i < baris.size(); i++) {
    laporan += baris.get(i) + "\n";   // objek string baru setiap iterasi
}

Setiap iterasi membangun string baru yang menyalin seluruh isi sebelumnya, sehingga total pekerjaan menyalin tumbuh kuadratik terhadap jumlah baris. SpotBugs mengenal pola ini sebagai SBSC_USE_STRING_CONCATENATION_IN_LOOP, dan perbaikan bakunya adalah StringBuilder dengan append di dalam loop, atau String.join("\n", baris) untuk kasus sederhana. Error Prone menangani keluarga serupa melalui pemeriksaan seperti StringConcatToTextBlock dan pemeriksaan performa lain yang relevan pada konteks masing-masing.

Memasang ke Build #

Error Prone terpasang sebagai annotation processor pada compiler. Konfigurasi Maven berikut memperagakan pola standarnya:

xml
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <compilerArgs>
      <arg>-XDcompilePolicy=simple</arg>
      <arg>-Xplugin:ErrorProne</arg>
    </compilerArgs>
    <annotationProcessorPaths>
      <path>
        <groupId>com.google.errorprone</groupId>
        <artifactId>error_prone_core</artifactId>
        <version>2.36.0</version>
      </path>
    </annotationProcessorPaths>
  </configuration>
</plugin>

Gradle memakai mekanisme plugins { id 'net.ltgt.errorprone' } dengan tujuan sama. Tim lazimnya memulai dengan seluruh pemeriksaan pada tingkat peringatan selama beberapa minggu, lalu menaikkan pemeriksaan berisiko tinggi (DeadException, EqualsHashCode, dan sejenisnya) menjadi galat yang menggagalkan build — strategi pengetatan bertahap yang sama dengan baseline Checkstyle.

Kini tiga lapis alat pelindung telah lengkap: gaya dijaga Checkstyle, bytecode disapu SpotBugs, dan kompilasi dijaga Error Prone. Lapisan berikutnya bahkan tidak menganalisis kode sama sekali — ia menyeragamkan bentuknya secara otomatis. Chapter berikutnya membahas code formatting dan berkas .editorconfig yang mengakhiri perdebatan spasi.

Referensi #

Berkas Sumber

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

ExceptionMati.java

Unduh berkas
java
public class ExceptionMati {
    public static void main(String[] args) {
        validasiUmur(-5);
        System.out.println("Program selesai tanpa satu pun pesan galat.");
    }

    static void validasiUmur(int umur) {
        if (umur < 0) {
            // BUG: exception dibuat, tetapi tidak pernah dilempar
            new IllegalArgumentException("umur tidak boleh negatif: " + umur);
        }
        System.out.println("umur diterima: " + umur);
    }
}

HimpunanRemove.java

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

public class HimpunanRemove {
    public static void main(String[] args) {
        List<Integer> angka = new ArrayList<>(List.of(10, 20, 30));

        // Overload remove(int indeks) terpilih, bukan remove(Object nilai)
        angka.remove(1);
        System.out.println("setelah remove(1)                 : " + angka);

        // Niat membuang nilai 30: bungkus eksplisit agar overload Object terpilih
        angka.remove(Integer.valueOf(30));
        System.out.println("setelah remove(Integer.valueOf(30)): " + angka);
    }
}