Invokedynamic 101
La versione Java 7 di Oracle ha introdotto una nuova invokedynamic
istruzione bytecode nella Java Virtual Machine (JVM) e un nuovo java.lang.invoke
pacchetto API nella libreria di classi standard. Questo post ti introduce a queste istruzioni e API.
Il cosa e il come di invokedynamic
D: Cos'è invokedynamic
?
R: invokedynamic
è un'istruzione bytecode che facilita l'implementazione di linguaggi dinamici (per la JVM) tramite invocazione di metodi dinamici. Questa istruzione è descritta in Java SE 7 Edition della specifica JVM.
Linguaggi dinamici e statici
Un linguaggio dinamico (noto anche come linguaggio di tipizzazione dinamica ) è un linguaggio di programmazione di alto livello il cui controllo del tipo viene solitamente eseguito in fase di esecuzione, una funzionalità nota come digitazione dinamica . Il controllo del tipo verifica che un programma sia indipendente dal tipo : tutti gli argomenti dell'operazione hanno il tipo corretto. Groovy, Ruby e JavaScript sono esempi di linguaggi dinamici. (L' @groovy.transform.TypeChecked
annotazione fa sì che Groovy digiti il controllo in fase di compilazione.)
Al contrario, un linguaggio statico (noto anche come linguaggio di tipo statico ) esegue il controllo del tipo in fase di compilazione, una funzionalità nota come digitazione statica . Il compilatore verifica che un programma sia corretto nel tipo, sebbene possa rimandare alcuni controlli del tipo al runtime (si pensi ai cast e checkcast
all'istruzione). Java è un esempio di linguaggio statico. Il compilatore Java utilizza queste informazioni sul tipo per produrre bytecode fortemente tipizzato, che può essere eseguito in modo efficiente dalla JVM.
D: Come invokedynamic
facilita l'implementazione dinamica del linguaggio?
R: In un linguaggio dinamico, il controllo del tipo si verifica in genere in fase di esecuzione. Gli sviluppatori devono passare i tipi appropriati o rischiare errori di runtime. È spesso il caso che java.lang.Object
sia il tipo più accurato per un argomento di metodo. Questa situazione complica il controllo del tipo, che influisce sulle prestazioni.
Un'altra sfida è rappresentata dal fatto che i linguaggi dinamici in genere offrono la capacità di aggiungere campi / metodi e rimuoverli dalle classi esistenti. Di conseguenza, è necessario rimandare la risoluzione di classe, metodo e campo al runtime. Inoltre, è spesso necessario adattare una chiamata di metodo a una destinazione che ha una firma diversa.
Queste sfide richiedono tradizionalmente la creazione di un supporto runtime ad hoc sulla JVM. Questo supporto include classi di tipo wrapper, l'utilizzo di tabelle hash per fornire una risoluzione dinamica dei simboli e così via. Bytecode viene generato con punti di ingresso al runtime sotto forma di chiamate al metodo utilizzando una delle quattro istruzioni di invocazione del metodo:
invokestatic
viene utilizzato per invocarestatic
metodi.invokevirtual
viene utilizzato per invocare metodipublic
eprotected
nonstatic
tramite invio dinamico.invokeinterface
è simile adinvokevirtual
eccezione del fatto che l'invio del metodo è basato su un tipo di interfaccia.invokespecial
viene utilizzato per invocare metodi di inizializzazione dell'istanza (costruttori) nonchéprivate
metodi e metodi di una superclasse della classe corrente.
Questo supporto runtime influisce sulle prestazioni. Il bytecode generato spesso richiede diversi richiami di metodi JVM effettivi per un solo richiamo del metodo del linguaggio dinamico. La riflessione è ampiamente utilizzata e contribuisce al degrado delle prestazioni. Inoltre, i numerosi percorsi di esecuzione diversi rendono impossibile per il compilatore just-in-time (JIT) di JVM applicare le ottimizzazioni.
Per affrontare le scarse prestazioni, l' invokedynamic
istruzione elimina il supporto di runtime ad hoc. Invece, la prima chiamata esegue il bootstrap invocando la logica di runtime che seleziona in modo efficiente un metodo di destinazione e le chiamate successive in genere richiamano il metodo di destinazione senza dover riavviare il bootstrap.
invokedynamic
inoltre avvantaggia gli implementatori di linguaggi dinamici supportando i target del sito di chiamata che cambiano dinamicamente: un sito di chiamata , più specificamente, un sito di chiamata dinamico è invokedynamic
un'istruzione. Inoltre, poiché la JVM supporta internamente invokedynamic
, questa istruzione può essere ottimizzata meglio dal compilatore JIT.
Maniglie del metodo
D: Capisco che invokedynamic
funziona con gli handle del metodo per facilitare il richiamo dinamico del metodo. Cos'è un handle di metodo?
A: Un handle di metodo è "un riferimento tipizzato, direttamente eseguibile a un metodo sottostante, un costruttore, un campo o un'operazione simile di basso livello, con trasformazioni opzionali di argomenti o valori restituiti". In altre parole, è simile a un puntatore a funzione in stile C che punta al codice eseguibile - un obiettivo - e che può essere dereferenziato per richiamare questo codice. Gli handle del metodo sono descritti dalla java.lang.invoke.MethodHandle
classe astratta .
D: Potete fornire un semplice esempio di creazione e invocazione dell'handle del metodo?
A: Controlla il listato 1.
Listato 1. MHD.java
(versione 1)
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; public class MHD { public static void main(String[] args) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findStatic(MHD.class, "hello", MethodType.methodType(void.class)); mh.invokeExact(); } static void hello() { System.out.println("hello"); } }
Listato 1 descrive un programma dimostrativo metodo manico costituito da main()
e hello()
metodi di classe. L'obiettivo di questo programma è invocare hello()
tramite un handle di metodo.
main()
Il primo compito è ottenere un java.lang.invoke.MethodHandles.Lookup
oggetto. Questo oggetto è una factory per la creazione di handle di metodo e viene utilizzato per cercare destinazioni come metodi virtuali, metodi statici, metodi speciali, costruttori e funzioni di accesso ai campi. Inoltre, dipende dal contesto di chiamata di un sito di chiamata e applica le restrizioni di accesso all'handle del metodo ogni volta che viene creato un handle del metodo. In altre parole, un sito di chiamata (come il main()
metodo del listato 1 che funge da sito di chiamata) che ottiene un oggetto di ricerca può accedere solo a quelle destinazioni che sono accessibili al sito di chiamata. L'oggetto di ricerca si ottiene invocando il metodo java.lang.invoke.MethodHandles
della classe MethodHandles.Lookup lookup()
.
publicLookup()
MethodHandles
dichiara anche un MethodHandles.Lookup publicLookup()
metodo. A differenza di lookup()
, che può essere utilizzato per ottenere un handle del metodo per qualsiasi metodo / costruttore o campo accessibile, publicLookup()
può essere utilizzato per ottenere un handle del metodo solo per un campo accessibile pubblicamente o per un metodo / costruttore accessibile pubblicamente.
Dopo aver ottenuto l'oggetto di ricerca, il MethodHandle findStatic(Class refc, String name, MethodType type)
metodo di questo oggetto viene chiamato per ottenere un handle del hello()
metodo per il metodo. Il primo argomento passato a findStatic()
è un riferimento alla classe ( MHD
) da cui hello()
si accede al metodo ( ), e il secondo argomento è il nome del metodo. Il terzo argomento è un esempio di un tipo di metodo , che "rappresenta gli argomenti e il tipo di ritorno accettati e restituiti da un handle di metodo, o gli argomenti e il tipo di ritorno passati e attesi da un chiamante di handle di metodo." È rappresentato da un'istanza della java.lang.invoke.MethodType
classe e ottenuto (in questo esempio) chiamando java.lang.invoke.MethodType
il MethodType methodType(Class rtype)
metodo di. Questo metodo viene chiamato perché hello()
fornisce solo un tipo restituito, che sembra esserevoid
. Questo tipo restituito viene reso disponibile methodType()
passando void.class
a questo metodo.
L'handle del metodo restituito viene assegnato mh
. Questo oggetto viene quindi utilizzato per chiamare MethodHandle
il Object invokeExact(Object... args)
metodo di, per invocare il metodo handle. In altre parole, invokeExact()
risulta hello()
essere chiamato e hello
scritto nel flusso di output standard. Poiché invokeExact()
è dichiarato per lanciare Throwable
, l'ho aggiunto throws Throwable
all'intestazione del main()
metodo.
D: Nella tua risposta precedente, hai menzionato che l'oggetto di ricerca può accedere solo a quei target che sono accessibili al sito della chiamata. Potete fornire un esempio che dimostri il tentativo di ottenere un handle di metodo per un target inaccessibile?
A: Controlla il listato 2.
Listato 2. MHD.java
(versione 2)
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; class HW { public void hello1() { System.out.println("hello from hello1"); } private void hello2() { System.out.println("hello from hello2"); } } public class MHD { public static void main(String[] args) throws Throwable { HW hw = new HW(); MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual(HW.class, "hello1", MethodType.methodType(void.class)); mh.invoke(hw); mh = lookup.findVirtual(HW.class, "hello2", MethodType.methodType(void.class)); } }
Il listato 2 dichiara HW
(Hello, World) e MHD
classes. HW
dichiara un public
hello1()
metodo di istanza e un private
hello2()
metodo di istanza. MHD
dichiara un main()
metodo che tenterà di richiamare questi metodi.
main()
Il primo compito è creare istanze HW
in preparazione per invocare hello1()
e hello2()
. Successivamente, ottiene un oggetto di ricerca e utilizza questo oggetto per ottenere un handle di metodo per invocare hello1()
. Questa volta, MethodHandles.Lookup
's findVirtual()
metodo viene chiamato ed il primo argomento passato a questo metodo è un Class
oggetto che descrive la HW
classe.
Si scopre che findVirtual()
avrà successo e mh.invoke(hw);
verrà richiamata l' espressione successiva hello1()
, risultando in hello from hello1
output.
Perché hello1()
è public
accessibile al main()
sito di chiamata al metodo. Al contrario, hello2()
non è accessibile. Di conseguenza, la seconda findVirtual()
invocazione fallirà con un IllegalAccessException
.
Quando esegui questa applicazione, dovresti osservare il seguente output:
hello from hello1 Exception in thread "main" java.lang.IllegalAccessException: member is private: HW.hello2()void, from MHD at java.lang.invoke.MemberName.makeAccessException(MemberName.java:507) at java.lang.invoke.MethodHandles$Lookup.checkAccess(MethodHandles.java:1172) at java.lang.invoke.MethodHandles$Lookup.checkMethod(MethodHandles.java:1152) at java.lang.invoke.MethodHandles$Lookup.accessVirtual(MethodHandles.java:648) at java.lang.invoke.MethodHandles$Lookup.findVirtual(MethodHandles.java:641) at MHD.main(MHD.java:27)
D: Gli elenchi 1 e 2 utilizzano i metodi invokeExact()
e invoke()
per eseguire un handle di metodo. Qual è la differenza tra questi metodi?
R: Sebbene invokeExact()
e invoke()
siano progettati per eseguire un handle del metodo (in realtà, il codice di destinazione a cui si riferisce l'handle del metodo), differiscono quando si tratta di eseguire conversioni di tipo sugli argomenti e sul valore restituito. invokeExact()
non esegue la conversione automatica del tipo compatibile sugli argomenti. I suoi argomenti (o espressioni di argomento) devono essere un tipo esatto corrispondente alla firma del metodo, con ogni argomento fornito separatamente o tutti gli argomenti forniti insieme come un array. invoke()
richiede che i suoi argomenti (o espressioni di argomento) siano una corrispondenza compatibile con il tipo con la firma del metodo: vengono eseguite conversioni di tipo automatiche, con ogni argomento fornito separatamente o tutti gli argomenti forniti insieme come un array.
D: Potete fornirmi un esempio che mostri come invocare il getter e il setter di un campo di istanza?
A: Controlla il listato 3.
Listato 3. MHD.java
(versione 3)
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; class Point { int x; int y; } public class MHD { public static void main(String[] args) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); Point point = new Point(); // Set the x and y fields. MethodHandle mh = lookup.findSetter(Point.class, "x", int.class); mh.invoke(point, 15); mh = lookup.findSetter(Point.class, "y", int.class); mh.invoke(point, 30); mh = lookup.findGetter(Point.class, "x", int.class); int x = (int) mh.invoke(point); System.out.printf("x = %d%n", x); mh = lookup.findGetter(Point.class, "y", int.class); int y = (int) mh.invoke(point); System.out.printf("y = %d%n", y); } }
Il Listato 3 introduce una Point
classe con una coppia di campi di istanza interi a 32 bit denominati x
e y
. Setter e getter di ogni campo si accede chiamando MethodHandles.Lookup
s' findSetter()
e findGetter()
metodi, e la risultante MethodHandle
viene restituito. Ciascuno di findSetter()
e findGetter()
richiede un Class
argomento che identifichi la classe del campo, il nome del campo e un Class
oggetto che identifichi la firma del campo.
Il invoke()
metodo viene utilizzato per eseguire un setter o un getter: dietro le quinte, si accede ai campi dell'istanza tramite le JVM putfield
e le getfield
istruzioni. Questo metodo richiede che un riferimento all'oggetto a cui si accede al campo venga passato come argomento iniziale. Per le chiamate di setter, deve essere passato anche un secondo argomento, costituito dal valore assegnato al campo.
Quando esegui questa applicazione, dovresti osservare il seguente output:
x = 15 y = 30
D: La tua definizione di handle del metodo include la frase "con trasformazioni opzionali di argomenti o valori restituiti". Potete fornire un esempio di trasformazione dell'argomento?
A: Ho creato un esempio basato sul metodo di Math
classe della double pow(double a, double b)
classe. In questo esempio, ottengo un handle del pow()
metodo per il metodo e trasformo questo handle del metodo in modo che il secondo argomento passato a pow()
sia sempre 10
. Dai un'occhiata al listato 4.