Funzionalità Bytecode non disponibili nel linguaggio Java


Ci sono attualmente (Java 6) cose che puoi fare in Java bytecode che non puoi fare all'interno del linguaggio Java?

So che entrambi sono completi, quindi leggi "può fare" come "può fare significativamente più velocemente/meglio, o solo in un modo diverso".

Sto pensando a bytecode extra come invokedynamic, che non possono essere generati usando Java, tranne che uno specifico è per una versione futura.

Author: Bart van Heukelom, 2011-07-26

9 answers

Per quanto ne so non ci sono caratteristiche importanti nei bytecode supportati da Java 6 che non sono accessibili anche dal codice sorgente Java. La ragione principale di questo è ovviamente che il bytecode Java è stato progettato con il linguaggio Java in mente.

Ci sono alcune caratteristiche che non sono prodotte dai moderni compilatori Java, tuttavia:

  • Il ACC_SUPER contrassegno:

    Questo è un flag che può essere impostato su una classe e specifica come un caso d'angolo specifico del invokespecial bytecode viene gestito per questa classe. È impostato da tutti i moderni compilatori Java (dove "moderno" è >= Java 1.1, se non ricordo male) e solo i compilatori Java antichi producevano file di classe in cui questo era non impostato. Questo flag esiste solo per ragioni di retrocompatibilità. Si noti che a partire da Java 7u51, ACC_SUPER viene ignorato completamente a causa di motivi di sicurezza.

  • Il jsr/ret bytecode.

    Questi bytecode sono stati utilizzati per implementare sotto-routine (principalmente per l'implementazione finally blocchi). Sono non più prodotti da Java 6. Il motivo della loro deprecazione è che complicano molto la verifica statica senza grandi guadagni (cioè il codice che usa può quasi sempre essere ri-implementato con salti normali con un sovraccarico molto piccolo).

  • Avere due metodi in una classe che differiscono solo nel tipo di ritorno.

    La specifica del linguaggio Java non consente due metodi nella stessa classe quando differiscono solo nel loro ritorno tipo (cioè stesso nome, stesso elenco di argomenti, ...). La specifica JVM tuttavia, non ha tale restrizione, quindi un file di classe può contenere due di questi metodi, non c'è modo di produrre un file di classe simile usando il normale compilatore Java. C'è un bel esempio/spiegazione in questa risposta.

 59
Author: Joachim Sauer, 2017-05-23 11:33:19

Dopo aver lavorato con il codice byte Java per un po ' e aver fatto qualche ricerca aggiuntiva su questo argomento, ecco un riassunto delle mie scoperte:

Eseguire il codice in un costruttore prima di chiamare un costruttore super o un costruttore ausiliario

Nel linguaggio di programmazione Java (JPL), la prima istruzione di un costruttore deve essere un'invocazione di un super costruttore o di un altro costruttore della stessa classe. Questo non è vero per Java byte code (JBC). All'interno del codice byte, è assolutamente legittimo eseguire qualsiasi codice prima di un costruttore, purché:

  • Un altro costruttore compatibile viene chiamato in qualche momento dopo questo blocco di codice.
  • Questa chiamata non è all'interno di 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 di istanza prima di chiamare un super costruttore o ausiliario costruttore

Come accennato in precedenza, è perfettamente legale impostare un valore di campo di un'istanza prima di chiamare un altro costruttore. Esiste anche un hack legacy che lo rende in grado di sfruttare questa "funzionalità" nelle versioni Java prima di 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.

Branch un super costruttore chiama

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 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 solo un bug in questa versione del compilatore. Nelle versioni più recenti del compilatore, questo è di nuovo possibile.

Definire a classe senza 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 utilizza reflection. Tuttavia, l'utilizzo di sun.misc.Unsafe consente ancora la creazione di tali istanze.

Definire metodi con firma identica ma con tipo di ritorno diverso

Nel JPL, un metodo è identificato come univoco dal suo nome e i suoi tipi di parametri grezzi. In JBC, viene inoltre considerato il tipo di ritorno non elaborato.

Definire 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.

Genera eccezioni controllate non dichiarate senza catturarle

Il runtime Java e il codice byte Java non sono a conoscenza del concetto di eccezioni controllate. È solo il compilatore Java che verifica che le eccezioni controllate vengano sempre catturate o dichiarate se vengono lanciate.

Usa l'invocazione del metodo dinamico al di fuori delle espressioni lambda

La cosiddetta chiamata al metodo dinamico può essere utilizzata per qualsiasi cosa, non solo per le espressioni lambda di Java. L'utilizzo di questa funzione consente ad esempio di disattivare la logica di esecuzione in fase di runtime. Molti linguaggi di programmazione dinamici che si riducono a JBC hanno migliorato le loro prestazioni usando questa istruzione. Nel codice byte Java, è anche possibile emulare espressioni lambda in Java 7 in cui il compilatore non ha ancora consentito l'uso di invocazione del metodo dinamico mentre la JVM ha 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 non denominati <init> o <clinit> non possono contenere < e >.

Riassegnare i parametri final o il riferimento this

final i parametri non esistono in JBC e possono quindi essere riassegnati. Qualsiasi parametro, incluso il riferimento this viene memorizzato solo in un semplice array all'interno della JVM che consente di riassegnare il riferimento this all'indice 0 all'interno di un singolo frame metodo.

Riassegna final campi

Finché un campo finale viene assegnato all'interno di un costruttore, è legale riassegnare questo valore o addirittura non assegnare affatto un valore. 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 campi static final, è anche consentito riassegnare i campi al di fuori di l'inizializzatore di classe.

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 questo, è semplicemente una convenzione di denominazione Java che i costruttori devono essere chiamati <init> e che l'inizializzatore di classe è chiamato <clinit>. Oltre a questa differenza, la rappresentazione di metodi e costruttori è identica. Come Holger sottolineato in un commento, è anche possibile definire costruttori con tipi di ritorno diversi da void o un inizializzatore di classe con argomenti, anche se non è possibile chiamare questi metodi.

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. Ciò 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 richiamare Foo#baz durante il salto passo Bar#baz. Mentre è ancora possibile definire un'invocazione esplicita per chiamare un'altra implementazione del super metodo rispetto a quella della super classe diretta, questo non ha più alcun effetto nelle versioni Java dopo 1.1. In Java 1.1, questo comportamento è stato controllato impostando il flag ACC_SUPER che abiliterebbe lo stesso comportamento che chiama solo l'implementazione della super classe diretta.

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 genererà sempre un RuntimeException quando foo viene richiamato su un'istanza di Bar. Non è possibile definire il metodo Foo::foo per richiamare il proprio bar metodo definito in Foo. Poiché bar è un metodo di istanza non privato, la chiamata è sempre virtuale. Con il codice byte, si può tuttavia definire l'invocazione per utilizzare l'opcode INVOKESPECIAL che collega direttamente la chiamata al metodo bar in Foo::foo a Foo versione. Questo opcode viene normalmente utilizzato per implementare le invocazioni del super metodo, ma è possibile riutilizzare l'opcode per implementare il comportamento descritto.

Annotazioni di tipo a grana fine

In Java, le annotazioni vengono applicate in base al loro @Target che le annotazioni dichiarano. Utilizzando la manipolazione del codice byte, è possibile definire annotazioni indipendentemente da questo controllo. Inoltre, è ad esempio possibile annotare un tipo di parametro senza annotare il parametro anche se l'annotazione @Target 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.

Troppo pieno e implicitamente assegnare byte, short, char eboolean valori

Questi ultimi tipi primitivi non sono normalmente noti in JBC, ma sono definiti solo per i tipi di array o per i descrittori di campo e metodo. All'interno delle istruzioni del codice byte, tutti i tipi denominati occupano lo spazio a 32 bit che consente di rappresentarli come int. Ufficialmente, solo il int, float, long e i tipi double esistono all'interno del codice di byte che richiedono tutti una conversione esplicita secondo la regola della JVM Verificatore.

Non rilasciare un monitor

Un blocco synchronized è in realtà costituito da due istruzioni, una per acquisire e una per rilasciare un monitor. In JBC, è possibile acquisire uno senza rilasciarlo.

Nota: Nelle recenti implementazioni di HotSpot, questo porta invece a un IllegalMonitorStateException alla fine di un metodo o a un rilascio implicito se il metodo è terminato da un'eccezione stessa.

Aggiungere più di un'istruzione return a un tipo inizializzatore

In Java, anche un inizializzatore di tipo banale come

class Foo {
  static {
    return;
  }
}

È illegale. Nel codice byte, l'inizializzatore di tipo viene trattato come qualsiasi altro metodo, ovvero le istruzioni return 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, che il compilatore Java non fa mai.

Definire un ricorsivo blocco di cattura

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 blocco synchronized in Java in cui qualsiasi eccezione durante il rilascio di un monitor ritorna all'istruzione per il rilascio di questo monitor. Normalmente, non dovrebbe verificarsi alcuna eccezione su tale istruzione, ma se lo facesse (ad esempio 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:

  1. Il metodo deve essere il più specifico (non deve essere sovrascritto da un'interfaccia secondaria implementata da qualsiasi tipo, inclusi i tipi super).
  2. Il tipo di interfaccia del metodo predefinito deve essere implementato direttamente dalla classe che sta chiamando il metodo predefinito. Tuttavia, se l'interfaccia B estende l'interfaccia A ma non sovrascrive un metodo in A, il il metodo può ancora essere invocato.

Per il codice byte Java, conta solo la seconda condizione. Il primo è tuttavia irrilevante.

Invoca un metodo super su un'istanza che non lo èthis

Il compilatore Java consente solo di richiamare un metodo super (o di default dell'interfaccia) sulle istanze di this. Nel codice byte, è tuttavia anche possibile invocare 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";
  }
}

Accesso membri sintetici

Nel codice byte Java, è possibile accedere direttamente ai membri sintetici. Ad esempio, si consideri come nell'esempio seguente si accede all'istanza esterna di un'altra istanza Bar:

class Foo {
  class Bar { 
    void bar(Bar bar) {
      Foo foo = bar.Foo.this;
    }
  }
}

Questo è generalmente vero per qualsiasi campo sintetico, classe o metodo.

Definire informazioni di tipo generico non sincronizzate

Mentre il runtime Java non elabora tipi generici (dopo che il compilatore Java applica la cancellazione del tipo), questo le informazioni sono ancora collegate a una classe compilata come meta informazioni e rese accessibili tramite l'API reflection.

Il verificatore non controlla la coerenza di questi metadati String-valori codificati. È quindi possibile definire informazioni sui tipi generici che non corrispondono alla cancellazione. Come concequenza, le seguenti asserzioni 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 tale che venga generata un'eccezione di runtime. Questa eccezione è gettato quando si accede alle informazioni per la prima volta in quanto viene valutato pigramente. (Simile ai valori di annotazione con un errore.)

Aggiungi meta informazioni sui 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 flag parameter abilitato. Nel formato di file di classe Java, queste informazioni sono tuttavia memorizzate per metodo, ciò che rende possibile incorporare solo tali informazioni sul metodo per alcuni metodi.

Incasina le cose e blocca duramente la tua JVM

Ad esempio, nel codice byte Java, è possibile definire di richiamare qualsiasi metodo su qualsiasi tipo. Di solito, il verificatore si lamenterà se un tipo non conosce un 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 terminerà una volta richiamata l'istruzione. Questa non è certo una caratteristica però, ma è tecnicamente qualcosa che non è possibile con javac Java compilato. 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 una dichiarazione generale che una caratteristica, però.

Annota il tipo di ricevitore di un costruttore quando non esiste una classe esterna

Da Java 8, metodi non statici e i costruttori di classi interne possono dichiarare un tipo di ricevitore e annotare questi tipi. I costruttori di classi di primo livello 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() restituisce tuttavia un AnnotatedType che rappresenta Foo, è possibile includere annotazioni di tipo per il costruttore di Foo direttamente nel file di classe in cui queste annotazioni vengono successivamente lette dall'API reflection.

Usa codice byte inutilizzato / legacy istruzioni

Dal momento che altri lo hanno chiamato, lo includerò anche io. Java in precedenza faceva uso di subroutine dalle istruzioni JSR e RET. JBC conosceva anche il proprio tipo di indirizzo di ritorno per questo scopo. Tuttavia, l'uso di subroutine ha complicato eccessivamente l'analisi del codice statico, motivo per cui queste istruzioni non vengono più utilizzate. Invece, il compilatore Java duplicherà il codice che compila. Tuttavia, questo fondamentalmente crea una logica identica, motivo per cui non considero davvero per ottenere qualcosa di diverso. Allo stesso modo, potresti ad esempio aggiungere l'istruzione NOOP byte code che non è utilizzata dal compilatore Java, ma questo non ti permetterebbe di ottenere qualcosa di nuovo. Come sottolineato nel contesto, queste "istruzioni di funzionalità" menzionate sono ora rimosse dal set di opcode legali che le rendono ancora meno di una funzionalità.

 386
Author: Rafael Winterhalter, 2017-05-23 11:55:19

Ecco alcune funzionalità che possono essere eseguite in bytecode Java ma non nel codice sorgente Java:

  • Lanciare un'eccezione controllata da un metodo senza dichiarare che il metodo lo genera. Le eccezioni controllate e deselezionate sono una cosa che viene controllata solo dal compilatore Java, non dalla JVM. Per questo motivo, ad esempio, Scala può generare eccezioni controllate dai metodi senza dichiararle. Anche se con Java generics c'è una soluzione chiamata subdolo lancia .

  • Avere due metodi in una classe che differiscono solo nel tipo di ritorno, come già menzionato in La risposta di Joachim: La specifica del linguaggio Java non consente due metodi nella stessa classe quando differiscono solo nel loro tipo di ritorno (cioè stesso nome, stesso elenco di argomenti, ...). La specifica JVM tuttavia, non ha tale restrizione, quindi un file di classe può contenere due di questi metodi, non c'è modo di produrre un file di classe del genere utilizzando il normale compilatore Java. C'è un bel esempio/spiegazione in questa risposta.

 12
Author: Esko Luontola, 2017-05-23 11:47:36
  • GOTO può essere utilizzato con le etichette per creare le proprie strutture di controllo (diverse da for while ecc)
  • È possibile sovrascrivere la variabile locale this all'interno di un metodo
  • Combinando entrambi questi è possibile creare create tail call bytecode ottimizzato (lo faccio in JCompilo)

Come punto correlato è possibile ottenere il nome del parametro per i metodi se compilato con debug (Paranamer lo fa leggendo il bytecode

 6
Author: Daniel Worthington-Bodart, 2013-02-14 16:50:37

Forse sezione 7A in questo documento è di interesse, anche se si tratta di bytecode insidie piuttosto che bytecode caratteristiche .

 4
Author: eljenso, 2011-07-26 09:19:38

In linguaggio Java la prima istruzione in un costruttore deve essere una chiamata al costruttore di super classe. Bytecode non ha questa limitazione, invece la regola è che il costruttore di super classe o un altro costruttore nella stessa classe deve essere chiamato per l'oggetto prima di accedere ai membri. Ciò dovrebbe consentire una maggiore libertà come:

  • Crea un'istanza di un altro oggetto, memorizzala in una variabile locale (o stack) e passala come parametro al costruttore di super classe mentre ancora mantenendo il riferimento in quella variabile per altro uso.
  • Chiama diversi altri costruttori in base a una condizione. Questo dovrebbe essere possibile: Come chiamare un costruttore diverso condizionatamente in Java?

Non li ho testati, quindi per favore correggimi se sbaglio.

 3
Author: msell, 2017-05-23 12:10:54

Qualcosa che puoi fare con il codice byte, piuttosto che con il semplice codice Java, è generare codice che può essere caricato ed eseguito senza un compilatore. Molti sistemi hanno JRE piuttosto che JDK e se si desidera generare codice dinamicamente potrebbe essere meglio, se non più semplice, generare codice byte invece che il codice Java deve essere compilato prima di poter essere utilizzato.

 2
Author: Peter Lawrey, 2011-07-26 09:11:35

Ho scritto un bytecode optimizer quando ero un I-Play, (è stato progettato per ridurre le dimensioni del codice per le applicazioni J2ME). Una caratteristica che ho aggiunto è stata la possibilità di utilizzare bytecode inline (simile al linguaggio assembly inline in C++). Sono riuscito a ridurre la dimensione di una funzione che faceva parte di un metodo di libreria utilizzando l'istruzione DUP, poiché ho bisogno del valore due volte. Ho anche avuto istruzioni a zero byte (se stai chiamando un metodo che prende un carattere e vuoi passare un int, che sai no ho aggiunto int2char (var) per sostituire char(var) e rimuoverebbe l'istruzione i2c per ridurre la dimensione del codice. Ho anche fatto float a = 2.3; float b = 3.4; float c = a + b; e questo sarebbe stato convertito in punto fisso (più veloce, e anche alcuni J2ME non supportavano il punto mobile).

 1
Author: nharding, 2015-04-17 15:48:00

In Java, se si tenta di sovrascrivere un metodo pubblico con un metodo protetto (o qualsiasi altra riduzione dell'accesso), viene visualizzato un errore: "tentativo di assegnare privilegi di accesso più deboli". Se lo fai con il bytecode JVM, il verificatore va bene con esso e puoi chiamare questi metodi tramite la classe genitore come se fossero pubblici.

 0
Author: Joseph Sible, 2018-01-22 02:44:02