Java membagi exception menjadi dua kategori: checked yang wajib dideklarasikan di signature function, dan unchecked yang bebas dari aturan tersebut. Kotlin menghapus pembedaan ini — semua exception diperlakukan unchecked. Keputusan ini menyederhanakan kode, namun juga menggeser tanggung jawab disiplin dokumentasi ke pengembang. Pada chapter ini kita akan melihat perbedaan keduanya, demonstrasi Kotlin yang melempar IOException tanpa throws, serta kapan anotasi @Throws tetap berguna untuk interop dengan Java.
Pembedaan di Java #
Di Java, setiap exception yang mewarisi Throwable tapi bukan turunan RuntimeException atau Error bersifat checked. Compiler Java mengharuskan function yang melempar exception checked untuk mendeklarasikannya dengan keyword throws, dan setiap pemanggil wajib menangkap atau mendeklarasikannya kembali.
// Di Java, signature wajib mencantumkan `throws IOException`
public String bacaFile(String path) throws IOException {
FileInputStream stream = new FileInputStream(path); // bisa melempar IOException
return new String(stream.readAllBytes());
}Tujuan awal checked exception adalah memaksa pemanggil sadar akan kegagalan yang mungkin terjadi. Dalam praktiknya, aturan ini sering kali menambah boilerplate: pemanggil yang tidak punya strategi pemulihan akhirnya menangkap dan mengabaikan, atau membungkus ke RuntimeException agar terbebas dari deklarasi.
Kotlin Menghapus Pembedaan #
Kotlin tidak memiliki konsep checked exception. Function Kotlin boleh melempar exception apa pun tanpa mendeklarasikannya di signature. Pemanggil bebas memilih: menangkap bila punya strategi, atau membiarkan naik bila tidak. Compiler Kotlin tidak menghasilkan error bila kita memanggil function Java yang melempar checked exception dan tidak menangkapnya.
import java.io.FileInputStream
import java.io.IOException
fun bacaFileBesar(path: String): String {
// FileInputStream konstruktor melempar FileNotFoundException (checked di Java).
// Kotlin tidak memaksa deklarasi `throws` di sini.
val stream = FileInputStream(path)
return stream.use { it.readBytes().decodeToString() }
}Kode di atas memanggil konstruktor FileInputStream yang melempar FileNotFoundException (checked di Java). Compiler Kotlin tidak mengeluh meskipun bacaFileBesar tidak mendeklarasikan throws IOException. Pemanggil bebas menangkap atau tidak.
Demonstrasi Lengkap #
Contoh berikut menunjukkan Kotlin yang melempar IOException, anotasi @Throws untuk interop Java, dan demonstrasi bahwa exception checked Java seperti ParseException dapat dilempar dan ditangkap tanpa deklarasi.
// Demo checked vs unchecked: Kotlin tidak memaksa deklarasi throws; interop dengan Java.
import java.io.FileInputStream
import java.io.IOException
// Function Kotlin yang melempar IOException — tidak ada `throws` di signature.
fun bacaFileBesar(path: String): String {
// FileInputStream konstruktor melempar FileNotFoundException (checked di Java).
// Kotlin tidak memaksa kita mendeklarasikan `@Throws` di sini.
val stream = FileInputStream(path)
return stream.use {
it.readBytes().decodeToString()
}
}
// Bila ingin interop dengan Java yang baik, tambahkan @Throws secara eksplisit.
@Throws(IOException::class)
fun bacaFileUntukJava(path: String): String {
val stream = FileInputStream(path)
return stream.use { it.readBytes().decodeToString() }
}
fun tangkapIOException(path: String): String {
return try {
bacaFileBesar(path)
} catch (e: IOException) {
"[tangkap] ${e::class.simpleName}: ${e.message}"
} catch (e: RuntimeException) {
"[tangkap] ${e::class.simpleName}: ${e.message}"
}
}
fun lemparDenganAtauTanpaThrows(): String {
// Tidak ada perbedaan perilaku runtime antara function yang dianotasi @Throws
// dan yang tidak. Perbedaannya hanya di signature bytecode yang dilihat Java.
return "Perilaku runtime identik; @Throws hanya memengaruhi signature bytecode untuk Java."
}
fun bandingkanCheckedUnchecked(): String {
val sb = StringBuilder()
sb.appendLine("Kategori exception di JVM:")
sb.appendLine("- Checked (di Java): turunan Exception selain RuntimeException, wajib dideklarasikan.")
sb.appendLine("- Unchecked (di Java): turunan RuntimeException atau Error, bebas dari deklarasi.")
sb.appendLine("- Kotlin : SEMUA exception diperlakukan unchecked (tidak ada `throws` wajib).")
return sb.toString()
}
fun main() {
println("=== Tangkap IOException dari bacaFileBesar ===")
println(tangkapIOException("/file/tidak/ada.txt"))
println()
println("=== Catatan @Throws untuk interop Java ===")
println(lemparDenganAtauTanpaThrows())
println()
println("=== Ringkasan kategori ===")
println(bandingkanCheckedUnchecked())
println()
println("=== Demo lempar turunan Exception non-RuntimeException ===")
// java.text.ParseException adalah checked di Java; di Kotlin kita tangkap bebas.
try {
throw java.text.ParseException("parse gagal di posisi 5", 5)
} catch (e: java.text.ParseException) {
println("tangkap ${e::class.simpleName}: ${e.message}, errorOffset=${e.errorOffset}")
}
}Output yang Dihasilkan:
=== Tangkap IOException dari bacaFileBesar ===
[tangkap] FileNotFoundException: /file/tidak/ada.txt (No such file or directory)
=== Catatan @Throws untuk interop Java ===
Perilaku runtime identik; @Throws hanya memengaruhi signature bytecode untuk Java.
=== Ringkasan kategori ===
Kategori exception di JVM:
- Checked (di Java): turunan Exception selain RuntimeException, wajib dideklarasikan.
- Unchecked (di Java): turunan RuntimeException atau Error, bebas dari deklarasi.
- Kotlin : SEMUA exception diperlakukan unchecked (tidak ada `throws` wajib).
=== Demo lempar turunan Exception non-RuntimeException ===
tangkap ParseException: parse gagal di posisi 5, errorOffset=5Mari kita amati bagian-bagian penting dari kode di atas:
bacaFileBesar: Tidak adathrowsdi signature Kotlin, meskipun konstruktorFileInputStreammelemparFileNotFoundException(checked di Java). Pemanggil dapat menangkapIOExceptionbila perlu, tetapi tidak dipaksa.bacaFileUntukJava: Diberi anotasi@Throws(IOException::class). Perilaku runtime identik denganbacaFileBesar, namun bytecode yang dihasilkan memiliki signaturethrows IOExceptiondi file.class. Compiler Java yang memanggil function ini dari kode Java akan mengenali dan memperlakukannya sebagai checked.tangkapIOException: Membuktikan bahwa exception daribacaFileBesardapat ditangkap secara bebas. Bila path tidak ada,FileNotFoundExceptionmuncul dan ditangkap oleh cabangIOException(karenaFileNotFoundExceptionadalah subkelasnya).bandingkanCheckedUnchecked: Merangkum tiga kategori: checked di Java, unchecked di Java, dan Kotlin yang menyatukan keduanya sebagai unchecked.ParseExceptiondimain:java.text.ParseExceptionadalah checked di Java, namun Kotlin mengizinkan kita melempar dan menangkap tanpa deklarasi. FielderrorOffsetdapat diakses langsung sebagai konteks tambahan.
Tabel Perbandingan #
| Aspek | Java | Kotlin |
|---|---|---|
Turunan RuntimeException | Unchecked (bebas) | Unchecked |
Turunan Exception non-Runtime | Checked (wajib deklarasi) | Unchecked (bebas) |
Error | Unchecked | Unchecked |
Keyword throws di signature | Wajib untuk checked | Tidak ada keyword throws |
Anotasi @Throws | Tidak ada (pakai keyword throws) | Opsional, untuk interop Java |
| Pemanggil wajib tangkap? | Ya untuk checked | Tidak, semua bebas |
Trade-off Tanpa Checked Exception #
Menghapus checked exception memberi beberapa keuntungan: signature function lebih ringkas, lambda lebih mudah ditulis (tidak perlu membungkus checked exception), dan refactoring tidak tersekat oleh hierarki exception. Namun, trade-off ini menuntut disiplin yang lebih tinggi dari pengembang.
Tanpa deklarasi throws, pemanggil tidak punya cara otomatis untuk mengetahui exception apa yang mungkin dilempar. Solusinya adalah dokumentasi KDoc eksplisit:
/**
* Membaca konfigurasi dari path yang diberikan.
*
* @return Isi file konfigurasi sebagai string.
* @throws IOException bila file tidak dapat dibaca atau tidak ditemukan.
* @throws IllegalStateException bila format konfigurasi tidak valid.
*/
fun bacaKonfigurasi(path: String): String { ... }Dengan KDoc yang lengkap, pemanggil tahu kapan harus menangkap tanpa mengandalkan compiler.
Kapan @Throws Diperlukan #
Anotasi @Throws hanya mempengaruhi signature bytecode yang dilihat Java. Kotlin murni tidak peduli. Gunakan @Throws bila kode Kotlin dipanggil dari Java dan Anda ingin compiler Java memperlakukan exception sebagai checked, atau bila library publik dipakai lintas bahasa.
Referensi Lanjutan #
- Exceptions — penjelasan resmi bahwa semua exception Kotlin bersifat unchecked
- Throwable API reference
— API reference
Throwablesebagai akar hierarki exception
Penutup #
Kotlin menyatukan checked dan unchecked menjadi satu kategori: unchecked. Signature function lebih ringkas dan refactoring lebih luwes, namun disiplin dokumentasi menjadi pengganti peran compiler. Untuk interop Java, anotasi @Throws tetap berguna agar konsumen Java memperlakukan exception sesuai konvensinya. Berikutnya, kita akan melihat alternatif lain untuk merepresentasikan kegagalan: tipe Result<T> yang memuat nilai sukses atau kegagalan secara eksplisit tanpa melempar exception.