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:
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:
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:
Runnable menjumlahkan 1..100 : 5050
Callable menghitung 10! : 3628800Mari kita telaah baris-baris kode di atas:
- Baris 7 (
long[] hasilRunnable = new long[1];): menyiapkan wadah manual untuk jalurRunnable. Pola ini adalah satu-satunya cararun()mengirim hasil keluar karena tanda tanganvoid. - Baris 17 (
hasilRunnable[0] = total;): threadrunnablemenulis hasil ke array. Pemanggil harus menunggut1.join()sebelum membaca nilai tersebut agar tidak membaca data yang belum terisi. - Baris 21 (
Callable<Long> tugasCallable = () -> {...}): lambda untukCallableboleh mengembalikan nilai (return hasil;) karena tipe fungsionalnyacall(), bukanrun(). - Baris 30 (
FutureTask<Long> tugas = new FutureTask<>(tugasCallable);): membungkusCallablemenjadi objek yang sekaligusRunnabledanFuture— inilah jembatan yang membuatCallablebisa dimasukkan ke constructorThread. - Baris 36 (
Long hasilCallable = tugas.get();): mengambil hasil. Karenat2.join()sudah kembali, tugas pasti sudah selesai sehinggaget()langsung mengembalikan nilai tanpa menunggu. - Baris 37-38: kedua hasil dicetak dari thread
mainsetelah 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:
get() melempar ExecutionException
penyebabnya : IllegalStateException
pesan penyebab : berkas konfigurasi tidak ditemukanAlurnya 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 #
| Aspek | Runnable | Callable<V> |
|---|---|---|
| Method kontrak | void run() | V call() throws Exception |
| Nilai balik | Tidak ada — butuh wadah manual (array, queue) | Ada, bertipe generic V |
| Checked exception | Tidak boleh lempar dari run() | Boleh lempar exception apa pun |
Diterima constructor Thread | Ya | Tidak — perlu FutureTask atau executor |
| Pengambilan hasil | Manual + join() | get() (atau get(timeout)) |
| Dukungan lambda | Ya (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 #
- Oracle: Interface Callable (Java API Specification)
- Oracle: Class FutureTask (Java API Specification)
- Oracle: Package java.util.concurrent (Summary)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
CallableMelemparException.java
Unduh berkasimport 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 berkasimport 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);
}
}