Reflection bukan jalur eksekusi murah. Literatur dan pengukuran komunitas umumnya menempatkan operasi reflection berkali lipat lebih lambat daripada pemanggilan langsung — angka yang sering dikutip berkisar 10 sampai 100 kali untuk pemanggilan method di jalur panas, meski angka pastinya bergantung pada versi JVM dan bentuk operasinya. Daripada memercayai angka mentah, lebih penting memahami dari mana biaya itu berasal — begitu sumbernya jelas, strategi menguranginya mengikuti dengan sendirinya.

Tiga Sumber Biaya #

Biaya per operasi reflection berasal dari tiga pekerjaan yang tidak dipikul pemanggilan langsung:

  1. Pemeriksaan akses: setiap invoke, get, atau set memverifikasi ulang bahwa pemanggil boleh mengakses anggota — kecuali setAccessible(true) sudah dipanggil lebih dulu.
  2. Pembungkusan (boxing) argument dan nilai kembalian: signature Method.invoke adalah Object invoke(Object, Object...), sehingga argument primitif wajib dibungkus wrapper dan hasilnya kembali dibuka. Pada loop besar, tekanan alokasi ini terasa juga pada garbage collector.
  3. Penghalang optimasi JIT: call site reflection berbentuk Method.invoke yang dinamis — compiler JIT tidak dapat men-inline-kan method target seperti pemanggilan biasa, sehingga banyak optimasi klasik (inlining, escape analysis) tidak berlaku.

Dua dari tiga sumber di atas bisa dibuktikan tanpa stopwatch — cukup struktur program. Mari kita lihat:

import java.lang.reflect.Method;

class Kalkulator {
    public static int tambah(int a, int b) {
        return a + b;
    }
}

public class StrukturBiaya {
    public static void main(String[] args) throws Exception {
        // Bukti 1: hasil pencarian tidak di-cache JVM — setiap getMethod membuat salinan
        Method m1 = Kalkulator.class.getMethod("tambah", int.class, int.class);
        Method m2 = Kalkulator.class.getMethod("tambah", int.class, int.class);
        System.out.println("Hasil pencarian sama (equals)? " + m1.equals(m2));
        System.out.println("Objek yang sama (==)?          " + (m1 == m2));

        // Bukti 2: argument primitif wajib dibungkus (boxing) ke Object[]
        Method tambah = Kalkulator.class.getMethod("tambah", int.class, int.class);
        tambah.setAccessible(true);
        Object[] argumen = {2, 3};
        System.out.println("Tipe argumen[0]: " + argumen[0].getClass().getSimpleName());

        Object hasil = tambah.invoke(null, argumen);
        System.out.println("Tipe kembalian : " + hasil.getClass().getSimpleName());
        System.out.println("Hasil          : " + hasil);
    }
}

Output yang Dihasilkan:

text
Hasil pencarian sama (equals)? true
Objek yang sama (==)?          false
Tipe argumen[0]: Integer
Tipe kembalian : Integer
Hasil          : 5

Dua baris pertama output adalah bukti sumber biaya pencarian: dua pemanggilan getMethod yang sama persis menghasilkan objek Method yang setara tetapi berbeda (equals bernilai true, == bernilai false). JVM tidak meng-cache hasil pencarian method — setiap getMethod mengulangi penelusuran dan penyalinan metadata. Dua baris berikutnya membuktikan sumber kedua: bilangan 2 dan 3 yang kita tulis sebagai int di signature method harus hidup sebagai Integer di dalam array Object[], dan nilai kembalian int juga kembali sebagai Integer. Sumber ketiga — penghalang JIT — tidak bisa dibuktikan dari struktur; ia adalah konsekuensi bentuk call site yang dinamis.

Mitigasi Klasik #

Tiga kebiasaan murah menutup sebagian besar biaya pada kode reflection yang tidak bisa dihindari:

  • Cache objek Method/Field/Constructor di static final atau Map<String, Method> sejak awal program. Bukti == false di atas menunjukkan pencarian berulang hanya membuang pekerjaan.
  • Panggil setAccessible(true) sekali per objek — tepat setelah pencarian, bukan di dalam loop. Pemeriksaan akses lalu dilewati pada seluruh pemanggilan berikutnya.
  • Pindahkan reflection keluar dari loop ketat. Bila jumlah operasi besar, lakukan pencarian dan pembukaan akses sekali, lalu eksekusi berkali-kali memakai objek yang sama.

Polanya sama dengan memoisasi biasa: kerja mahal (pencarian + pembukaan akses) dikerjakan sekali, kerja berulang (invoke) sesering yang dibutuhkan. Untuk loop yang benar-benar panas, dua kebiasaan itu pun belum menyaingi pemanggilan langsung — call site-nya tetap dinamis — dan di titik itulah Java modern menawarkan jalur lain.

MethodHandles Jalur Modern #

Package java.lang.invoke — diresmikan lewat JSR 292 dan disempurnakan bertahap, termasuk JEP 271 Enhanced MethodHandles — menyediakan MethodHandle: pegangan bertipe ke method atau field yang dibuat sekali lalu dipanggil berkali-kali dengan signature statis. Berbeda dari Method, MethodHandle dirancang agar JIT dapat mengenalinya dan mengoptimasi call site-nya; invokeExact bahkan memaksa signature pemanggilan cocok persis dengan tipe pegangan.

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class ContohMethodHandle {
    static int tambah(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType tipe = MethodType.methodType(int.class, int.class, int.class);
        MethodHandle tambah = lookup.findStatic(ContohMethodHandle.class, "tambah", tipe);

        int pasti = (int) tambah.invokeExact(2, 3);
        System.out.println("invokeExact : " + pasti);

        Object luwes = tambah.invoke(2, 3);
        System.out.println("invoke      : " + luwes);
    }
}

Output yang Dihasilkan:

text
invokeExact : 5
invoke      : 5

Mari kita telaah baris-baris pentingnya:

  1. Baris 13 (MethodHandles.lookup()): objek Lookup adalah gerbang pencarian yang membawa konteks akses kode pemanggil — method privat tetap bisa ditemukan selama berada dalam class yang sama, tanpa setAccessible per operasi.
  2. Baris 14 (MethodType.methodType(...)): signature lengkap method dinyatakan eksplisit — tipe kembalian int diikuti dua parameter int. Signature menjadi bagian tipe pegangan, bukan diperiksa lewat Object[] di tiap pemanggilan.
  3. Baris 15 (lookup.findStatic(...)): mencari method static bernama tambah dengan signature tersebut. Hasilnya MethodHandle yang siap dipanggil berulang tanpa pencarian ulang.
  4. Baris 18 ((int) tambah.invokeExact(2, 3)): pemanggilan paling cepat — compiler mengadaptasi invokeExact ke signature pegangan, argument 2 dan 3 tetap int tanpa boxing, dan hasilnya langsung bertipe int. Signature yang tidak cocok membuat WrongMethodTypeException terjadi saat runtime.
  5. Baris 21 (tambah.invoke(2, 3)): versi longgar yang mengizinkan konversi tipe otomatis dan mengembalikan Object — lebih fleksibel, sedikit lebih mahal dari invokeExact.

Keluarga java.lang.invoke juga memuat VarHandle — pegangan bertipe untuk akses field dengan mode akses terkontrol (plain, volatile, compare-and-set) yang dipakai class concurrency JDK sendiri. Untuk framework yang butuh akses field berperforma tinggi, VarHandle adalah pengganti langsung Field.get/set.

Mengukur dengan Benar #

Seluruh diskusi angka di atas baru sah bila diukur pada program nyata — dan pengukuran mikro yang ditulis sendiri hampir selalu menyesatkan karena JIT, warm-up, dan dead-code elimination. Standar industri untuk ini adalah JMH (Java Microbenchmark Harness) dari OpenJDK, yang mengelola warm-up, fork proses, dan statistik hasil. Penggunaannya kita bahas tuntas pada Section Performance dan Optimization, Chapter JMH; untuk reflection, benchmark yang adil membandingkan pemanggilan langsung, Method.invoke tanpa cache, Method.invoke dengan cache, dan MethodHandle.invokeExact.

Dengan biaya dan mitigasinya tuntas, satu sisi reflection masih terbuka: keamanan. setAccessible yang kita pakai bebas di Chapter ini sebenarnya melanggar kontrak akses — dan pada module system modern, JDK bahkan menolaknya keras. Chapter penutup Section ini membedah batas tersebut.

Referensi #

Berkas Sumber

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

ContohMethodHandle.java

Unduh berkas
java
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class ContohMethodHandle {
    static int tambah(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType tipe = MethodType.methodType(int.class, int.class, int.class);
        MethodHandle tambah = lookup.findStatic(ContohMethodHandle.class, "tambah", tipe);

        int pasti = (int) tambah.invokeExact(2, 3);
        System.out.println("invokeExact : " + pasti);

        Object luwes = tambah.invoke(2, 3);
        System.out.println("invoke      : " + luwes);
    }
}

StrukturBiaya.java

Unduh berkas
java
import java.lang.reflect.Method;

class Kalkulator {
    public static int tambah(int a, int b) {
        return a + b;
    }
}

public class StrukturBiaya {
    public static void main(String[] args) throws Exception {
        // Bukti 1: hasil pencarian tidak di-cache JVM — setiap getMethod membuat salinan
        Method m1 = Kalkulator.class.getMethod("tambah", int.class, int.class);
        Method m2 = Kalkulator.class.getMethod("tambah", int.class, int.class);
        System.out.println("Hasil pencarian sama (equals)? " + m1.equals(m2));
        System.out.println("Objek yang sama (==)?          " + (m1 == m2));

        // Bukti 2: argument primitif wajib dibungkus (boxing) ke Object[]
        Method tambah = Kalkulator.class.getMethod("tambah", int.class, int.class);
        tambah.setAccessible(true);
        Object[] argumen = {2, 3};
        System.out.println("Tipe argumen[0]: " + argumen[0].getClass().getSimpleName());

        Object hasil = tambah.invoke(null, argumen);
        System.out.println("Tipe kembalian : " + hasil.getClass().getSimpleName());
        System.out.println("Hasil          : " + hasil);
    }
}