Reflection (riflessione) è un meccanismo di runtime che consente al codice di ispezionare la propria struttura: ottenere classi, metodi, campi e annotazioni senza conoscere i tipi in fase di compilazione. Questo strumento è alla base di molti framework mobili — la serializzazione JSON (Gson, Moshi), la dependency injection (Dagger, Koin) e i runner di test (JUnit, XCTest). Secondo il Tutorial di Reflection di Oracle Java, 2024, reflection è un elemento obbligatorio della piattaforma Java, utilizzato da tutte le principali librerie.
Punti chiave
Reflection è la capacità di un programma di osservare e modificare la propria struttura e il proprio comportamento durante l'esecuzione. Nei linguaggi orientati agli oggetti ciò significa ottenere oggetti Class, Method, Field e Constructor, che rappresentano gli elementi del programma come dati disponibili per la lettura e l'invocazione.
Il termine “reflection” è stato introdotto nella comunità dell'intelligenza artificiale nel 1982 (Brian Cantwell Smith) e implementato nel linguaggio Smalltalk. Nello sviluppo mobile, reflection è apparso per la prima volta in Java ME e Objective-C (1986, NextStep). Oggi ogni grande piattaforma mobile ha la propria API di reflection: Java/Kotlin per Android, Objective-C Runtime per iOS, Swift Mirror API per Swift.
Il meccanismo di reflection si basa sui metadati che il compilatore salva nel bytecode o nel binario. Android memorizza informazioni complete sulle classi nei file DEX, iOS nella sezione __objc_classlist del segmento Mach-O. Il runtime carica questi metadati in memoria e fornisce un'API per esaminarli.
Java Reflection API è costruita attorno alla classe java.lang.Class. Qualsiasi oggetto in Java può essere convertito in Class tramite .getClass() o Class.forName(). Da Class si estraggono tutti i metodi, i campi, i costruttori, le annotazioni e le superclassi. Kotlin eredita la reflection di Java e aggiunge le proprie KClass, KFunction, KProperty dal pacchetto kotlin.reflect.
import kotlin.reflect.full.declaredMemberFunctions
data class User(
val name: String,
val email: String
)
fun inspectClass() {
val kClass = User::class
val properties = kClass.declaredMemberProperties
val functions = kClass.declaredMemberFunctions
properties.forEach { prop ->
println("Proprietà: ${prop.name}, tipo: ${prop.returnType}")
}
}
In questo esempio KClass fornisce i metadati della data class User. declaredMemberProperties restituisce un elenco di proprietà con i loro tipi e getter. La reflection di Kotlin è strettamente integrata con le coroutine: KFunction supporta il modificatore suspend, che consente di chiamare metodi asincroni tramite reflection.
La reflection di Java lavora con Class<?>, Method.setAccessible() e Field.get(). setAccessible(true) disattiva i controlli di accesso del linguaggio Java per gli elementi privati. Questo è un meccanismo potente ma pericoloso: su Android a partire dall'API 28, chiamare setAccessible su metodi di sistema nascosti può causare InaccessibleObjectException.
// Java reflection: chiamata di un metodo privato
Class> clazz = Class.forName("com.example.MyClass");
Object instance = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getDeclaredMethod("privateMethod", String.class);
method.setAccessible(true);
method.invoke(instance, "reflection test");
Il codice dimostra Class.forName() — il caricamento dinamico di una classe tramite nome stringa. Questa è la base delle architetture a plugin: una classe può essere sconosciuta in fase di compilazione, ma caricata ed eseguita tramite reflection in runtime. getDeclaredMethod(“privateMethod”, ...) trova un metodo per nome e tipi di parametri, e invoke lo esegue.
Il runtime di Objective-C fornisce le funzioni class_copyMethodList, class_copyPropertyList, objc_getAssociatedObject. A differenza di Java, Objective-C non nasconde i metodi privati per impostazione predefinita — il runtime vede tutti i metodi della classe. Questo spiega perché il method swizzling funziona senza setAccessible: il runtime non ha incapsulamento a livello di metadati.
Reflection è utilizzato nelle librerie chiave dello sviluppo mobile. La serializzazione JSON (Gson, Moshi, Kotlinx.serialization) ottiene le proprietà dell'oggetto tramite reflection e le confronta con le chiavi JSON. La dependency injection (Dagger, Koin, Swinject) analizza costruttori e campi per l'iniezione automatica delle dipendenze. Le librerie ORM (Room, Realm) usano reflection per mappare le classi sulle tabelle del database.
Ognuna di queste applicazioni funziona esattamente in runtime — il codice non sa in anticipo con quali classi si confronterà. Reflection fornisce un meccanismo universale per superare questa incertezza al costo di prestazioni e sicurezza.
Reflection è da 10 a 100 volte più lenta delle chiamate dirette ai metodi. Il motivo è la mancanza di ottimizzazioni JIT (devirtualization, inlining), il controllo dei tipi a ogni chiamata e il raggruppamento dei parametri in Object[]/varargs. ART su Android 14 non può ottimizzare con inline le chiamate di reflection perché il metodo di destinazione è sconosciuto fino al momento dell'esecuzione.
| Operazione | Chiamata diretta | Tramite Reflection | Rallentamento |
|---|---|---|---|
| Chiamata di un metodo senza parametri | ~3 ns | ~120 ns | 40x |
| Lettura di un campo int | ~1 ns | ~85 ns | 85x |
| Chiamata di un metodo con 2 parametri | ~4 ns | ~250 ns | 62x |
| Creazione di un'istanza tramite il costruttore | ~5 ns | ~180 ns | 36x |
| Risoluzione di una classe tramite stringa | — | ~800 ns | — |
I dati sono stati ottenuti su un Google Pixel 8 (Android 14, ART). Le prestazioni di reflection migliorano a ogni versione di Android: su Android 9 una chiamata tramite Method.invoke() era 150 volte più lenta di una diretta, su Android 14 è 40 volte. ART utilizza meccanismi integrati di method handle per l'ottimizzazione.
Per le sezioni critiche per le prestazioni gli sviluppatori sostituiscono reflection con la generazione di codice: Dagger usa l'elaborazione delle annotazioni invece della ricerca in runtime, Kotlinx.serialization genera i serializzatori tramite KSP, Moshi adatta @JsonClass(generateAdapter = true) per il codegen in fase di compilazione.
L'elaborazione delle annotazioni (KAPT, KSP) e la generazione di codice sono le principali alternative a reflection nello sviluppo mobile. Esse spostano l'analisi dei metadati dal runtime alla fase di compilazione: il codice viene generato prima dell'avvio dell'applicazione, il che elimina l'overhead di reflection e migliora le prestazioni.
// KSP: code generation invece di reflection
@Serializable
data class Config(
val apiUrl: String,
val timeout: Int
)
// KSP genera ConfigSerializer senza reflection
fun loadConfig(json: String): Config {
return Config.serializer().decodeFromString(json)
}
In questo esempio @Serializable è l'annotazione di Kotlinx.serialization. KSP (Kotlin Symbol Processing) analizza il codice sorgente in fase di compilazione, trova tutte le classi @Serializable e genera i serializzatori. Durante l'esecuzione dell'applicazione reflection non viene usata — il serializzatore è già compilato in codice macchina.
La generazione di codice offre migliori prestazioni, sicurezza dei tipi e dimensioni del binario inferiori (l'eliminazione del codice morto rimuove le dipendenze di reflection non utilizzate). Reflection rimane necessaria per i compiti in cui i tipi sono sconosciuti in fase di compilazione: caricamento dinamico di plugin, proxy in runtime, strumentazione dei test. Secondo Kotlin, Kotlinx.serialization con KSP è da 3 a 5 volte più veloce di Gson, basata su reflection.
Reflection sulle piattaforme mobili ha limitazioni di sicurezza e prestazioni. Android a partire dall'API 28 (Pie) limita setAccessible per le interfacce non-SDK — tentare di aprire un metodo di sistema nascosto causa un'eccezione o un avviso. iOS con Swift non supporta reflection nel senso classico: la Swift Mirror API fornisce solo la lettura delle proprietà (name, value), senza modifica o chiamate a metodi.
Google Play rifiuta le applicazioni che usano reflection per aggirare le restrizioni della piattaforma: sostituzione dei servizi di sistema, modifica delle policy SELinux, lettura di permessi protetti. Anche Apple blocca le applicazioni che chiamano API private tramite reflection — il controllo di App Review scansiona il binario alla ricerca di firme stringa di objc_msgSend con selettori privati noti.
ProGuard/R8 è un'altra limitazione. L'offuscamento e la minificazione del codice rinominano classi e metodi in nomi brevi (a, b, c). Se il codice usa Class.forName(“com.example.MyClass”), si romperà dopo l'offuscamento. La soluzione sono le keep rules in proguard-rules.pro:
// Regole keep di ProGuard per reflection
-keep class com.example.** { *; }
-keep class * implements java.io.Serializable { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName ;
}
-keepattributes Signature, InnerClasses, EnclosingMethod
Le regole -keep indicano a R8 di non rinominare le classi usate tramite reflection. Senza queste regole un'applicazione offuscata andrà in crash con ClassNotFoundException — il runtime non riuscirà a trovare la classe tramite il nome stringa che è cambiato.
Domande frequenti
Sì, reflection è da 10 a 100 volte più lenta di una chiamata diretta. I motivi principali: mancanza di ottimizzazioni JIT (inlining, devirtualization), raggruppamento dei parametri e controllo dei tipi a ogni chiamata. Per il codice di produzione si raccomanda di sostituire reflection con la generazione di codice tramite KSP o l'elaborazione delle annotazioni.
La reflection di Java funziona tramite Class, Method, Field e richiede setAccessible per i membri privati. La reflection di Kotlin usa KClass, KFunction, KProperty e supporta sealed class, data class, coroutine (funzioni suspend) e null-safety. La reflection di Kotlin si basa su quella di Java, ma aggiunge un'API type-safe.
Aggiungere keep rules ProGuard/R8 per classi, metodi e campi usati tramite reflection. Per ogni Class.forName(), getDeclaredMethod(), getDeclaredField() deve esserci una direttiva -keep corrispondente. Strumenti come GreenDAO e Room generano automaticamente le keep rules.
Swift non ha reflection nel senso pieno. Mirror API (Swift 2+) consente di leggere le proprietà di una struct o classe: nome, valore, tipo. Chiamare metodi, modificare campi e creare istanze per tipo sono impossibili. Per questo si usa l'Objective-C Runtime quando si eredita da NSObject con @objc dynamic.
Gson (serializzazione JSON), Retrofit (creazione di implementazioni di interfacce tramite proxy dinamico), Mockito (creazione di mock), Koin (dependency injection), Room (verifica di Entity in fase di compilazione tramite KAPT), Firebase Crashlytics (analisi dello stack trace). La maggior parte delle librerie sta passando alla generazione di codice con KSP/KAPT.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche