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:
umur diterima: -5
Program selesai tanpa satu pun pesan galat.Mari kita bedah apa yang sebenarnya terjadi:
- Baris
new IllegalArgumentException(...): ekspresi ini membuat objek exception di heap — lengkap dengan pesannya — lalu langsung membuang referensinya. Tanpathrow, tidak ada mekanisme yang membawa objek itu ke mana pun. - Keluaran
umur diterima: -5: cabangifdieksekusi, objek exception dibuat, tetapi validasi tidak menolak nilai negatif. Method melanjutkan seolah semuanya benar. - Pemeriksaan
DeadExceptionpada Error Prone: pola “objek exception dibuat tanpathrow” dikenali pada AST, sehingga kelalaian ini ditandai saat kompilasi — tepat pada baris yang salah, sebelum program sempat menipu siapa pun dengan kelakuannya yang tenang. - Varian sejenis: hasil
String.format(...)yang dibuang, ataunew Thread(...)tanpastart(), 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:
setelah remove(1) : [10, 30]
setelah remove(Integer.valueOf(30)): [10]Poin bedah untuk program tersebut:
angka.remove(1): compiler memilih overloadremove(int)karena literal1bertipeintdan cocok persis tanpa boxing. Elemen indeks 1 — nilai20— terbuang, terlihat dari keluaran[10, 30].angka.remove(Integer.valueOf(30)): argumen bertipeInteger(object), sehingga overloadremove(Object)terpilih dan nilai30dibuang — keluaran[10].- Pemeriksaan
ListRemoveAmbiguous: Error Prone menandai pemanggilanremoveyang maknanya mudah tertukar pada tipe elemen numerik, mendorong penulisan eksplisitremove(Integer.valueOf(...))atauremoveIf(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:
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:
<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 berkaspublic 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 berkasimport 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);
}
}