Après avoir travaillé avec le code octet Java pendant un certain temps et effectué des recherches supplémentaires à ce sujet, voici un résumé de mes résultats:
Exécuter du code dans un constructeur avant d'appeler un super constructeur ou un constructeur auxiliaire
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 le cas pour le code d'octet Java (JBC). Dans le code d'octet, il est absolument légitime d'exécuter n'importe quel code avant un constructeur, à condition que:
- Un autre constructeur compatible est appelé à un moment donné après ce bloc de code.
- Cet appel ne fait pas partie d'une instruction conditionnelle.
- Avant cet appel de constructeur, aucun champ de l'instance construite n'est lu et aucune de ses méthodes n'est appelée. Cela implique l'élément suivant.
Définissez les champs d'instance avant d'appeler un super constructeur ou un constructeur auxiliaire
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 permet 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 que le super constructeur ne soit appelé, ce qui n'est cependant plus possible. Dans JBC, ce comportement peut toujours être implémenté.
Branche un appel de super constructeur
En Java, il n'est pas possible de définir un appel 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 HotSpot VM a cependant manqué ce contrôle, c'est pourquoi il é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 versions plus récentes du compilateur, c'est encore possible.
Définir une classe sans aucun 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 obligatoire. Cela permet la création de classes qui ne peuvent pas être construites même en utilisant la réflexion. Cependant, l'utilisation sun.misc.Unsafe
permet toujours de créer 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éfinissez des champs qui ne diffèrent pas par leur nom mais uniquement 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 machine virtuelle Java fait toujours référence à un champ comme un 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 connaissent pas le concept d'exceptions vérifiées. Seul le compilateur Java vérifie que les exceptions vérifiées sont toujours soit interceptées, soit déclarées si elles sont levées.
Utiliser l'appel de méthode dynamique en dehors des expressions lambda
L'appel de méthode dite dynamique peut être utilisé pour n'importe quoi, pas seulement pour les expressions lambda de Java. L'utilisation de cette fonctionnalité permet par exemple de désactiver la logique d'exécution lors de l'exécution. De nombreux langages de programmation dynamiques qui se résument à JBC ont 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'a pas encore autorisé l'utilisation de l'invocation de méthode dynamique alors que la JVM comprenait déjà l'instruction.
Utilisez des identifiants qui ne sont normalement pas considérés comme légaux
Avez-vous déjà eu envie d'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 identifiants sont .
, ;
, [
et /
. En outre, les méthodes qui ne sont pas nommées <init>
ou <clinit>
ne peuvent pas contenir <
et >
.
Réaffecter les final
paramètres ou la this
référence
final
les paramètres n'existent pas dans JBC et peuvent par conséquent être réaffectés. Tout paramètre, y compris la this
référence, n'est stocké que dans un simple tableau au sein de la JVM, ce qui permet de réaffecter la this
référence à l'index 0
dans une seule trame de méthode.
Réaffecter les final
champs
Tant qu'un champ final est assigné dans un constructeur, il est légal de réaffecter cette valeur ou même de ne pas attribuer 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 les 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'il s'agissait de méthodes
C'est plus une fonctionnalité conceptuelle, mais les constructeurs ne sont pas traités différemment dans JBC que les méthodes normales. Seul le vérificateur de la JVM garantit que les constructeurs appellent un autre constructeur légal. En dehors de cela, c'est simplement une convention de dénomination Java que 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 l'a 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.
Créez des enregistrements asymétriques * .
Lors de la création d'un enregistrement
record Foo(Object bar) { }
javac générera un fichier de classe avec un seul champ nommé bar
, une méthode d'accesseur nommée bar()
et un constructeur en prenant un seul Object
. En outre, un attribut d'enregistrement pour bar
est ajouté. En générant manuellement un enregistrement, il est possible de créer, une forme de constructeur différente, de sauter le champ et d'implémenter l'accesseur différemment. Dans le même temps, il est toujours possible de faire croire à l'API de réflexion que la classe représente un enregistrement réel.
Appelez n'importe quelle super méthode (jusqu'à Java 1.1)
Cependant, cela n'est possible que pour les versions Java 1 et 1.1. Dans JBC, les méthodes sont toujours distribuées sur un type de 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 de mettre en œuvre Qux#baz
pour invoquer Foo#baz
en sautant Bar#baz
. S'il est toujours possible de définir un appel explicite pour appeler une autre implémentation de super méthode que celle de la super classe directe, cela n'a plus d'effet dans les versions Java après 1.1. Dans Java 1.1, ce comportement était contrôlé en définissant l' ACC_SUPER
indicateur qui activerait le même comportement qui n'appelle que 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
En 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 un appel RuntimeException
quand foo
sur une instance de Bar
. Il n'est pas possible de définir la Foo::foo
méthode pour invoquer sa propre bar
méthode qui est définie dans Foo
. Comme bar
c'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' INVOKESPECIAL
opcode qui relie directement l' bar
appel de méthode Foo::foo
à Foo
la version de. 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 ce @Target
que les annotations déclarent. En utilisant la manipulation de code d'octet, il est possible de définir des annotations indépendamment de ce contrôle. Aussi, il est par exemple possible d'annoter un type de paramètre sans annoter le paramètre même si l' @Target
annotation 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 les champs, méthodes ou 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 vous fier au mécanisme de chargement de classe Java mais vous devez extraire les méta-informations vous-même.
Débordement et implicitement Assigner byte
, short
, char
et les boolean
valeurs
Ces derniers types primitifs ne sont normalement pas connus dans JBC mais ne sont définis que pour les types de tableaux ou pour les descripteurs de champ et de méthode. Dans les instructions de code octet, tous les types nommés prennent l'espace de 32 bits ce qui permet de les représenter comme int
. Officiellement, seuls les int
, float
, long
et double
types existent dans le code octet qui tous besoin de conversion explicite par la règle du vérificateur de la machine virtuelle Java.
Ne pas libérer un moniteur
Un synchronized
bloc 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 relâcher.
Remarque : dans les implémentations récentes de HotSpot, cela conduit à la place à un IllegalMonitorStateException
à la fin d'une méthode ou à une version implicite si la méthode se termine par une exception elle-même.
Ajouter plus d'une return
instruction à un initialiseur de type
En Java, même un initialiseur de type trivial tel que
class Foo {
static {
return;
}
}
est illégal. Dans le code octet, l'initialiseur de type est traité comme n'importe quelle autre méthode, c'est-à-dire que les instructions de retour peuvent être définies n'importe où.
Créer des boucles irréductibles
Le compilateur Java convertit les boucles en instructions goto dans le code d'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 bloc catch récursif
Dans le code d'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 synchronized
bloc en Java où toute exception lors de la libération d'un moniteur renvoie à 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, la version obsolète ThreadDeath
), le moniteur serait toujours libéré.
Appelez n'importe quelle méthode par défaut
Le compilateur Java requiert que plusieurs conditions soient remplies afin de permettre l'invocation d'une méthode par défaut:
- La méthode doit être la plus spécifique (ne doit pas être remplacée par une sous-interface implémentée par n'importe quel type, y compris les super types).
- 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. Cependant, si l'interface
B
étend l'interface A
mais ne remplace pas une méthode dans A
, la méthode peut toujours être appelée.
Pour le code d'octet Java, seule la deuxième condition compte. Le premier n'est cependant pas pertinent.
Invoquer une super méthode sur une instance qui ne l'est pas this
Le compilateur Java permet uniquement d'appeler une méthode super (ou par défaut d'interface) sur les instances de this
. En byte code, il est cependant également possible d'appeler 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éder aux membres synthétiques
Dans le code octet Java, il est possible d'accéder directement aux membres synthétiques. Par exemple, considérez comment dans l'exemple suivant l' Bar
accès à l' instance externe d'une autre instance:
class Foo {
class Bar {
void bar(Bar bar) {
Foo foo = bar.Foo.this;
}
}
}
Cela est généralement vrai pour tout champ, classe ou méthode synthétique.
Définir les informations de type générique désynchronisées
Bien que l'environnement d'exécution Java ne traite pas les types génériques (après que le compilateur Java a appliqué l'effacement de type), ces informations sont toujours attachées à une classe compilée en tant que méta-informations et rendues accessibles via l'API de réflexion.
Le vérificateur ne vérifie pas la cohérence de ces String
valeurs codées par métadonnées. Il est donc possible de définir des informations sur des types génériques qui ne correspondent pas à l'effacement. Par principe, les affirmations suivantes peuvent être vraies:
Method method = ...
assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());
Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);
En outre, la signature peut être définie comme non valide de sorte qu'une exception d'exécution est levée. Cette exception est levée lorsque les informations sont accédées pour la première fois car elles sont évaluées 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'incorporer le nom du paramètre et les informations de modificateur lors de la compilation d'une classe avec l' parameter
indicateur activé. Dans le format de fichier de classe Java, ces informations sont cependant stockées par méthode ce qui permet de n'incorporer ces informations de méthode que pour certaines méthodes.
Gâcher les choses et planter durement 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 ne connaît pas une telle méthode. Cependant, si vous invoquez une méthode inconnue sur un tableau, j'ai trouvé un bogue dans une version de JVM où le vérificateur manquera cela et votre JVM se terminera une fois l'instruction invoquée. Ce n'est cependant pas une fonctionnalité, mais c'est techniquement quelque chose qui n'est pas possible avec Java compilé javac . 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 ignorant le compilateur, vous pourriez trouver un point faible dans la validation du vérificateur. Il s'agit cependant d'une déclaration générale plutôt que d'une fonctionnalité.
Annoter le type de récepteur d'un constructeur lorsqu'il n'y a pas de classe externe
Depuis Java 8, les 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 généralement pas un.
class Foo {
class Bar {
Bar(@TypeAnnotation Foo Foo.this) { }
}
Foo() { } // Must not declare a receiver type
}
Comme Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()
il renvoie cependant une AnnotatedType
représentation Foo
, il est possible d'inclure des annotations de type pour Foo
le constructeur de s directement dans le fichier de classe où ces annotations sont lues ultérieurement par l'API de réflexion.
Utiliser des instructions de code d'octet inutilisées / héritées
Puisque d'autres l'ont nommé, je l'inclurai également. Java utilisait autrefois des sous-programmes par les instructions JSR
et RET
. JBC connaissait même son propre type d'adresse de retour à cette fin. Cependant, l'utilisation de sous-programmes compliquait trop l'analyse de code statique, raison pour laquelle 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 qu'il y ait quelque chose de différent. De même, vous pouvez par exemple ajouter leNOOP
Instruction de code d'octet qui n'est pas non plus utilisée par le compilateur Java mais qui ne vous permettrait pas vraiment de réaliser quelque chose de nouveau non plus. Comme indiqué dans le contexte, ces "instructions de fonctionnalités" mentionnées sont désormais supprimées de l'ensemble des opcodes légaux, ce qui les rend encore moins d'une fonctionnalité.