Interface Runnable adalah pintu masuk paling sederhana ke dunia konkurensi Java, tetapi kontraknya ketat: method run() tidak mengembalikan nilai dan tidak boleh melempar checked exception. Padahal tugas nyata sering kali harus mengembalikan hasil — total penjualan, hasil parsing, jumlah baris berkas — dan gagal dengan exception yang ingin kita tangani dengan rapi. Untuk kebutuhan itu, pustaka java.util.concurrent menyediakan interface Callable<T>. Chapter ini membandingkan kedua kontrak tersebut dan memperkenalkan FutureTask sebagai jembatan agar Callable bisa dijalankan oleh Thread biasa.

Batasan Runnable #

Kedeklarasian Runnable terlihat seperti ini:

java
public interface Runnable {
    public abstract void run();
}

Dua batasan langsung mengikuti bentuk tersebut. Pertama, void berarti body thread tidak punya jalur resmi untuk mengembalikan nilai; trik umum seperti mengisi slot array bersama (seperti contoh pada Chapter Membuat Thread) berfungsi, tetapi bersifat manual — kita yang menjamin slot terisi sebelum dibaca. Kedua, run() tidak mendeklarasikan checked exception, sehingga exception bertipe IOException atau SQLException di dalam body terpaksa harus ditangkap di dalam run() sendiri atau dibungkus sebagai unchecked exception.

Callable Mengembalikan Nilai #

Interface Callable<T> memperbaiki kedua batasan itu:

java
public interface Callable<V> {
    V call() throws Exception;
}

Method call() mengembalikan nilai bertipe V (parameter tipe generic) dan boleh melempar exception apa pun karena deklarasinya menulis throws Exception. Sebuah Callable<Integer> yang gagal memuat data cukup melempar IllegalStateException dari dalam call(); pemanggil yang mengambil hasil akan menerima kegagalan tersebut dalam bentuk terbungkus rapi, seperti kita lihat pada bagian akhir Chapter ini.

Ada satu hal teknis yang perlu dicermati: constructor Thread hanya menerima Runnable, bukan Callable. Kedua interface tersebut tidak berhubungan pewarisan — Callable bukan turunan Runnable.

FutureTask sebagai Jembatan #

Class FutureTask<V> mengimplementasikan interface RunnableFuture, yang mewarisi Runnable sekaligus Future<V>. Karena FutureTask adalah Runnable, objek tersebut bisa dimasukkan ke constructor Thread; karena sekaligus Future, objek yang sama menyediakan method get() untuk mengambil hasil tugas. Program berikut membandingkan kedua jalur secara berdampingan:

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class RunnableVsCallable {
    public static void main(String[] args) throws Exception {
        // ---- Runnable: run() tidak mengembalikan nilai ----
        long[] hasilRunnable = new long[1];
        Runnable tugasRunnable = () -> {
            long total = 0;
            for (int i = 1; i <= 100; i++) {
                total += i;
            }
            hasilRunnable[0] = total; // hasil diselundupkan lewat array bersama
        };
        Thread t1 = new Thread(tugasRunnable, "runnable");
        t1.start();
        t1.join();

        // ---- Callable: call() mengembalikan nilai ----
        Callable<Long> tugasCallable = () -> {
            long hasil = 1;
            for (int i = 1; i <= 10; i++) {
                hasil *= i;
            }
            return hasil; // nilai dikembalikan langsung ke pemanggil
        };
        FutureTask<Long> tugas = new FutureTask<>(tugasCallable);
        Thread t2 = new Thread(tugas, "callable");
        t2.start();
        t2.join();

        Long hasilCallable = tugas.get(); // mengambil hasil setelah tugas selesai
        System.out.println("Runnable menjumlahkan 1..100 : " + hasilRunnable[0]);
        System.out.println("Callable menghitung 10!       : " + hasilCallable);
    }
}

Output yang Dihasilkan:

text
Runnable menjumlahkan 1..100 : 5050
Callable menghitung 10!       : 3628800

Mari kita telaah baris-baris kode di atas:

  1. Baris 7 (long[] hasilRunnable = new long[1];): menyiapkan wadah manual untuk jalur Runnable. Pola ini adalah satu-satunya cara run() mengirim hasil keluar karena tanda tangan void.
  2. Baris 17 (hasilRunnable[0] = total;): thread runnable menulis hasil ke array. Pemanggil harus menunggu t1.join() sebelum membaca nilai tersebut agar tidak membaca data yang belum terisi.
  3. Baris 21 (Callable<Long> tugasCallable = () -> {...}): lambda untuk Callable boleh mengembalikan nilai (return hasil;) karena tipe fungsionalnya call(), bukan run().
  4. Baris 30 (FutureTask<Long> tugas = new FutureTask<>(tugasCallable);): membungkus Callable menjadi objek yang sekaligus Runnable dan Future — inilah jembatan yang membuat Callable bisa dimasukkan ke constructor Thread.
  5. Baris 36 (Long hasilCallable = tugas.get();): mengambil hasil. Karena t2.join() sudah kembali, tugas pasti sudah selesai sehingga get() langsung mengembalikan nilai tanpa menunggu.
  6. Baris 37-38: kedua hasil dicetak dari thread main setelah kedua thread bergabung, sehingga urutan keluaran tetap terprediksi.

Ketika Callable Gagal #

Nilai balik bukan satu-satunya hal yang bisa dikirim Callable; kegagalan juga. Method get() dari FutureTask akan melempar ExecutionException bila tugas selesai karena exception di dalam call(). Exception aslinya bisa diambil lewat getCause():

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableMelemparException {
    public static void main(String[] args) throws InterruptedException {
        Callable<Integer> pembacaKonfigurasi = () -> {
            String isi = null; // simulasi: konfigurasi gagal dimuat
            if (isi == null) {
                throw new IllegalStateException("berkas konfigurasi tidak ditemukan");
            }
            return Integer.parseInt(isi);
        };

        FutureTask<Integer> tugas = new FutureTask<>(pembacaKonfigurasi);
        Thread t = new Thread(tugas, "pembaca-konfigurasi");
        t.start();
        t.join(); // tunggu tugas selesai (selesai dengan kegagalan pun tetap selesai)

        try {
            tugas.get(); // tidak pernah berhasil karena tugas gagal
            System.out.println("baris ini tidak pernah tercetak");
        } catch (ExecutionException e) {
            Throwable penyebab = e.getCause();
            System.out.println("get() melempar ExecutionException");
            System.out.println("penyebabnya    : " + penyebab.getClass().getSimpleName());
            System.out.println("pesan penyebab : " + penyebab.getMessage());
        }
    }
}

Output yang Dihasilkan:

text
get() melempar ExecutionException
penyebabnya    : IllegalStateException
pesan penyebab : berkas konfigurasi tidak ditemukan

Alurnya bekerja seperti ini: exception yang lempar call() ditangkap framework, tugas ditandai gagal, lalu get() melempar ExecutionException yang membungkus exception asli sebagai cause. Dengan pola tersebut, kegagalan di thread pekerja tetap bisa ditangani di thread pemanggil menggunakan try-catch biasa — sesuatu yang mustahil dilakukan run() karena exception di dalam thread lain tidak pernah menjalar ke thread pemanggil.

Perbandingan Ringkas #

AspekRunnableCallable<V>
Method kontrakvoid run()V call() throws Exception
Nilai balikTidak ada — butuh wadah manual (array, queue)Ada, bertipe generic V
Checked exceptionTidak boleh lempar dari run()Boleh lempar exception apa pun
Diterima constructor ThreadYaTidak — perlu FutureTask atau executor
Pengambilan hasilManual + join()get() (atau get(timeout))
Dukungan lambdaYa (functional interface)Ya (functional interface)

FutureTask dan get() yang menunggu hingga hasil tersedia sebenarnya adalah pengantar menuju konsep yang lebih besar: Future. Sebelum sampai ke sana, kita perlu memahami keadaan-keadaan yang dilalui sebuah thread dari lahir sampai selesai — itu topik Chapter Thread Lifecycle berikutnya.

Referensi #

Berkas Sumber

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

CallableMelemparException.java

Unduh berkas
java
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableMelemparException {
    public static void main(String[] args) throws InterruptedException {
        Callable<Integer> pembacaKonfigurasi = () -> {
            String isi = null; // simulasi: konfigurasi gagal dimuat
            if (isi == null) {
                throw new IllegalStateException("berkas konfigurasi tidak ditemukan");
            }
            return Integer.parseInt(isi);
        };

        FutureTask<Integer> tugas = new FutureTask<>(pembacaKonfigurasi);
        Thread t = new Thread(tugas, "pembaca-konfigurasi");
        t.start();
        t.join(); // tunggu tugas selesai (selesai dengan kegagalan pun tetap selesai)

        try {
            tugas.get(); // tidak pernah berhasil karena tugas gagal
            System.out.println("baris ini tidak pernah tercetak");
        } catch (ExecutionException e) {
            Throwable penyebab = e.getCause();
            System.out.println("get() melempar ExecutionException");
            System.out.println("penyebabnya    : " + penyebab.getClass().getSimpleName());
            System.out.println("pesan penyebab : " + penyebab.getMessage());
        }
    }
}

RunnableVsCallable.java

Unduh berkas
java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class RunnableVsCallable {
    public static void main(String[] args) throws Exception {
        // ---- Runnable: run() tidak mengembalikan nilai ----
        long[] hasilRunnable = new long[1];
        Runnable tugasRunnable = () -> {
            long total = 0;
            for (int i = 1; i <= 100; i++) {
                total += i;
            }
            hasilRunnable[0] = total; // hasil diselundupkan lewat array bersama
        };
        Thread t1 = new Thread(tugasRunnable, "runnable");
        t1.start();
        t1.join();

        // ---- Callable: call() mengembalikan nilai ----
        Callable<Long> tugasCallable = () -> {
            long hasil = 1;
            for (int i = 1; i <= 10; i++) {
                hasil *= i;
            }
            return hasil; // nilai dikembalikan langsung ke pemanggil
        };
        FutureTask<Long> tugas = new FutureTask<>(tugasCallable);
        Thread t2 = new Thread(tugas, "callable");
        t2.start();
        t2.join();

        Long hasilCallable = tugas.get(); // mengambil hasil setelah tugas selesai
        System.out.println("Runnable menjumlahkan 1..100 : " + hasilRunnable[0]);
        System.out.println("Callable menghitung 10!       : " + hasilCallable);
    }
}