Fonctionnalités bytecode non disponibles dans le langage Java


Existe-t-il actuellement (Java 6) des choses que vous pouvez faire en bytecode Java que vous ne pouvez pas faire à partir du langage Java?

Je sais que les deux sont Turing complets, alors lisez "peut faire" comme "peut faire beaucoup plus vite/mieux, ou simplement d'une manière différente".

Je pense à des bytecodes supplémentaires comme invokedynamic, qui ne peuvent pas être générés en utilisant Java, sauf que celui spécifique est pour une version future.

Author: Bart van Heukelom, 2011-07-26

9 answers

Pour autant que je sache, il n'y a pas de fonctionnalités majeures dans les bytecodes pris en charge par Java 6 qui ne sont pas également accessibles à partir du code source Java. La raison principale en est évidemment que le bytecode Java a été conçu avec le langage Java à l'esprit.

Cependant, certaines fonctionnalités ne sont pas produites par les compilateurs Java modernes:

  • Le ACC_SUPER drapeau:

    Il s'agit d'un indicateur qui peut être défini sur une classe et spécifie comment un cas de coin spécifique du invokespecial le bytecode est géré pour cette classe. Il est défini par tous les compilateurs Java modernes (où "modern" est >= Java 1.1, si je me souviens bien) et seuls les anciens compilateurs Java ont produit des fichiers de classe où cela n'était pas défini. Cet indicateur n'existe que pour des raisons de rétrocompatibilité. Notez qu'à partir de Java 7u51, ACC_SUPER est complètement ignoré pour des raisons de sécurité.

  • Le jsr/ret code d'octets.

    Ces bytecodes ont été utilisés pour implémenter des sous-routines (principalement pour implémenter finally blocs). Ils ne sont plus produits depuis Java 6 . La raison de leur dépréciation est qu'ils compliquent beaucoup la vérification statique sans grand gain (c'est-à-dire que le code utilisé peut presque toujours être ré-implémenté avec des sauts normaux avec très peu de frais généraux).

  • Ayant deux méthodes dans une classe qui ne diffèrent que par le type de retour.

    La spécification du langage Java n'autorise pas deux méthodes dans la même classe lorsqu'elles diffèrent uniquement dans leur retour type (même nom, même liste d'arguments ...). La spécification JVM cependant, n'a pas une telle restriction, donc un fichier de classe peut contenir deux de ces méthodes, il n'y a tout simplement aucun moyen de produire un tel fichier de classe en utilisant le compilateur Java normal. Il y a un bel exemple/explication dans cette réponse.

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

Après avoir travaillé avec le code d'octet Java pendant un certain temps et fait quelques recherches supplémentaires à ce sujet, voici un résumé de mes conclusions:

Exécuter du code dans un constructeur avant d'appeler un super constructeur ou de l'auxiliaire constructeur

Dans le langage de programmation Java (JPL), la première instruction d'un constructeur doit être une invocation d'un super constructeur ou d'un autre constructeur de la même classe. Ce n'est pas vrai pour le code d'octet Java (JBC). Dans le code octet, il est absolument légitime pour exécuter n'importe quel code avant un constructeur, tant que:

  • Un autre constructeur compatible est appelé à un moment donné après ce bloc de code.
  • Cet appel n'est pas dans une instruction conditionnelle.
  • Avant cet appel du constructeur, aucun champ de l'instance construite n'est lu et aucune de ses méthodes n'est invoquée. Cela implique que l'élément suivant.

Définir des champs d'instance avant d'appeler un super constructeur ou de l'auxiliaire constructeur

Comme mentionné précédemment, il est parfaitement légal de définir une valeur de champ d'une instance avant d'appeler un autre constructeur. Il existe même un hack hérité qui le rend capable d'exploiter cette "fonctionnalité" dans les versions Java antérieures à 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();
  }
}

De cette façon, un champ pourrait être défini avant l'appel du super constructeur, ce qui n'est cependant plus possible. Dans JBC, ce comportement peut toujours être implémenté.

Branche un super constructeur appel de

En Java, il n'est pas possible de définir un appel de constructeur comme

class Foo {
  Foo() { }
  Foo(Void v) { }
}

class Bar() {
  if(System.currentTimeMillis() % 2 == 0) {
    super();
  } else {
    super(null);
  }
}

Jusqu'à Java 7u23, le vérificateur de la machine virtuelle HotSpot a cependant manqué cette vérification, ce qui explique pourquoi c'était possible. Cela a été utilisé par plusieurs outils de génération de code comme une sorte de hack mais il n'est plus légal d'implémenter une classe comme celle-ci.

Ce dernier n'était qu'un bogue dans cette version du compilateur. Dans les nouvelles versions du compilateur, cela est à nouveau possible.

Définir un classe sans constructeur

Le compilateur Java implémentera toujours au moins un constructeur pour n'importe quelle classe. Dans le code d'octet Java, cela n'est pas requis. Cela permet la création de classes qui ne peuvent pas être construites même en utilisant la réflexion. Cependant, l'utilisation de sun.misc.Unsafe permet toujours la création de telles instances.

Définir des méthodes avec une signature identique mais avec un type de retour différent

Dans le JPL, une méthode est identifiée comme unique par son nom et ses types de paramètres bruts. Dans JBC, le type de retour brut est également pris en compte.

Définir des champs qui ne diffèrent pas par leur nom mais seulement par leur type

Un fichier de classe peut contenir plusieurs champs du même nom tant qu'ils déclarent un type de champ différent. La JVM fait toujours référence à un champ en tant que tuple de nom et de type.

Lancer des exceptions vérifiées non déclarées sans les attraper

Le runtime Java et le code d'octet Java ne sont pas conscients du concept d'exceptions vérifiées. Seul le compilateur Java vérifie que les exceptions vérifiées sont toujours interceptées ou déclarées si elles sont levées.

Utiliser l'invocation de méthode dynamique en dehors des expressions lambda

La soi-disant invocation de méthode dynamique peut être utilisée pour n'importe quoi, pas seulement pour les expressions lambda de Java. L'utilisation de cette fonctionnalité permet par exemple de passer d'une logique d'exécution lors de l'exécution. De nombreux langages de programmation dynamiques cela se résume à JBC a amélioré leurs performances en utilisant cette instruction. Dans le code d'octet Java, vous pouvez également émuler des expressions lambda dans Java 7 où le compilateur n'autorisait pas encore l'utilisation de l'invocation de méthode dynamique alors que la JVM comprenait déjà l'instruction.

Utiliser des identifiants qui ne sont normalement pas considérés comme légaux

Avez-vous déjà imaginé utiliser des espaces et un saut de ligne dans le nom de votre méthode? Créez votre propre JBC et bonne chance pour la révision du code. Les seuls caractères illégaux pour les identificateurs sont ., ;, [ et /. De plus, les méthodes qui ne sont pas nommées <init> ou <clinit> ne peuvent pas contenir < et >.

Réaffecter les paramètres final ou la référence this

final les paramètres n'existent pas dans JBC et peuvent donc être réaffectés. Tout paramètre, y compris la référence this n'est stocké que dans un tableau simple au sein de la JVM ce qui permet de réaffecter la référence this à l'index 0 dans un cadre de méthode unique.

Réaffecter final champs

Tant qu'un champ final est attribué dans un constructeur, il est légal de réaffecter cette valeur ou même de ne pas assigner de valeur du tout. Par conséquent, les deux constructeurs suivants sont légaux:

class Foo {
  final int bar;
  Foo() { } // bar == 0
  Foo(Void v) { // bar == 2
    bar = 1;
    bar = 2;
  }
}

Pour static final champs, il est même permis de réaffecter les champs en dehors de l'initialiseur de classe.

Traitez les constructeurs et l'initialiseur de classe comme s'ils étaient les méthodes de

Il s'agit plus d'une caractéristique conceptuelle mais les constructeurs ne sont pas traités différemment dans JBC que les méthodes normales. C'est seulement le vérificateur de la JVM qui assure que les constructeurs appellent un autre constructeur légal. En dehors de cela, il s'agit simplement d'une convention de nommage Java selon laquelle les constructeurs doivent être appelés <init> et que l'initialiseur de classe est appelé <clinit>. Outre cette différence, la représentation des méthodes et des constructeurs est identique. Comme Holger souligné dans un commentaire, vous pouvez même définir des constructeurs avec des types de retour autres que void ou un initialiseur de classe avec des arguments, même s'il n'est pas possible d'appeler ces méthodes.

Appelez n'importe quelle super méthode (jusqu'à Java 1.1)

Cependant, cela n'est possible que pour les versions 1 et 1.1 de Java. Dans JBC, les méthodes sont toujours distribuées sur un type cible explicite. Cela signifie que pour

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"); }
}

Il était possible d'implémenter Qux#baz pour invoquer Foo#baz en sautant plus de Bar#baz. Bien qu'il soit toujours possible de définir une invocation explicite pour appeler une autre implémentation de super méthode que celle de la super classe directe, cela n'a plus aucun effet dans les versions Java après 1.1. Dans Java 1.1, ce comportement était contrôlé en définissant l'indicateur ACC_SUPER qui activerait le même comportement qui appelle uniquement l'implémentation de la super classe directe.

Définir un appel non virtuel d'une méthode déclarée dans la même classe

Dans Java, il n'est pas possible de définir une classe

class Foo {
  void foo() {
    bar();
  }
  void bar() { }
}

class Bar extends Foo {
  @Override void bar() {
    throw new RuntimeException();
  }
}

Le code ci-dessus entraînera toujours une RuntimeException lorsque foo est appelée sur une instance de Bar. Il n'est pas possible de définir la méthode Foo::foo pour invoquer la sienne bar méthode définie dans Foo. Comme bar est une méthode d'instance non privée, l'appel est toujours virtuel. Avec le byte code, on peut cependant définir l'invocation pour utiliser l'opcode INVOKESPECIAL qui relie directement l'appel de méthode bar dans Foo::foo à Foo's version. Cet opcode est normalement utilisé pour implémenter des invocations de super méthode mais vous pouvez réutiliser l'opcode pour implémenter le comportement décrit.

Annotations de type à grain fin

En Java, les annotations sont appliquées en fonction de leur @Target que les annotations déclarent. En utilisant la manipulation de code octet, il est possible de définir des annotations indépendamment de ce contrôle. De plus, il est par exemple possible d'annoter un type de paramètre sans annoter le paramètre même si l'annotation @Target s'applique aux deux éléments.

Définir n'importe quel attribut pour un type ou ses membres

Dans le langage Java, il est uniquement possible de définir des annotations pour des champs, des méthodes ou des classes. Dans JBC, vous pouvez essentiellement intégrer n'importe quelle information dans les classes Java. Pour utiliser ces informations, vous ne pouvez cependant plus compter sur le mécanisme de chargement de classe Java mais vous devez extraire les méta-informations par vous-même.

Débordement et, implicitement, d'attribuer byte, short, char et boolean valeurs

Ces derniers types primitifs ne sont normalement pas connus dans JBC mais ne sont définis que pour les types de tableau ou pour les descripteurs de champ et de méthode. Dans les instructions de code octet, tous les types nommés prennent l'espace 32 bits qui permet de les représenter comme int. Officiellement, seul le int, float, long et double les types existent dans le code octet qui ont tous besoin d'une conversion explicite par la règle de la JVM verifier.

Ne pas libérer un moniteur

Un bloc synchronized est en fait composé de deux instructions, une pour acquérir et une pour libérer un moniteur. Dans JBC, vous pouvez en acquérir un sans le libérer.

Note : Dans les implémentations récentes de HotSpot, cela conduit plutôt à un IllegalMonitorStateException à la fin d'une méthode ou à une version implicite si la méthode est terminée par une exception elle-même.

Ajouter plus d'une instruction return à un type initialiseur

En Java, même un initialiseur de type trivial tel que

class Foo {
  static {
    return;
  }
}

Est illégal. Dans le code byte, l'initialiseur de type est traité comme n'importe quelle autre méthode, c'est-à-dire que les instructions return peuvent être définies n'importe où.

Créer des boucles irréductibles

Le compilateur Java convertit les boucles en instructions goto en code octet Java. De telles instructions peuvent être utilisées pour créer des boucles irréductibles, ce que le compilateur Java ne fait jamais.

Définir un récursif bloc de capture

En code octet Java, vous pouvez définir un bloc:

try {
  throw new Exception();
} catch (Exception e) {
  <goto on exception>
  throw Exception();
}

Une instruction similaire est créée implicitement lors de l'utilisation d'un bloc synchronized en Java où toute exception lors de la libération d'un moniteur retourne à l'instruction de libération de ce moniteur. Normalement, aucune exception ne devrait se produire sur une telle instruction, mais si elle le faisait (par exemple le ThreadDeath obsolète), le moniteur serait toujours libéré.

Appelez n'importe quelle méthode par défaut

Le compilateur Java nécessite plusieurs conditions à remplir pour permettre l'invocation d'une méthode par défaut:

  1. La méthode doit être la plus spécifique (ne doit pas être remplacée par une sous-interface implémentée par tout type, y compris les super types).
  2. Le type d'interface de la méthode par défaut doit être implémenté directement par la classe qui appelle la méthode par défaut. Toutefois, si l'interface B s'étend de l'interface A, mais ne remplace pas une méthode dans A, l' la méthode peut encore être invoquée.

Pour le code octet Java, seule la deuxième condition compte. La première est cependant pas pertinent.

Invoquer une super méthode sur une instance qui n'est pas this

Le compilateur Java ne permet d'invoquer une méthode super (ou interface default) que sur les instances de this. En byte code, il est cependant également possible d'invoquer la super méthode sur une instance du même type similaire à la suivante:

class Foo {
  void m(Foo f) {
    f.super.toString(); // calls Object::toString
  }
  public String toString() {
    return "foo";
  }
}

Accès membres synthétiques

En code octet Java, il est possible d'accéder directement aux membres synthétiques. Par exemple, considérez comment, dans l'exemple suivant, l'instance externe d'une autre instance Bar est accessible:

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

Ceci est généralement vrai pour tout champ, classe ou méthode synthétique.

Définir des informations de type générique désynchronisées

Alors que le runtime Java ne traite pas les types génériques (après que le compilateur Java applique l'effacement de type), ceci les informations sont toujours attachées à une classe compilée en tant que méta-informations et rendues accessibles via l'API reflection.

Le vérificateur ne vérifie pas la cohérence de ces valeurs codées par les métadonnées String. Il est donc possible de définir des informations sur des types génériques qui ne correspondent pas à l'effacement. En conséquence, les assertions suivantes peuvent être vraies:

Method method = ...
assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());

Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);

De plus, la signature peut être définie comme invalide de sorte qu'une exception d'exécution est levée. Cette exception est jeté lorsque l'information est accédée pour la première fois car elle est évaluée paresseusement. (Similaire aux valeurs d'annotation avec une erreur.)

Ajouter des méta - informations de paramètre uniquement pour certaines méthodes

Le compilateur Java permet d'intégrer le nom du paramètre et les informations de modification lors de la compilation d'une classe avec l'indicateur parameter activé. Dans le format de fichier de classe Java, ces informations sont cependant stockées par méthode ce qui permet d'intégrer uniquement de telles informations de méthode pour certaines méthodes.

Gâchez les choses et plantez votre JVM

Par exemple, dans le code d'octet Java, vous pouvez définir pour appeler n'importe quelle méthode sur n'importe quel type. Habituellement, le vérificateur se plaindra si un type n'est pas connu d'une telle méthode. Cependant, si vous invoquez une méthode inconnue sur un tableau, j'ai trouvé un bogue dans une version de la JVM où le vérificateur manquera cela et votre JVM se terminera une fois l'instruction invoquée. Ce n'est pas une caractéristique cependant, mais c'est techniquement quelque chose qui n'est pas possible avec javac Java compilé. Java a une sorte de double validation. La première validation est appliquée par le compilateur Java, la seconde par la JVM lorsqu'une classe est chargée. En sautant le compilateur, vous pourriez trouver un point faible dans la validation du vérificateur. Ceci est plutôt une déclaration générale qu'une caractéristique, cependant.

Annoter le type de récepteur d'un constructeur lorsqu'il n'y a pas de classe externe

Depuis Java 8, méthodes non statiques et les constructeurs de classes internes peuvent déclarer un type de récepteur et annoter ces types. Les constructeurs de classes de niveau supérieur ne peuvent pas annoter leur type de récepteur car ils n'en déclarent pas un.

class Foo {
  class Bar {
    Bar(@TypeAnnotation Foo Foo.this) { }
  }
  Foo() { } // Must not declare a receiver type
}

Étant donné que Foo.class.getDeclaredConstructor().getAnnotatedReceiverType() renvoie cependant un AnnotatedType représentant Foo, il est possible d'inclure des annotations de type pour le constructeur de Foo directement dans le fichier de classe où ces annotations sont ensuite lues par l'API reflection.

Utiliser le code octet inutilisé / hérité instructions

Puisque d'autres l'ont nommé, je l'inclurai aussi. Java utilisait autrefois des sous-programmes par les instructions JSR et RET. JBC connaissait même son propre type d'adresse de retour à cet effet. Cependant, l'utilisation de sous-programmes a compliqué l'analyse de code statique, ce qui explique pourquoi ces instructions ne sont plus utilisées. Au lieu de cela, le compilateur Java dupliquera le code qu'il compile. Cependant, cela crée fondamentalement une logique identique, c'est pourquoi je ne considère pas vraiment pour atteindre quelque chose de différent. De même, vous pouvez par exemple ajouter l'instruction NOOP byte code qui n'est pas non plus utilisée par le compilateur Java mais cela ne vous permettrait pas non plus de réaliser quelque chose de nouveau. Comme indiqué dans le contexte, ces "instructions de fonctionnalité" mentionnées sont maintenant supprimées de l'ensemble des opcodes légaux, ce qui les rend encore moins d'une fonctionnalité.

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

Voici quelques fonctionnalités qui peuvent être effectuées en bytecode Java mais pas en code source Java:

  • Lancer une exception vérifiée à partir d'une méthode sans déclarer que la méthode la lance. Les exceptions cochées et non cochées sont une chose qui est vérifiée uniquement par le compilateur Java, pas par la JVM. Pour cette raison, par exemple, Scala peut lancer des exceptions vérifiées à partir de méthodes sans les déclarer. Bien qu'avec les génériques Java, il existe une solution de contournement appelée sneaky lancer .

  • Avoir deux méthodes dans une classe qui ne diffèrent que par le type de retour, comme déjà mentionné dans Réponse de Joachim: La spécification du langage Java n'autorise pas deux méthodes dans la même classe lorsqu'elles diffèrent uniquement dans leur type de retour (c'est-à-dire même nom, même liste d'arguments, ...). La spécification JVM n'a cependant pas une telle restriction, donc un fichier de classe peut contenir deux de ces méthodes, il n'y a tout simplement aucun moyen de produire un tel fichier de classe utilisation du compilateur Java normal. Il y a un bel exemple/explication dans cette réponse.

 12
Author: Esko Luontola, 2017-05-23 11:47:36
  • GOTO peut être utilisé avec des étiquettes pour créer vos propres structures de contrôle (autres que for while etc)
  • Vous pouvez remplacer la variable locale this dans une méthode
  • En combinant les deux, vous pouvez créer un bytecode optimisé pour l'appel de queue (je le fais dans JCompilo)

En tant que point connexe, vous pouvez obtenir le nom du paramètre pour les méthodes si elles sont compilées avec debug (Paranamer le fait en lisant le bytecode

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

Peut-être que la section 7A dans ce documentest d'intérêt, bien qu'il s'agisse de pièges de bytecode plutôt que de fonctionnalités de bytecode .

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

En langage Java, la première instruction d'un constructeur doit être un appel au constructeur de la super classe. Bytecode n'a pas cette limitation, la règle est plutôt que le constructeur de super classe ou un autre constructeur de la même classe doit être appelé pour l'objet avant d'accéder aux membres. Cela devrait permettre plus de liberté comme:

  • Créez une instance d'un autre objet, stockez-le dans une variable locale (ou une pile) et passez-le en tant que paramètre au constructeur de super classe pendant toujours garder la référence dans cette variable pour une autre utilisation.
  • Appelez différents autres constructeurs en fonction d'une condition. Cela devrait être possible: Comment appeler un constructeur différent conditionnellement en Java?

Je ne les ai pas testés, alors corrigez-moi si je me trompe.

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

Quelque chose que vous pouvez faire avec du code octet, plutôt que du code Java simple, est de générer du code qui peut être chargé et exécuté sans compilateur. De nombreux systèmes ont JRE plutôt que JDK et si vous voulez générer du code dynamiquement, il peut être préférable, sinon plus facile, de générer du code octet au lieu du code Java doit être compilé avant de pouvoir être utilisé.

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

J'ai écrit un optimiseur de bytecode quand j'étais un I-Play, (il a été conçu pour réduire la taille du code pour les applications J2ME). Une fonctionnalité que j'ai ajoutée était la possibilité d'utiliser le bytecode en ligne (similaire au langage d'assemblage en ligne en C++). J'ai réussi à réduire la taille d'une fonction qui faisait partie d'une méthode de bibliothèque en utilisant l'instruction DUP, car j'ai besoin de la valeur deux fois. J'avais aussi des instructions zéro octet (si vous appelez une méthode qui prend un caractère et que vous voulez passer un int, que vous savez ne le fait pas besoin d'être moulé J'ai ajouté int2char(var) pour remplacer char(var) et cela supprimerait l'instruction i2c pour réduire la taille du code. Je l'ai également fait flotter a = 2.3; flotter b = 3.4; flotter c = a + b; et cela serait converti en point fixe (plus rapide, et certains J2ME ne prenaient pas en charge les virgule flottante).

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

En Java, si vous essayez de remplacer une méthode publique par une méthode protégée (ou toute autre réduction d'accès), vous obtenez une erreur: "tentative d'assigner des privilèges d'accès plus faibles". Si vous le faites avec le bytecode JVM, le vérificateur le convient et vous pouvez appeler ces méthodes via la classe parente comme si elles étaient publiques.

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