Menangkap exception baru menyelesaikan separuh masalah: bagaimana dengan pekerjaan rumah yang harus berjalan apa pun yang terjadi? Berkas yang sudah dibuka harus ditutup, koneksi harus dilepas, dan status sementara harus dipulihkan — baik ketika blok try selesai mulus, maupun ketika exception sedang melewati method tersebut. Konstruksi finally hadir untuk kebutuhan itu: blok yang dijamin dieksekusi pada semua jalur keluar dari try. Pada Chapter ini kita buktikan jaminan tersebut lewat berbagai skenario, lalu kita lihat pola pembersihan klasik — beserta satu jebakan lama yang wajib dihindari.

Blok finally Selalu Berjalan #

Blok finally ditulis setelah blok catch terakhir (atau langsung setelah try tanpa catch). JVM menjalankan blok tersebut pada setiap jalur keluar dari try: eksekusi normal, exception yang tertangkap, exception yang tidak tertangkap dan terus merambat, sampai statement return dari dalam try. Program berikut menguji keempat jalur tersebut sekaligus:

public class FinallySelaluJalan {
    static void uji(String skenario) {
        System.out.println("--- " + skenario);
        try {
            switch (skenario) {
                case "normal" -> System.out.println("try selesai tanpa kegagalan");
                case "tertangkap" -> throw new NumberFormatException("abc");
                case "kembali" -> {
                    System.out.println("try melakukan return");
                    return;
                }
                default -> throw new IllegalStateException("tidak ditangkap di sini");
            }
        } catch (NumberFormatException e) {
            System.out.println("catch menangani: " + e.getMessage());
        } finally {
            System.out.println("finally berjalan");
        }
        System.out.println("setelah try-catch-finally");
    }

    public static void main(String[] args) {
        uji("normal");
        uji("tertangkap");
        uji("kembali");
        try {
            uji("lepas");
        } catch (IllegalStateException e) {
            System.out.println("main menangkap: " + e.getMessage());
        }
    }
}

Output yang Dihasilkan:

text
--- normal
try selesai tanpa kegagalan
finally berjalan
setelah try-catch-finally
--- tertangkap
catch menangani: abc
finally berjalan
setelah try-catch-finally
--- kembali
try melakukan return
finally berjalan
--- lepas
finally berjalan
main menangkap: tidak ditangkap di sini

Baris finally berjalan muncul di keempat skenario — itulah jaminan intinya. Mari kita bedah per skenario:

  1. Skenario normal: blok try tuntas tanpa kegagalan, finally berjalan, lalu statement setelah konstruksi dieksekusi.
  2. Skenario tertangkap: NumberFormatException tertangani oleh catch, finally tetap berjalan setelah catch selesai, dan eksekusi lanjut normal.
  3. Skenario kembali: statement return di dalam try hendak keluar dari method, tetapi JVM menahan keluar tersebut — finally dieksekusi lebih dulu, baru method benar-benar kembali. Perhatikan bahwa baris setelah try-catch-finally tidak tercetak karena return sudah menutup method.
  4. Skenario lepas: IllegalStateException tidak tertangkap oleh catch yang hanya menerima NumberFormatException. Exception tersebut keluar dari method uji — dan finally tetap dieksekusi sebelum keluar terjadi. Baris setelah try-catch-finally juga tidak tercetak di skenario ini, dan main-lah yang akhirnya menangkap exception tersebut.

Pola Pembersihan Klasik #

Penggunaan finally paling klasik adalah melepas resource secara manual. Variabel sumber dideklarasikan sebelum try, sumber dipakai di dalam try, dan penutupannya dilakukan di finally sehingga berjalan baik saat sukses maupun saat gagal:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;

public class BersihkanSumber {
    static void bacaData() throws IOException {
        BufferedReader pembaca = new BufferedReader(new StringReader("satu\ndua"));

        try {
            System.out.println("baris 1: " + pembaca.readLine());
            System.out.println("baris 2: " + pembaca.readLine());
        } finally {
            System.out.println("menutup pembaca");
            pembaca.close();
        }
    }

    public static void main(String[] args) throws IOException {
        bacaData();
    }
}

Output yang Dihasilkan:

text
baris 1: satu
baris 2: dua
menutup pembaca

Urutan keluarannya: dua baris data dari dalam try, lalu menutup pembaca dari finally. Jika readLine() melempar IOException, penutupan tetap terjadi sebelum exception keluar dari method — itulah gunanya pola ini. Tetapi pola manual punya beban yang terasa: variabel harus dideklarasikan di luar try agar terlihat di finally, close() sendiri melempar IOException yang harus dinyatakan, dan bila jumlah sumber bertambah, kode pembersihan menumpuk. Chapter try-with-resources memperkenalkan bentuk ringkas yang menghapus seluruh beban tersebut.

Bahaya return di Dalam finally #

Blok finally yang mengandung return memiliki kekuatan yang berbahaya: statement tersebut menggantikan exception yang sedang merambat. Sinyal kegagalan yang seharusnya sampai ke pemanggil berakhir begitu saja, seolah tidak pernah ada:

public class FinallyMenelanReturn {
    static int bacaSaldo() {
        try {
            throw new IllegalStateException("sinyal kegagalan penting");
        } finally {
            return 0; // statement ini menelan exception yang sedang terbang
        }
    }

    public static void main(String[] args) {
        System.out.println("saldo: " + bacaSaldo());
        System.out.println("program selesai tanpa jejak kegagalan");
    }
}

Output yang Dihasilkan:

text
saldo: 0
program selesai tanpa jejak kegagalan

Method bacaSaldo() melempar IllegalStateException di baris pertama try — tetapi keluaran program hanya menampilkan saldo: 0 tanpa jejak kegagalan apa pun. Statement return 0 di dalam finally memutus jalur keluar method dan membuang exception yang sedang terbang; pemanggil menerima nilai 0 dan tidak pernah tahu bahwa pembacaan saldo gagal. Bayangkan nilai 0 itu mengalir ke perhitungan pajak. Perilaku serupa juga terjadi bila finally melempar exception baru: exception lama tertimpa dan hilang dari catatan.

Aturan praktisnya ringkas: finally untuk membersihkan, catch untuk menanggapi, dan keduanya tidak boleh mengubah sinyal kegagalan yang sedang berjalan. Dengan disiplin itu, kita siap melangkah ke bentuk pembersihan modern — try-with-resources — yang memanggil close() secara otomatis dan berurutan tanpa satu baris pun di finally.

Referensi #

Berkas Sumber

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

BersihkanSumber.java

Unduh berkas
java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;

public class BersihkanSumber {
    static void bacaData() throws IOException {
        BufferedReader pembaca = new BufferedReader(new StringReader("satu\ndua"));

        try {
            System.out.println("baris 1: " + pembaca.readLine());
            System.out.println("baris 2: " + pembaca.readLine());
        } finally {
            System.out.println("menutup pembaca");
            pembaca.close();
        }
    }

    public static void main(String[] args) throws IOException {
        bacaData();
    }
}

FinallyMenelanReturn.java

Unduh berkas
java
public class FinallyMenelanReturn {
    static int bacaSaldo() {
        try {
            throw new IllegalStateException("sinyal kegagalan penting");
        } finally {
            return 0; // statement ini menelan exception yang sedang terbang
        }
    }

    public static void main(String[] args) {
        System.out.println("saldo: " + bacaSaldo());
        System.out.println("program selesai tanpa jejak kegagalan");
    }
}

FinallySelaluJalan.java

Unduh berkas
java
public class FinallySelaluJalan {
    static void uji(String skenario) {
        System.out.println("--- " + skenario);
        try {
            switch (skenario) {
                case "normal" -> System.out.println("try selesai tanpa kegagalan");
                case "tertangkap" -> throw new NumberFormatException("abc");
                case "kembali" -> {
                    System.out.println("try melakukan return");
                    return;
                }
                default -> throw new IllegalStateException("tidak ditangkap di sini");
            }
        } catch (NumberFormatException e) {
            System.out.println("catch menangani: " + e.getMessage());
        } finally {
            System.out.println("finally berjalan");
        }
        System.out.println("setelah try-catch-finally");
    }

    public static void main(String[] args) {
        uji("normal");
        uji("tertangkap");
        uji("kembali");
        try {
            uji("lepas");
        } catch (IllegalStateException e) {
            System.out.println("main menangkap: " + e.getMessage());
        }
    }
}