Dopo aver lavorato con il codice byte Java per un po 'di tempo e aver fatto ulteriori ricerche su questo argomento, ecco un riepilogo delle mie scoperte:
Eseguire il codice in un costruttore prima di chiamare un costruttore super o costruttore ausiliario
Nel linguaggio di programmazione Java (JPL), la prima istruzione di un costruttore deve essere l'invocazione di un super costruttore o di un altro costruttore della stessa classe. Questo non è vero per il codice byte Java (JBC). All'interno del codice byte, è assolutamente legittimo eseguire qualsiasi codice prima di un costruttore, purché:
- Un altro costruttore compatibile viene chiamato dopo questo blocco di codice.
- Questa chiamata non è inclusa in un'istruzione condizionale.
- Prima di questa chiamata del costruttore, nessun campo dell'istanza costruita viene letto e nessuno dei suoi metodi viene invocato. Ciò implica l'elemento successivo.
Impostare i campi dell'istanza prima di chiamare un costruttore super o costruttore ausiliario
Come accennato in precedenza, è perfettamente legale impostare un valore di campo di un'istanza prima di chiamare un altro costruttore. Esiste persino un hack legacy che lo rende in grado di sfruttare questa "funzionalità" nelle versioni Java precedenti alla 6:
class Foo {
public String s;
public Foo() {
System.out.println(s);
}
}
class Bar extends Foo {
public Bar() {
this(s = "Hello World!");
}
private Bar(String helper) {
super();
}
}
In questo modo, è possibile impostare un campo prima che venga invocato il super costruttore, che tuttavia non è più possibile. In JBC, questo comportamento può ancora essere implementato.
Diramazione di una chiamata del super costruttore
In Java, non è possibile definire una chiamata del costruttore come
class Foo {
Foo() { }
Foo(Void v) { }
}
class Bar() {
if(System.currentTimeMillis() % 2 == 0) {
super();
} else {
super(null);
}
}
Fino a Java 7u23, il verificatore della VM HotSpot non ha comunque perso questo controllo, motivo per cui è stato possibile. Questo è stato utilizzato da diversi strumenti di generazione del codice come una sorta di hack, ma non è più legale implementare una classe come questa.
Quest'ultimo era semplicemente un bug in questa versione del compilatore. Nelle versioni più recenti del compilatore, questo è di nuovo possibile.
Definire una classe senza alcun costruttore
Il compilatore Java implementerà sempre almeno un costruttore per qualsiasi classe. Nel codice byte Java, questo non è richiesto. Ciò consente la creazione di classi che non possono essere costruite anche quando si usa la riflessione. Tuttavia, l'utilizzo sun.misc.Unsafe
consente ancora di creare tali istanze.
Definire metodi con firma identica ma con tipo di ritorno diverso
Nella JPL, un metodo è identificato come unico dal nome e dai tipi di parametri non elaborati. In JBC, viene considerato anche il tipo di ritorno non elaborato.
Definire i campi che non differiscono per nome ma solo per tipo
Un file di classe può contenere diversi campi con lo stesso nome purché dichiarino un tipo di campo diverso. La JVM si riferisce sempre a un campo come una tupla di nome e tipo.
Lancia eccezioni controllate non dichiarate senza catturarle
Il runtime Java e il codice byte Java non sono a conoscenza del concetto di eccezioni verificate. È solo il compilatore Java che verifica che le eccezioni verificate vengano sempre rilevate o dichiarate se vengono generate.
Usa l'invocazione del metodo dinamico al di fuori delle espressioni lambda
La cosiddetta chiamata del metodo dinamico può essere utilizzata per qualsiasi cosa, non solo per le espressioni lambda di Java. L'uso di questa funzione consente ad esempio di cambiare la logica di esecuzione in fase di esecuzione. Molti linguaggi di programmazione dinamici che si riducono a JBC hanno migliorato le loro prestazioni usando queste istruzioni. Nel codice byte Java, è anche possibile emulare espressioni lambda in Java 7 in cui il compilatore non consentiva ancora l'uso della chiamata al metodo dinamico mentre JVM aveva già compreso l'istruzione.
Utilizzare identificatori che normalmente non sono considerati legali
Hai mai immaginato di usare spazi e un'interruzione di riga nel nome del tuo metodo? Crea il tuo JBC e buona fortuna per la revisione del codice. Gli unici caratteri non validi per gli identificatori sono .
, ;
, [
e /
. Inoltre, i metodi che non sono nominati <init>
o <clinit>
non possono contenere <
e >
.
Riassegna i final
parametri o il this
riferimento
final
i parametri non esistono in JBC e di conseguenza possono essere riassegnati. Qualsiasi parametro, incluso il this
riferimento, è memorizzato in un semplice array all'interno della JVM, ciò che consente di riassegnare il this
riferimento all'indice 0
all'interno di un singolo frame di metodo.
Riassegna i final
campi
Finché un campo finale viene assegnato all'interno di un costruttore, è legale riassegnare questo valore o addirittura non assegnarlo affatto. Pertanto, i seguenti due costruttori sono legali:
class Foo {
final int bar;
Foo() { } // bar == 0
Foo(Void v) { // bar == 2
bar = 1;
bar = 2;
}
}
Per i static final
campi, è anche consentito riassegnare i campi all'esterno dell'inizializzatore di classi.
Tratta i costruttori e l'inizializzatore di classe come se fossero metodi
Questa è più una caratteristica concettuale, ma i costruttori non sono trattati in modo diverso all'interno di JBC rispetto ai metodi normali. È solo il verificatore della JVM che assicura che i costruttori chiamino un altro costruttore legale. Oltre a ciò, è semplicemente una convenzione di denominazione Java che i costruttori devono essere chiamati <init>
e che viene chiamato l'inizializzatore di classe <clinit>
. Oltre a questa differenza, la rappresentazione di metodi e costruttori è identica. Come ha sottolineato Holger in un commento, puoi persino definire costruttori con tipi di ritorno diversi da void
o un inizializzatore di classe con argomenti, anche se non è possibile chiamare questi metodi.
Crea record asimmetrici * .
Quando si crea un record
record Foo(Object bar) { }
javac genererà un file di classe con un singolo campo denominato bar
, un metodo di accesso denominato bar()
e un costruttore che ne prende uno singolo Object
. Inoltre, bar
viene aggiunto un attributo record per . Generando manualmente un record, è possibile creare una forma del costruttore diversa, saltare il campo e implementare l'accessor in modo diverso. Allo stesso tempo, è ancora possibile far credere all'API di riflessione che la classe rappresenti un record effettivo.
Chiama qualsiasi metodo super (fino a Java 1.1)
Tuttavia, questo è possibile solo per le versioni Java 1 e 1.1. In JBC, i metodi vengono sempre inviati su un tipo di destinazione esplicito. Questo significa che per
class Foo {
void baz() { System.out.println("Foo"); }
}
class Bar extends Foo {
@Override
void baz() { System.out.println("Bar"); }
}
class Qux extends Bar {
@Override
void baz() { System.out.println("Qux"); }
}
è stato possibile implementare Qux#baz
per invocare Foo#baz
saltando sopra Bar#baz
. Mentre è ancora possibile definire un richiamo esplicito per chiamare un'altra implementazione di super metodo rispetto a quella della superclasse diretta, ciò non ha più alcun effetto nelle versioni Java dopo 1.1. In Java 1.1, questo comportamento era controllato impostando il ACC_SUPER
flag che abilitava lo stesso comportamento che chiama solo l'implementazione diretta della superclasse.
Definire una chiamata non virtuale di un metodo dichiarato nella stessa classe
In Java, non è possibile definire una classe
class Foo {
void foo() {
bar();
}
void bar() { }
}
class Bar extends Foo {
@Override void bar() {
throw new RuntimeException();
}
}
Il codice sopra riportato si tradurrà sempre in un RuntimeException
quando foo
viene invocato su un'istanza di Bar
. Non è possibile definire il Foo::foo
metodo per invocare il proprio bar
metodo in cui è definito Foo
. Come bar
è un metodo di istanza non privata, la chiamata è sempre virtuale. Con il codice byte, si può tuttavia definire l'invocazione per utilizzare il INVOKESPECIAL
codice operativo che collega direttamente la bar
chiamata del metodo Foo::foo
alla Foo
versione di. Questo codice operativo viene normalmente utilizzato per implementare invocazioni di super metodi ma è possibile riutilizzare il codice operativo per implementare il comportamento descritto.
Annotazioni di tipo a grana fine
In Java, le annotazioni vengono applicate in base al loro @Target
dichiarato dalle annotazioni. Utilizzando la manipolazione del codice byte, è possibile definire le annotazioni indipendentemente da questo controllo. Inoltre, è ad esempio possibile annotare un tipo di parametro senza annotare il parametro anche se l' @Target
annotazione si applica a entrambi gli elementi.
Definire qualsiasi attributo per un tipo o i suoi membri
All'interno del linguaggio Java, è possibile definire solo annotazioni per campi, metodi o classi. In JBC, puoi fondamentalmente incorporare qualsiasi informazione nelle classi Java. Per utilizzare queste informazioni, tuttavia, non è più possibile fare affidamento sul meccanismo di caricamento della classe Java, ma è necessario estrarre le meta informazioni da soli.
Overflow e implicitamente assegnare byte
, short
, char
e boolean
valori
Questi ultimi tipi primitivi non sono normalmente noti in JBC ma sono definiti solo per tipi di array o per descrittori di campi e metodi. All'interno delle istruzioni del codice byte, tutti i tipi nominati occupano lo spazio a 32 bit che consente di rappresentarli come int
. Ufficialmente, solo i int
, float
, long
ed double
esistono tipi del codice byte, che tutti hanno bisogno di conversione esplicita la regola del verificatore della JVM.
Non rilasciare un monitor
Un synchronized
blocco è in realtà composto da due istruzioni, una da acquisire e una da rilasciare un monitor. In JBC, puoi acquisirne uno senza rilasciarlo.
Nota : nelle recenti implementazioni di HotSpot, ciò porta invece IllegalMonitorStateException
alla fine di un metodo o a una versione implicita se il metodo viene terminato da un'eccezione stessa.
Aggiungi più di return
un'istruzione a un inizializzatore di tipo
In Java, anche un inizializzatore di tipo banale come
class Foo {
static {
return;
}
}
è illegale. Nel codice byte, l'inizializzatore del tipo viene trattato come qualsiasi altro metodo, vale a dire le istruzioni di ritorno possono essere definite ovunque.
Crea loop irriducibili
Il compilatore Java converte i loop in istruzioni goto nel codice byte Java. Tali istruzioni possono essere utilizzate per creare loop irriducibili, cosa che il compilatore Java non esegue mai.
Definire un blocco catch ricorsivo
Nel codice byte Java, è possibile definire un blocco:
try {
throw new Exception();
} catch (Exception e) {
<goto on exception>
throw Exception();
}
Un'istruzione simile viene creata implicitamente quando si utilizza un synchronized
blocco in Java in cui qualsiasi eccezione durante il rilascio di un monitor ritorna alle istruzioni per il rilascio di questo monitor. Normalmente, non dovrebbe verificarsi alcuna eccezione su tale istruzione, ma se ciò accadesse (ad es. Il deprecato ThreadDeath
), il monitor verrebbe comunque rilasciato.
Chiama qualsiasi metodo predefinito
Il compilatore Java richiede che siano soddisfatte diverse condizioni per consentire l'invocazione di un metodo predefinito:
- Il metodo deve essere il più specifico (non deve essere ignorato da un'interfaccia secondaria implementata da alcun tipo, inclusi i super tipi).
- Il tipo di interfaccia del metodo predefinito deve essere implementato direttamente dalla classe che chiama il metodo predefinito. Tuttavia, se l'interfaccia
B
estende l'interfaccia A
ma non sovrascrive un metodo A
, è comunque possibile invocare il metodo.
Per il codice byte Java, conta solo la seconda condizione. Il primo è tuttavia irrilevante.
Richiamare un metodo super su un'istanza che non lo è this
Il compilatore Java consente solo di invocare un metodo super (o interfaccia predefinito) su istanze di this
. Nel codice byte, tuttavia, è anche possibile richiamare il metodo super su un'istanza dello stesso tipo simile al seguente:
class Foo {
void m(Foo f) {
f.super.toString(); // calls Object::toString
}
public String toString() {
return "foo";
}
}
Accedi ai membri sintetici
Nel codice byte Java, è possibile accedere direttamente ai membri sintetici. Ad esempio, considera come nel seguente esempio Bar
si accede all'istanza esterna di un'altra istanza:
class Foo {
class Bar {
void bar(Bar bar) {
Foo foo = bar.Foo.this;
}
}
}
Questo è generalmente vero per qualsiasi campo, classe o metodo sintetico.
Definire informazioni di tipo generico non sincronizzate
Mentre il runtime Java non elabora tipi generici (dopo che il compilatore Java applica la cancellazione dei tipi), queste informazioni sono ancora associate a una classe compilata come meta informazioni e rese accessibili tramite l'API di reflection.
Il verificatore non controlla la coerenza di questi String
valori codificati con metadati . È quindi possibile definire informazioni su tipi generici che non corrispondono alla cancellazione. Come concezione, le seguenti affermazioni possono essere vere:
Method method = ...
assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());
Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);
Inoltre, la firma può essere definita non valida in modo da generare un'eccezione di runtime. Questa eccezione viene generata quando si accede alle informazioni per la prima volta quando viene valutata pigramente. (Simile ai valori di annotazione con un errore.)
Aggiungi le informazioni meta dei parametri solo per determinati metodi
Il compilatore Java consente di incorporare il nome del parametro e le informazioni sul modificatore durante la compilazione di una classe con il parameter
flag abilitato. Nel formato di file di classe Java, queste informazioni vengono comunque memorizzate per metodo, il che rende possibile incorporare tali informazioni sul metodo solo per determinati metodi.
Disordinare le cose e bloccare la tua JVM
Ad esempio, nel codice byte Java, è possibile definire di invocare qualsiasi metodo su qualsiasi tipo. Di solito, il verificatore si lamenterà se un tipo non è a conoscenza di tale metodo. Tuttavia, se invochi un metodo sconosciuto su un array, ho trovato un bug in alcune versioni di JVM in cui il verificatore mancherà questo e la tua JVM finirà una volta invocata l'istruzione. Questa non è una caratteristica però, ma è tecnicamente qualcosa che non è possibile con Java compilato javac . Java ha una sorta di doppia convalida. La prima convalida viene applicata dal compilatore Java, la seconda dalla JVM quando viene caricata una classe. Saltando il compilatore, potresti trovare un punto debole nella convalida del verificatore. Questa è piuttosto un'affermazione generale piuttosto che una caratteristica.
Annota il tipo di ricevitore di un costruttore quando non esiste una classe esterna
A partire da Java 8, metodi e costruttori non statici di classi interne possono dichiarare un tipo di ricevitore e annotare questi tipi. I costruttori di classi di livello superiore non possono annotare il loro tipo di ricevitore in quanto la maggior parte non ne dichiara uno.
class Foo {
class Bar {
Bar(@TypeAnnotation Foo Foo.this) { }
}
Foo() { } // Must not declare a receiver type
}
Poiché Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()
tuttavia restituisce un AnnotatedType
rappresentante Foo
, è possibile includere le annotazioni dei tipi per Foo
il costruttore direttamente nel file di classe in cui tali annotazioni vengono successivamente lette dall'API di reflection.
Utilizzare le istruzioni del codice byte non utilizzate / legacy
Dal momento che altri lo hanno chiamato, lo includerò anche io. In passato Java utilizzava subroutine dalle dichiarazioni JSR
e RET
. JBC conosceva anche il proprio tipo di indirizzo di ritorno per questo scopo. Tuttavia, l'uso di subroutine ha complicato notevolmente l'analisi del codice statico, motivo per cui queste istruzioni non vengono più utilizzate. Al contrario, il compilatore Java duplicherà il codice che compila. Tuttavia, questo in pratica crea una logica identica, motivo per cui non lo considero davvero per ottenere qualcosa di diverso. Allo stesso modo, potresti ad esempio aggiungere ilNOOP
istruzione di codice byte che non viene utilizzata neanche dal compilatore Java, ma ciò non consentirebbe in realtà di ottenere qualcosa di nuovo. Come sottolineato nel contesto, queste "istruzioni sulle funzionalità" menzionate vengono ora rimosse dall'insieme di codici operativi legali che le rendono ancora meno di una funzione.