Tampilan
Kenapa penting
Aplikasi backend terus-menerus membuka resource: koneksi database, file upload, koneksi HTTP ke layanan lain, socket, atau lock. Resource seperti ini terbatas — database hanya mengizinkan sekian koneksi, sistem operasi hanya mengizinkan sekian file terbuka. Satu method yang lupa menutup koneksi tidak terasa saat dites, tetapi di production, setelah ribuan request, connection pool habis dan aplikasi berhenti melayani dengan error seperti "too many open files" atau "timeout waiting for connection". Try-with-resources adalah cara Java memastikan resource selalu ditutup, apa pun yang terjadi — dan ini salah satu hal pertama yang diperiksa saat code review.
Konsep inti
Cara lama: try/finally dan masalahnya
Sebelum Java 7, menutup resource dilakukan manual di finally, lengkap dengan cek null dan try/catch tambahan untuk close() itu sendiri:
java
class Connection {
private final String name;
Connection(String name) {
this.name = name;
System.out.println("buka " + name);
}
void query(String sql) {
throw new IllegalStateException("query gagal: " + sql);
}
void close() {
System.out.println("tutup " + name);
throw new IllegalStateException("gagal menutup " + name);
}
}
public class OldStyle {
public static void main(String[] args) {
Connection connection = null;
try {
connection = new Connection("db-utama");
connection.query("SELECT * FROM orders");
} catch (IllegalStateException e) {
System.out.println("tertangkap: " + e.getMessage());
} finally {
if (connection != null) {
try {
connection.close();
} catch (IllegalStateException e) {
System.out.println("diabaikan: " + e.getMessage());
}
}
}
}
}Output:
text
buka db-utama
tertangkap: query gagal: SELECT * FROM orders
tutup db-utama
diabaikan: gagal menutup db-utamaKodenya benar, tetapi bertele-tele: belasan baris hanya untuk "pakai lalu tutup". Pola ini mudah salah tulis, dan ada jebakan yang lebih halus. Kalau finally tidak membungkus close() dengan try/catch sendiri, exception dari close() menggantikan exception utama:
java
class Connection {
void query() {
throw new IllegalStateException("query gagal");
}
void close() {
throw new IllegalStateException("gagal menutup koneksi");
}
}
public class MaskedError {
static void load() {
Connection connection = new Connection();
try {
connection.query();
} finally {
connection.close();
}
}
public static void main(String[] args) {
try {
load();
} catch (IllegalStateException e) {
System.out.println("yang sampai ke pemanggil: " + e.getMessage());
}
}
}Output: yang sampai ke pemanggil: gagal menutup koneksi. Akar masalahnya — query yang gagal — hilang dari log. Bayangkan men-debug insiden production hanya dengan pesan itu.
Try-with-resources
Sejak Java 7, resource bisa dideklarasikan di dalam tanda kurung setelah try. Java otomatis memanggil close() saat blok selesai — normal, lewat return, maupun karena exception. Syaratnya: tipe resource harus mengimplementasikan interface AutoCloseable, yang hanya punya satu method, close().
java
class AuditSession implements AutoCloseable {
private final String name;
AuditSession(String name) {
this.name = name;
System.out.println("buka " + name);
}
void record(String event) {
System.out.println(" [" + name + "] " + event);
}
@Override
public void close() {
System.out.println("tutup " + name);
}
}
public class CloseOrder {
public static void main(String[] args) {
try (AuditSession db = new AuditSession("koneksi-db");
AuditSession audit = new AuditSession("audit-log")) {
db.record("UPDATE stock");
audit.record("stok diubah");
}
System.out.println("selesai");
}
}Output:
text
buka koneksi-db
buka audit-log
[koneksi-db] UPDATE stock
[audit-log] stok diubah
tutup audit-log
tutup koneksi-db
selesaiBeberapa resource dipisahkan titik koma dan ditutup dalam urutan terbalik: yang terakhir dibuka, pertama ditutup. Ini masuk akal karena resource belakangan sering bergantung pada resource sebelumnya — misalnya ResultSet bergantung pada Statement, yang bergantung pada Connection. Variabel resource otomatis final dan hanya berlaku di dalam blok try.
Bila tipe resource bukan AutoCloseable, compiler menolak — walaupun class itu punya method close():
text
NotCloseable.java:7: error: incompatible types: try-with-resources not applicable to variable type
try (Session session = new Session()) {
^
(Session cannot be converted to AutoCloseable)AutoCloseable vs Closeable
| Interface | Paket | close() boleh melempar | Catatan |
|---|---|---|---|
java.lang.AutoCloseable | java.lang | Exception | induk; dipakai JDBC, lock, resource buatanmu |
java.io.Closeable | java.io | IOException | turunan AutoCloseable; dipakai stream & reader |
Untuk resource buatanmu, implementasikan AutoCloseable dan persempit throws di close() sesuai kebutuhan — atau hilangkan sama sekali bila menutupnya tidak pernah gagal, seperti AuditSession di atas. Usahakan close() aman dipanggil lebih dari sekali (idempotent).
Suppressed exception
Bagaimana bila badan try gagal dan close() juga gagal? Try-with-resources tidak membuang informasi apa pun: exception dari badan try tetap menjadi exception utama, sedangkan exception dari close() ditempelkan sebagai suppressed exception.
java
class FlakyConnection implements AutoCloseable {
void query() {
throw new IllegalStateException("query gagal");
}
@Override
public void close() {
throw new IllegalStateException("gagal menutup koneksi");
}
}
public class Suppressed {
public static void main(String[] args) {
try (FlakyConnection connection = new FlakyConnection()) {
connection.query();
} catch (IllegalStateException e) {
System.out.println("utama: " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("tertekan: " + suppressed.getMessage());
}
}
}
}Output:
text
utama: query gagal
tertekan: gagal menutup koneksiBandingkan dengan MaskedError di atas: kini akar masalah tetap terlihat. Saat stack trace dicetak, exception tambahan muncul dengan label Suppressed: di bawah exception utama — biasakan membacanya di log.
catch dan finally bersama try-with-resources
Try-with-resources boleh diikuti catch dan finally seperti try biasa. Urutannya penting: resource ditutup lebih dulu, baru catch, lalu finally.
java
class Resource implements AutoCloseable {
@Override
public void close() {
System.out.println("2. close");
}
}
public class CatchFinallyOrder {
public static void main(String[] args) {
try (Resource resource = new Resource()) {
System.out.println("1. badan try");
throw new IllegalStateException("gagal");
} catch (IllegalStateException e) {
System.out.println("3. catch: " + e.getMessage());
} finally {
System.out.println("4. finally");
}
}
}Output: 1. badan try, 2. close, 3. catch: gagal, 4. finally. Artinya, di dalam catch kamu tidak bisa lagi memakai resource itu — misalnya untuk rollback transaksi. Logika seperti itu harus ditaruh di dalam badan try.
Membaca teks baris per baris
BufferedReader (sebuah Closeable) membungkus Reader lain dan menyediakan readLine(), yang mengembalikan satu baris tanpa karakter akhir baris, atau null bila data habis. Sumbernya bisa file (Files.newBufferedReader(path)), koneksi jaringan, atau teks biasa lewat StringReader — cocok untuk contoh dan unit test.
java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
public class ReadLines {
public static void main(String[] args) throws IOException {
String text = """
baris pertama
baris ketiga
""";
BufferedReader reader = new BufferedReader(new StringReader(text));
try (reader) { // Java 9+: variabel effectively final
String line;
int number = 0;
while ((line = reader.readLine()) != null) {
number++;
System.out.println(number + ": [" + line + "]");
}
}
}
}Output:
text
1: [baris pertama]
2: []
3: [baris ketiga]Dua hal baru di sini. Pertama, ekspresi (line = reader.readLine()) != null menyimpan baris lalu langsung mengecek null — pola baku pembacaan baris. Kedua, sejak Java 9 header try boleh berisi variabel yang sudah ada, asalkan effectively final (tidak pernah di-assign ulang). Kalau di-assign ulang, compiler menolak dengan variable reader used as a try-with-resources resource neither final nor effectively final.
readLine() dan close() milik BufferedReader melempar IOException, yang merupakan checked exception. Jadi method yang membaca harus menangkapnya atau menulis throws IOException. Lupa keduanya menghasilkan error unreported exception IOException; must be caught or declared to be thrown — perhatikan, pesan itu bahkan menyebut implicit call to close().
Contoh di dunia backend
Fitur "import stok dari CSV" membaca file yang diunggah admin. Baris yang rusak tidak boleh menggagalkan seluruh import — cukup dicatat — tetapi sumber data harus selalu ditutup. Method menerima Reader (bukan nama file) sehingga bisa dipakai untuk file, upload HTTP, maupun unit test.
java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
class TrackingReader extends StringReader {
TrackingReader(String text) {
super(text);
}
@Override
public void close() {
super.close();
System.out.println("(sumber data ditutup)");
}
}
public class StockImport {
static Map<String, Integer> importStock(Reader source, List<String> errors) throws IOException {
Map<String, Integer> stock = new LinkedHashMap<>();
try (BufferedReader reader = new BufferedReader(source)) {
String line;
int lineNumber = 0;
while ((line = reader.readLine()) != null) {
lineNumber++;
if (lineNumber == 1 || line.isBlank()) {
continue; // lewati header & baris kosong
}
String[] parts = line.split(",");
if (parts.length != 2) {
errors.add("baris " + lineNumber + ": format salah");
continue;
}
try {
int quantity = Integer.parseInt(parts[1].strip());
stock.put(parts[0].strip(), quantity);
} catch (NumberFormatException e) {
errors.add("baris " + lineNumber + ": jumlah bukan angka (" + parts[1].strip() + ")");
}
}
}
return stock;
}
public static void main(String[] args) throws IOException {
String csv = """
sku,qty
KOPI-01, 25
TEH-02,abc
GULA-03
SUSU-04,12
""";
List<String> errors = new ArrayList<>();
Map<String, Integer> stock = importStock(new TrackingReader(csv), errors);
System.out.println("berhasil: " + stock);
System.out.println("gagal: " + errors);
}
}Output:
text
(sumber data ditutup)
berhasil: {KOPI-01=25, SUSU-04=12}
gagal: [baris 3: jumlah bukan angka (abc), baris 4: format salah]Perhatikan pembagian tugasnya: NumberFormatException per baris ditangkap di dalam loop karena import boleh lanjut, sedangkan IOException (sumber data putus di tengah jalan) diteruskan ke pemanggil karena import tidak bisa diselesaikan. Dalam kedua kasus, try-with-resources menjamin reader tertutup. Menutup BufferedReader juga menutup Reader yang dibungkusnya — itulah kenapa TrackingReader ikut tercetak. Di Spring Boot, koneksi database dari DataSource, InputStream dari file upload, dan HTTP client dikelola dengan pola yang sama.
Pertanyaan interview
1. Apa itu try-with-resources dan kenapa lebih baik dari try/finally?
Try-with-resources mendeklarasikan resource AutoCloseable di header try dan menutupnya otomatis saat blok selesai, dalam urutan terbalik. Dibanding try/finally manual, kodenya lebih ringkas, tidak bisa lupa menutup, dan bila close() gagal saat sudah ada exception utama, exception dari close() disimpan sebagai suppressed alih-alih menimpa exception utama.
2. Apa itu suppressed exception?
Exception yang terjadi saat menutup resource ketika sudah ada exception lain yang sedang dilempar. Java menempelkannya ke exception utama lewat addSuppressed, dan bisa dibaca dengan getSuppressed(). Di stack trace ia muncul dengan label Suppressed:. Tujuannya agar akar masalah tidak hilang.
3. Apa beda AutoCloseable dan Closeable?
AutoCloseable (java.lang, Java 7) adalah induknya; close()-nya boleh melempar Exception. Closeable (java.io) adalah turunannya untuk I/O; close()-nya hanya melempar IOException dan dianjurkan idempotent. Keduanya bisa dipakai di try-with-resources. Untuk resource buatan sendiri, implementasikan AutoCloseable.
Jebakan umum
- Membuka resource di luar header
trylalu lupa menutupnya di salah satu jalur (returnlebih awal, exception). Deklarasikan resource di headertrysejak dibuat. - Menutup resource di
finallytanpa menangani exceptionclose()→ exception utama hilang. Pakai try-with-resources. - Memakai resource di
catchataufinally. Saat itu resource sudah tertutup. - Menelan
IOExceptiondengancatchkosong. Kegagalan membaca file jadi tidak terlihat; teruskan denganthrowsatau bungkus dengan pesan yang jelas. - Mengabaikan baris
Suppressed:di stack trace. Sering justru di situ informasi tentang koneksi yang gagal ditutup. - Menutup resource milik orang lain. Method yang menerima resource dari luar dan menutupnya bisa mengejutkan pemanggil. Tulis jelas di Javadoc siapa yang bertanggung jawab menutup.
Ringkasan
- Resource (koneksi, file, socket) terbatas dan wajib ditutup; kebocoran baru terasa di production.
try (Resource r = ...) { ... }menutup resource otomatis, juga saatreturnatau exception.- Beberapa resource ditutup dalam urutan terbalik; resource harus
AutoCloseable(atauCloseable). - Exception dari
close()tidak menimpa exception utama — ia menjadi suppressed (getSuppressed()). - Resource ditutup sebelum
catchdanfinally; sejak Java 9 headertryboleh berisi variabel effectively final. BufferedReader.readLine()membaca baris sampainull;IOExceptionadalah checked exception.