Invokedynamic 101

La versione Java 7 di Oracle ha introdotto una nuova invokedynamicistruzione bytecode nella Java Virtual Machine (JVM) e un nuovo java.lang.invokepacchetto 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.TypeCheckedannotazione 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 checkcastall'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 invokedynamicfacilita 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.Objectsia 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:

  • invokestaticviene utilizzato per invocare staticmetodi.
  • invokevirtualviene utilizzato per invocare metodi publice protectednon statictramite invio dinamico.
  • invokeinterfaceè simile ad invokevirtualeccezione del fatto che l'invio del metodo è basato su un tipo di interfaccia.
  • invokespecialviene utilizzato per invocare metodi di inizializzazione dell'istanza (costruttori) nonché privatemetodi 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' invokedynamicistruzione 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.

invokedynamicinoltre 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 è invokedynamicun'istruzione. Inoltre, poiché la JVM supporta internamente invokedynamic, questa istruzione può essere ottimizzata meglio dal compilatore JIT.

Maniglie del metodo

D: Capisco che invokedynamicfunziona 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.MethodHandleclasse 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.Lookupoggetto. 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.MethodHandlesdella classe MethodHandles.Lookup lookup().

publicLookup()

MethodHandlesdichiara 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.MethodTypeclasse e ottenuto (in questo esempio) chiamando java.lang.invoke.MethodTypeil 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.classa questo metodo.

L'handle del metodo restituito viene assegnato mh. Questo oggetto viene quindi utilizzato per chiamare MethodHandleil Object invokeExact(Object... args)metodo di, per invocare il metodo handle. In altre parole, invokeExact()risulta hello()essere chiamato e helloscritto nel flusso di output standard. Poiché invokeExact()è dichiarato per lanciare Throwable, l'ho aggiunto throws Throwableall'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 MHDclasses. HWdichiara un publichello1()metodo di istanza e un privatehello2()metodo di istanza. MHDdichiara un main()metodo che tenterà di richiamare questi metodi.

main()Il primo compito è creare istanze HWin 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 Classoggetto che descrive la HWclasse.

Si scopre che findVirtual()avrà successo e mh.invoke(hw);verrà richiamata l' espressione successiva hello1(), risultando in hello from hello1output.

Perché hello1()è publicaccessibile 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 Pointclasse con una coppia di campi di istanza interi a 32 bit denominati xe y. Setter e getter di ogni campo si accede chiamando MethodHandles.Lookups' findSetter()e findGetter()metodi, e la risultante MethodHandleviene restituito. Ciascuno di findSetter()e findGetter()richiede un Classargomento che identifichi la classe del campo, il nome del campo e un Classoggetto 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 putfielde le getfieldistruzioni. 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 Mathclasse 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.