Dalam satu perjalanan keluar dari try-with-resources, dua kegagalan bisa terjadi hampir bersamaan: badan try melempar exception, lalu saat JVM menutup resource, method close() ikut melempar exception. Aturan dasarnya tegas — exception dari badan try berkuasa sebagai sinyal utama — tetapi menyingkirkan kegagalan penutupan berarti membuang informasi penting: sumber daya yang gagal ditutup bisa menjadi resource leak. Java menyelesaikan ketegangan ini dengan suppressed exception: kegagalan penutupan tidak dibuang, melainkan disimpan di belakang exception utama dan bisa dibaca lewat getSuppressed(). Pada Chapter ini kita buktikan mekanismenya secara nyata, mulai dari masalah yang memotivasinya.
Dua Kegagalan dalam Satu Perjalanan #
Sebelum try-with-resources ada, penutupan manual di finally menyimpan jebakan serius. Jika exception dari finally (misalnya dari close()) terbang saat exception badan try sedang merambat, exception dari finally menimpa exception yang lebih dulu terjadi:
class SaluranLama {
void tutup() {
throw new IllegalStateException("gagal menutup saluran");
}
}
public class TanpaSuppressed {
public static void main(String[] args) {
try {
System.out.println("mulai mengirim");
throw new ArithmeticException("badan gagal");
} finally {
SaluranLama saluran = new SaluranLama();
saluran.tutup(); // exception dari finally menimpa exception badan try
}
}
}Baris mulai mengirim tercetak, lalu program berhenti dengan exit code 1. Default handler JVM hanya melaporkan Exception in thread "main" java.lang.IllegalStateException: gagal menutup saluran beserta stack trace-nya — dan ArithmeticException: badan gagal tidak muncul di mana pun. Sinyal yang lebih dulu terjadi hilang tertimpa; pemelihara program tidak akan pernah tahu bahwa kegagalan aslinya berbeda. Masalah “dua exception, satu tempat” inilah yang diselesaikan mekanisme suppressed.
Penyelamatan try-with-resources #
try-with-resources membalik keputusan tersebut: exception dari badan try tetap menjadi sinyal utama, dan exception dari close() disimpan sebagai suppressed exception di dalam objek exception utama. Keduanya utuh, urutannya jelas. Mari kita buktikan dengan resource yang sengaja gagal ditutup:
class SaluranKirim implements AutoCloseable {
private final String nama;
SaluranKirim(String nama) {
this.nama = nama;
}
@Override
public void close() {
System.out.println("menutup " + nama);
throw new IllegalStateException("gagal menutup " + nama);
}
}
public class SuppressedDemo {
public static void main(String[] args) {
try (SaluranKirim saluran = new SaluranKirim("saluran-1")) {
throw new ArithmeticException("badan try gagal lebih dulu");
} catch (Exception utama) {
System.out.println("utama : " + utama.getClass().getSimpleName()
+ " -> " + utama.getMessage());
for (Throwable t : utama.getSuppressed()) {
System.out.println("tertindih : " + t.getClass().getSimpleName()
+ " -> " + t.getMessage());
}
}
}
}Output yang Dihasilkan:
menutup saluran-1
utama : ArithmeticException -> badan try gagal lebih dulu
tertindih : IllegalStateException -> gagal menutup saluran-1Mari kita bedah urutannya baris demi baris:
- Baris 20 (
throw new ArithmeticException(...)): badantrygagal lebih dulu. Exception ini menjadi kandidat sinyal utama yang akan merambat keluar. - Baris 5-6 (
close()mencetak lalu melempar): JVM tetap menjalankan penutupan — keluaranmenutup saluran-1terbukti tercetak. Ketikaclose()melemparIllegalStateException, JVM tidak meneruskannya sebagai exception kedua yang menimpa; JVM memanggiladdSuppressed()pada exception utama dan menitipkan kegagalan penutupan di sana. - Baris 22 (
catch (Exception utama)): yang sampai kecatchhanyalahArithmeticException— satu sinyal, sesuai aturan. Barisutama : ArithmeticException -> badan try gagal lebih dulumembuktikannya. - Baris 25 (
utama.getSuppressed()): method ini mengembalikan arrayThrowableberisi seluruh exception yang tertindih. Loop mencetak satu anggota:IllegalStateException -> gagal menutup saluran-1. Tidak ada informasi yang hilang.
Saat Hanya close yang Gagal #
Bagaimana jika badan try sukses dan hanya close() yang gagal? Mekanisme suppressed tidak dibutuhkan — tidak ada exception lain yang harus dilindungi. Exception dari close() langsung menjadi sinyal utama:
class SumberBermasalah implements AutoCloseable {
@Override
public void close() {
throw new IllegalStateException("gagal menutup sumber");
}
}
public class HanyaCloseGagal {
public static void main(String[] args) {
try (SumberBermasalah sumber = new SumberBermasalah()) {
System.out.println("badan try selesai tanpa kegagalan");
} catch (Exception e) {
System.out.println("utama : " + e.getClass().getSimpleName()
+ " -> " + e.getMessage());
System.out.println("tertindih : " + e.getSuppressed().length + " exception");
}
}
}Output yang Dihasilkan:
badan try selesai tanpa kegagalan
utama : IllegalStateException -> gagal menutup sumber
tertindih : 0 exceptionKeluarannya menegaskan aturan tersebut: IllegalStateException -> gagal menutup sumber tampil sebagai exception utama, dan getSuppressed() mengembalikan array kosong — angka 0 tercetak karena tidak ada kegagalan lain yang tertindih.
Satu catatan penutup tentang method addSuppressed(): method ini boleh dipanggil manual pada objek Throwable mana pun, dan itulah yang dilakukan JVM di balik layar. Sebelum mekanisme ini ada (Java 6 dan sebelumnya), pustaka lama terpaksa memilih satu exception dan kehilangan sisanya — warisan yang masih terasa pada kode yang memakai pola finally manual.
Kita kini telah melihat seluruh spektrum Throwable yang bisa ditangani: dari Exception sampai mekanisme suppressed. Masih ada satu cabang yang sengaja kita tunda sejak Chapter hierarki: Error — kegagalan level JVM yang berdiri di luar semua ini. Chapter berikutnya menempatkan Error berhadapan dengan Exception, dan menjelaskan mengapa StackOverflowError sebaiknya tidak ditangkap begitu saja.
Referensi #
- Class Throwable pada API java.lang
- Interface AutoCloseable pada API java.lang
- Oracle: try-with-resources di dev.java
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
HanyaCloseGagal.java
Unduh berkasclass SumberBermasalah implements AutoCloseable {
@Override
public void close() {
throw new IllegalStateException("gagal menutup sumber");
}
}
public class HanyaCloseGagal {
public static void main(String[] args) {
try (SumberBermasalah sumber = new SumberBermasalah()) {
System.out.println("badan try selesai tanpa kegagalan");
} catch (Exception e) {
System.out.println("utama : " + e.getClass().getSimpleName()
+ " -> " + e.getMessage());
System.out.println("tertindih : " + e.getSuppressed().length + " exception");
}
}
}
SuppressedDemo.java
Unduh berkasclass SaluranKirim implements AutoCloseable {
private final String nama;
SaluranKirim(String nama) {
this.nama = nama;
}
@Override
public void close() {
System.out.println("menutup " + nama);
throw new IllegalStateException("gagal menutup " + nama);
}
}
public class SuppressedDemo {
public static void main(String[] args) {
try (SaluranKirim saluran = new SaluranKirim("saluran-1")) {
throw new ArithmeticException("badan try gagal lebih dulu");
} catch (Exception utama) {
System.out.println("utama : " + utama.getClass().getSimpleName()
+ " -> " + utama.getMessage());
for (Throwable t : utama.getSuppressed()) {
System.out.println("tertindih : " + t.getClass().getSimpleName()
+ " -> " + t.getMessage());
}
}
}
}
TanpaSuppressed.java
Unduh berkasclass SaluranLama {
void tutup() {
throw new IllegalStateException("gagal menutup saluran");
}
}
public class TanpaSuppressed {
public static void main(String[] args) {
try {
System.out.println("mulai mengirim");
throw new ArithmeticException("badan gagal");
} finally {
SaluranLama saluran = new SaluranLama();
saluran.tutup(); // exception dari finally menimpa exception badan try
}
}
}