Perché dovrei usare la riflessione?


29

Sono nuovo di Java; attraverso i miei studi, ho letto che la riflessione è usata per invocare classi e metodi e per sapere quali metodi sono implementati o meno.

Quando dovrei usare la riflessione, e qual è la differenza tra usare la riflessione e creare un'istanza di oggetti e chiamare metodi in modo tradizionale?



10
Ti preghiamo di fare la tua parte di ricerca prima di pubblicare. C'è molto materiale su StackExchange (come notato da @Jalayn) e sul web in generale sulla riflessione. Ti suggerisco di leggere ad esempio il Tutorial Java su Reflection e di tornare dopo se hai altre domande concrete.
Péter Török l'

1
Devono esserci un milione di duplicati.
DeadMG

3
Più di alcuni programmatori professionisti risponderebbero "il più raramente possibile, forse nemmeno mai".
Ross Patterson,

Risposte:


38
  • La riflessione è molto più lenta della semplice chiamata dei metodi con il loro nome, perché deve ispezionare i metadati nel bytecode invece di usare solo indirizzi e costanti precompilati.

  • La riflessione è anche più potente: puoi recuperare la definizione di un protectedo un finalmembro, rimuovere la protezione e manipolarla come se fosse stata dichiarata mutabile! Ovviamente questo sovverte molte delle garanzie che la lingua normalmente offre ai tuoi programmi e può essere molto, molto pericolosa.

E questo spiega praticamente quando usarlo. Di solito no. Se vuoi chiamare un metodo, chiamalo e basta. Se vuoi mutare un membro, dichiaralo mutabile invece di andare dietro la compilazione.

Un utile uso del mondo reale della riflessione è quando si scrive un framework che deve interagire con le classi definite dall'utente, in cui l'autore del framework non sa quali saranno i membri (o persino le classi). La riflessione consente loro di affrontare qualsiasi classe senza conoscerla in anticipo. Ad esempio, non penso che sarebbe possibile scrivere una complessa libreria orientata all'aspetto senza riflessione.

Come altro esempio, JUnit utilizzava un po 'di banale riflesso: enumera tutti i metodi nella tua classe, presume che tutti quelli chiamati testXXXsiano metodi di test ed esegue solo quelli. Ma ora questo può essere fatto meglio con le annotazioni, e infatti JUnit 4 si è invece spostato in gran parte nelle annotazioni.


6
"Più potente" ha bisogno di cure. Non hai bisogno di riflessione per ottenere la completezza di Turing, quindi nessun calcolo ha mai bisogno di riflessione. Naturalmente Turing complete non dice nulla su altri tipi di alimentazione, come le capacità di I / O e, ovviamente, la riflessione.
Steve314,

1
La riflessione non è necessariamente "molto più lenta". È possibile utilizzare il reflection una volta per generare un bytecode del wrapper di chiamata diretta.
SK-logic,

2
Lo saprai quando ne avrai bisogno. Mi chiedevo spesso perché (al di fuori della generazione del linguaggio) uno ne avesse bisogno. Poi, all'improvviso, l'ho fatto ... Ho dovuto camminare su e giù per le catene genitore / figlio per sbirciare / colpire i dati quando ho ottenuto pannelli da altri sviluppatori per entrare in uno dei sistemi che mantengo.
Brian Knoblauch,

@ SK-logic: in realtà per generare un bytecode non hai assolutamente bisogno di reflection (in effetti reflection non contiene affatto API per la manipolazione di bytecode!).
Joachim Sauer,

1
@JoachimSauer, ovviamente, ma avrai bisogno di un'API di riflessione per caricare questo bytecode generato.
SK-logic,

15

Una volta ero come te, non sapevo molto del riflesso - ancora non lo so - ma l'ho usato una volta.

Avevo una classe con due classi interne e ogni classe aveva molti metodi.

Avevo bisogno di invocare tutti i metodi nella classe interna e invocarli manualmente sarebbe stato troppo lavoro.

Usando la riflessione, ho potuto invocare tutti questi metodi in sole 2-3 righe di codice, anziché nel numero dei metodi stessi.


4
Perché il downvote?
Mahmoud Hossam,

1
Voto per il preambolo
Dmitry Minkovsky,

1
@MahmoudHossam Forse non è una buona pratica, ma la tua risposta illustra un'importante tattica che puoi implementare.
ankush981,

13

Raggrupperei gli usi della riflessione in tre gruppi:

  1. Istanza di classi arbitrarie. Ad esempio, in un framework di iniezione delle dipendenze, probabilmente si dichiara che l'interfaccia ThingDoer è implementata dalla classe NetworkThingDoer. Il framework troverebbe quindi il costruttore di NetworkThingDoer e lo creerebbe un'istanza.
  2. Marshalling e unmarshalling in qualche altro formato. Ad esempio, mappando un oggetto con getter e impostazioni che seguono la convenzione bean su JSON e viceversa. Il codice in realtà non conosce i nomi dei campi o i metodi, esamina solo la classe.
  3. Avvolgere una classe in un livello di reindirizzamento (forse quel List non è effettivamente caricato, ma solo un puntatore a qualcosa che sa come recuperarlo dal database) o falsificare una classe interamente (jMock creerà una classe sintetica che implementa un'interfaccia a scopo di test).

Questa è la migliore spiegazione della riflessione che ho trovato su StackExchange. La maggior parte delle risposte ripete ciò che dice Java Trail (che è "puoi accedere a tutte queste proprietà", ma non il motivo per cui lo faresti), fornire esempi di fare cose con la riflessione di cui è molto più facile fare a meno o dare una vaga risposta su come Spring lo usa. Questa risposta fornisce in realtà tre esempi validi che NON POSSONO essere facilmente elaborati da JVM senza riflessione. Grazie!
ndm13

3

Reflection consente a un programma di lavorare con codice che potrebbe non essere presente e farlo in modo affidabile.

Il "codice normale" ha frammenti come i URLConnection c = nullquali per la sua pura presenza fanno sì che il caricatore di classi carichi la classe URLConnection come parte del caricamento di questa classe, generando un'eccezione ClassNotFound ed uscendo.

Reflection ti consente di caricare le classi in base ai loro nomi in forma di stringa e di testarle per varie proprietà (utile per più versioni al di fuori del tuo controllo) prima di avviare le classi effettive che dipendono da esse. Un esempio tipico è il codice specifico di OS X utilizzato per rendere nativi i programmi Java in OS X, che non sono presenti su altre piattaforme.


2

Fondamentalmente, riflessione significa usare il codice del programma come dati.

Pertanto, l'utilizzo di reflection potrebbe essere una buona idea quando il codice del programma è una fonte di dati utile. (Ma ci sono dei compromessi, quindi potrebbe non essere sempre una buona idea.)

Ad esempio, considera una classe semplice:

public class Foo {
  public int value;
  public string anotherValue;
}

e vuoi generare XML da esso. È possibile scrivere codice per generare l'XML:

public XmlNode generateXml(Foo foo) {
  XmlElement root = new XmlElement("Foo");
  XmlElement valueElement = new XmlElement("value");
  valueElement.add(new XmlText(Integer.toString(foo.value)));
  root.add(valueElement);
  XmlElement anotherValueElement = new XmlElement("anotherValue");
  anotherValueElement.add(new XmlText(foo.anotherValue));
  root.add(anotherValueElement);
  return root;
}

Ma questo è un sacco di codice boilerplate e ogni volta che cambi classe, devi aggiornare il codice. Davvero, potresti descrivere cosa fa questo codice

  • creare un elemento XML con il nome della classe
  • per ogni proprietà della classe
    • creare un elemento XML con il nome della proprietà
    • inserire il valore della proprietà nell'elemento XML
    • aggiungi l'elemento XML alla radice

Questo è un algoritmo e l'input dell'algoritmo è la classe: abbiamo bisogno del suo nome e dei nomi, tipi e valori delle sue proprietà. È qui che entra in gioco la riflessione: ti dà accesso a queste informazioni. Java consente di ispezionare i tipi utilizzando i metodi della Classclasse.

Alcuni altri casi d'uso:

  • definire gli URL in un server Web in base ai nomi dei metodi di una classe e i parametri URL in base agli argomenti del metodo
  • converte la struttura di una classe in una definizione di tipo GraphQL
  • chiama ogni metodo di una classe il cui nome inizia con "test" come caso di unit test

Tuttavia, la riflessione completa significa non solo guardare il codice esistente (che di per sé è noto come "introspezione"), ma anche modificare o generare codice. Ci sono due casi d'uso importanti in Java per questo: proxy e mock.

Supponiamo che tu abbia un'interfaccia:

public interface Froobnicator {
  void froobnicateFruits(List<Fruit> fruits);
  void froobnicateFuel(Fuel fuel);
  // lots of other things to froobnicate
}

e hai un'implementazione che fa qualcosa di interessante:

public class PowerFroobnicator implements Froobnicator {
  // awesome implementations
}

E in effetti hai anche una seconda implementazione:

public class EnergySaverFroobnicator implements Froobnicator {
  // efficient implementations
}

Ora vuoi anche un po 'di output del log; vuoi semplicemente un messaggio di registro ogni volta che viene chiamato un metodo. È possibile aggiungere in modo esplicito l'output del registro a ogni metodo, ma sarebbe fastidioso e dovresti farlo due volte; una volta per ogni implementazione. (Quindi ancora di più quando aggiungi più implementazioni.)

Invece, puoi scrivere un proxy:

public class LoggingFroobnicator implements Froobnicator {
  private Logger logger;
  private Froobnicator inner;

  // constructor that sets those two

  public void froobnicateFruits(List<Fruit> fruits) {
    logger.logDebug("froobnicateFruits called");
    inner.froobnicateFruits(fruits);
  }

  public void froobnicateFuel(Fuel fuel) {
    logger.logDebug("froobnicateFuel( called");
    inner.froobnicateFuel(fuel);
  }
  // lots of other things to froobnicate
}

Ancora una volta, tuttavia, esiste un modello ripetitivo che può essere descritto da un algoritmo:

  • un proxy logger è una classe che implementa un'interfaccia
  • ha un costruttore che richiede un'altra implementazione dell'interfaccia e un logger
  • per ogni metodo nell'interfaccia
    • l'implementazione registra un messaggio "$ methodname chiamato"
    • e quindi chiama lo stesso metodo sull'interfaccia interna, passando tutti gli argomenti

e l'input di questo algoritmo è la definizione dell'interfaccia.

Reflection ti consente di definire una nuova classe usando questo algoritmo. Java ti consente di farlo usando i metodi della java.lang.reflect.Proxyclasse e ci sono librerie che ti danno ancora più potenza.

Quindi quali sono gli svantaggi della riflessione?

  • Il tuo codice diventa più difficile da capire. Sei un livello di astrazione ulteriormente rimosso dagli effetti concreti del tuo codice.
  • Il tuo codice diventa più difficile da eseguire il debug. Soprattutto con le librerie che generano codice, il codice che viene eseguito potrebbe non essere il codice che hai scritto, ma il codice che hai generato e il debugger potrebbe non essere in grado di mostrarti quel codice (o di lasciare dei punti di interruzione).
  • Il tuo codice diventa più lento. La lettura dinamica delle informazioni sui tipi e l'accesso ai campi tramite i loro handle di runtime invece dell'accesso con codifica rigida è più lento. La generazione di codice dinamico può mitigare questo effetto, a costo di essere ancora più difficile da eseguire il debug.
  • Il tuo codice potrebbe diventare più fragile. L'accesso al riflesso dinamico non viene verificato dal compilatore, ma genera errori in fase di esecuzione.

1

Reflection può sincronizzare automaticamente parti del programma, dove in precedenza si sarebbe dovuto aggiornare manualmente il programma per utilizzare le nuove interfacce.


5
Il prezzo da pagare in questo caso è che si perde la verifica del tipo da parte del compilatore e la sicurezza del refactor nell'IDE. È un compromesso che non sono disposto a fare.
Barend,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.