Mini ORM pada Chapter sebelumnya memakai Class, Field, dan getAnnotation tanpa membahas dari mana semua objek itu berasal. Jawabannya adalah reflection — mekanisme Java untuk memeriksa dan memanipulasi struktur program saat program berjalan, bukan saat kompilasi. Dengan reflection, kode dapat bertanya: class apa ini, field apa saja isinya, method apa yang bisa dipanggil, annotation apa yang menempel — bahkan mengakses anggota yang dideklarasikan private. Kekuatan sebesar itu menjelaskan mengapa reflection menjadi pondasi framework, sekaligus sumber biaya performa dan risiko keamanan yang akan kita bahas pada dua Chapter terakhir Section ini.
Bagaimana Reflection Bekerja #
Setiap kali JVM memuat sebuah class dari berkas .class, JVM menyimpan representasi runtime lengkap dari class tersebut: nama, modifier, field, method, constructor, annotation, sampai bytecode method-nya. Representasi ini diakses lewat satu objek tunggal bertipe java.lang.Class — sering disebut class object. Objek Class adalah gerbang: dari situ kita bisa meminta larik Field, Method, Constructor, atau objek Annotation, dan masing-masing tipe itu punya method untuk membaca modifier, tipe, nama, dan nilai.
Karena informasinya berasal dari JVM — bukan dari teks sumber — reflection tetap bekerja pada class yang dikompilasi pihak lain. Framework dapat memeriksa class pustaka tertutup tanpa membuka kode sumbernya. Konsekuensinya juga simetris: apa pun yang tertulis di berkas .class dapat dilihat reflection, termasuk anggota private.
Tiga Cara Mendapatkan Objek Class #
Semua operasi reflection dimulai dari memperoleh objek Class. Java menyediakan tiga jalur, dan ketiganya menghasilkan objek yang sama karena JVM hanya membuat satu class object per tipe yang dimuat:
public class TigaCaraClass {
public static void main(String[] args) throws Exception {
// Cara 1: class literal — tipe sudah diketahui saat menulis kode
Class<String> lewatLiteral = String.class;
// Cara 2: getClass() — dari objek yang sudah ada saat runtime
Class<?> lewatObjek = "Halo, Budi".getClass();
// Cara 3: Class.forName — dari string nama class ber-package
Class<?> lewatNama = Class.forName("java.lang.String");
System.out.println("literal == objek : " + (lewatLiteral == lewatObjek));
System.out.println("objek == nama : " + (lewatObjek == lewatNama));
Class<?> sendiri = Class.forName("TigaCaraClass");
System.out.println("Class sendiri : " + sendiri.getName());
System.out.println("Class primitif : " + int.class.getName());
System.out.println("Class array : " + int[].class.getName());
}
}Output yang Dihasilkan:
literal == objek : true
objek == nama : true
Class sendiri : TigaCaraClass
Class primitif : int
Class array : [IMari kita telaah ketiga jalur tersebut:
- Baris 4 (
String.class): class literal — nama tipe diikuti.class. Pilihan tercepat dan paling aman karena ketidaksesuaian tipe tertangkap compiler; satu-satunya cara untuk tipe primitif (int.class) dan array (int[].class). - Baris 7 (
"Halo, Budi".getClass()): method instance yang diwarisi semua objek dariObject. Berguna ketika tipe objek tidak diketahui saat menulis kode — misalnya objek yang baru tiba dari pengguna atau jaringan. - Baris 10 (
Class.forName("java.lang.String")): memuat class berdasarkan string nama lengkapnya. Inilah jalur yang dipakai JDBC driver, plugin system, dan deserializer; kelemahannya, kesalahan ketik nama baru terasa saat runtime sebagaiClassNotFoundException.
Kedua pembandingan == mencetak true — JVM menjamin satu class object per tipe per class loader, sehingga ketiga jalur berujung pada objek yang identik. Baris Class sendiri memperlihatkan efek package: class TigaCaraClass berada di default package (tanpa deklarasi package), sehingga getName() hanya mengembalikan nama sederhananya. Class ber-package seperti java.lang.String menghasilkan nama lengkap. Perhatikan pula notasi array int[] yang tercetak [I — encoding internal JVM untuk “array satu dimensi bertipe int”.
Peta Paket java.lang.reflect #
Package java.lang.reflect berisi keluarga objek yang semuanya lahir dari Class:
| Kelas | Mewakili | Dihasilkan lewat |
|---|---|---|
Class | tipe itu sendiri | literal, getClass(), Class.forName |
Field | field deklarasi | getField, getDeclaredField, getDeclaredFields |
Method | method deklarasi | getMethod, getDeclaredMethod, getDeclaredMethods |
Constructor | constructor | getConstructor, getDeclaredConstructors |
Parameter | parameter method/constructor | Method.getParameters() |
Modifier | decode bit modifier public/static/… | Modifier.toString(int) |
Array | pembuat/pengakses array dinamis | Array.newInstance |
RecordComponent | komponen record | Class.getRecordComponents() |
Program berikut memakai sebagian peta tersebut pada class domain Produk — menghitung anggotanya, lalu membuat objek dan memanggil toString tanpa menulis new Produk(...) secara langsung:
import java.lang.reflect.*;
class Produk {
private final String nama;
private final int harga;
public Produk(String nama, int harga) {
this.nama = nama;
this.harga = harga;
}
public String getNama() {
return nama;
}
@Override
public String toString() {
return nama + " - Rp" + harga;
}
}
public class PetaRefleksi {
public static void main(String[] args) throws Exception {
Class<?> c = Produk.class;
System.out.println("Field : " + c.getDeclaredFields().length);
System.out.println("Method : " + c.getDeclaredMethods().length);
System.out.println("Constructor : " + c.getDeclaredConstructors().length);
Constructor<?> ctor = c.getConstructor(String.class, int.class);
Object kopi = ctor.newInstance("Kopi Gayo", 85000);
Method toString = c.getMethod("toString");
System.out.println("Objek baru : " + toString.invoke(kopi));
}
}Output yang Dihasilkan:
Field : 2
Method : 2
Constructor : 1
Objek baru : Kopi Gayo - Rp85000Baris getConstructor(String.class, int.class) mencari constructor publik yang parameternya persis String lalu int — tipe parameter dinyatakan sebagai class object. Pemanggilan newInstance("Kopi Gayo", 85000) membuat objek baru dari constructor tersebut, dan toString.invoke(kopi) memanggil method toString pada objek itu. Angka pada output — 2 field, 2 method, 1 constructor — persis mengikuti deklarasi Produk; itu bukti bahwa reflection membaca struktur yang tersimpan di JVM, bukan menebak dari nama.
Kekuatan Besar Datang dengan Batasan #
Reflection menyelesaikan pekerjaan yang mustahil dilakukan kode statis: framework yang tidak mengenal class Anda tetap dapat mengelolanya. Pola ini muncul di mana-mana — serialisasi JSON membaca field, dependency injection memanggil constructor, ORM memetakan field ke kolom, dan test runner menemukan method @Test. Tanpa reflection, semua itu harus dikodekan ulang untuk setiap class.
Kekuatannya berbayar. Pertama, pemeriksaan tipe bergeser dari compile-time ke runtime: getMethod("toStringn") salah ketik baru terasa sebagai NoSuchMethodException saat program berjalan. Kedua, reflection melompati enkapsulasi — setAccessible(true) dapat membuka field private, memutus kontrak yang ditulis pembuat class. Ketiga, biaya per operasi jauh lebih tinggi daripada pemanggilan langsung: ada pemeriksaan keamanan, pembungkusan (boxing) argument, dan penghalang bagi optimasi JIT. Dua Chapter terakhir Section ini membahas sisi performa dan keamanan ini secara khusus.
Langkah alami berikutnya adalah menjelajahi isi sebuah class secara sistematis: nama, modifier, superclass, interface, sampai deteksi record dan enum. Chapter berikutnya menuntaskan inspeksi struktur lewat Class itu sendiri.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PetaRefleksi.java
Unduh berkasimport java.lang.reflect.*;
class Produk {
private final String nama;
private final int harga;
public Produk(String nama, int harga) {
this.nama = nama;
this.harga = harga;
}
public String getNama() {
return nama;
}
@Override
public String toString() {
return nama + " - Rp" + harga;
}
}
public class PetaRefleksi {
public static void main(String[] args) throws Exception {
Class<?> c = Produk.class;
System.out.println("Field : " + c.getDeclaredFields().length);
System.out.println("Method : " + c.getDeclaredMethods().length);
System.out.println("Constructor : " + c.getDeclaredConstructors().length);
Constructor<?> ctor = c.getConstructor(String.class, int.class);
Object kopi = ctor.newInstance("Kopi Gayo", 85000);
Method toString = c.getMethod("toString");
System.out.println("Objek baru : " + toString.invoke(kopi));
}
}
TigaCaraClass.java
Unduh berkaspublic class TigaCaraClass {
public static void main(String[] args) throws Exception {
// Cara 1: class literal — tipe sudah diketahui saat menulis kode
Class<String> lewatLiteral = String.class;
// Cara 2: getClass() — dari objek yang sudah ada saat runtime
Class<?> lewatObjek = "Halo, Budi".getClass();
// Cara 3: Class.forName — dari string nama class ber-package
Class<?> lewatNama = Class.forName("java.lang.String");
System.out.println("literal == objek : " + (lewatLiteral == lewatObjek));
System.out.println("objek == nama : " + (lewatObjek == lewatNama));
Class<?> sendiri = Class.forName("TigaCaraClass");
System.out.println("Class sendiri : " + sendiri.getName());
System.out.println("Class primitif : " + int.class.getName());
System.out.println("Class array : " + int[].class.getName());
}
}