JVM Hardcore – Part 21 – Bytecode – Manipuler des Objets
Toutes les instructions que nous avons vues jusqu’à présent nous ont permis de nous concentrer sur de la programmation procédurale. Aujourd’hui nous allons nous intéresser à de nombreuses instructions nous permettant de faire de la programmation orientée objet.
Dans cet article nous verrons comment :
- instancier une classe ;
- définir un constructeur ;
- récupérer et fixer la valeur d’un champ d’instance ;
- appeler les différents types de méthode ;
- définir une classe abstraite ;
- définir une interface.
Le code est disponible sur Github (tag et branche)
Tous les articles déjà publiés de la série portent le tag jvmhardcore.
Représentation de la pile (Rappel)
La JVM étant basée sur le modèle de la pile, il est essentiel de connaître quel est l’impact des instructions. Pour représenter l’état avant/après l’exécution d’une instruction, nous allons reprendre le format utiliser par la JVMS et qui est le suivant :
..., valeur1, valeur2 → ..., résultat
, où les valeurs les plus à droite sont au sommet de la pile. valeur1
et valeur2
étant les deux valeurs utilisées pour le calcul et résultat
le résultat.
Il est important de noter que dans cette représentation les long
et les double
sont considérés comme une seule valeur. Par conséquent, lorsque nécessaire nous présenterons les différents cas d’utilisation d’une instruction en utilisant plusieurs formes.
Instancier une classe
L’instruction new
(0xbb) permet de créer un nouvel objet. Elle prend en argument un nombre non signé de deux octets représentant un index dans le pool de constantes de la classe. L’élément à cet index étant de type ConstantClass
contient le nom complètement qualifié de la classe à instancier.
État de la pile avant → après exécution : ... → objref, ...
, où objref
est une référence de l’objet crée.
Néanmoins, après exécution de l’instruction new
, l’objet est créé mais est inutilisable puisqu’il n’est pas initialisé. Pour ce faire il est nécessaire d’utiliser l’instruction invokespecial
(0xb7) qui nous permet d’appeler le constructeur.
L’instruction invokespecial
prend en argument un nombre non signé de deux octets représentant un index dans le pool de constantes de la classe. L’élément à cet index étant de type ConstantMethodRef
contient (directement ou indirectement à travers le type ConstantNameAndType
) :
- le nom complètement qualifié de la classe à instancier,
- la valeur
correspondant au constructeur à appeler et
- le descripteur du constructeur.
État de la pile avant → après exécution : ..., objectref, [arg1, [arg2 ...]] → ...
, où objref
est une référence de l’objet et arg1
, arg2
, … sont les paramètres du constructeur.
En PJB, l’instruction invokespecial
peut être utilisée de la manière suivante :
invokespecial <nom_de_la_classe>
<nom_de_la_méthode>
<descripteur_de_la_méthode>
Concrètement, en Java, pour instancier un StringBuilder
il nous suffit d’écrire :
new StringBuilder();
alors qu’en PJB (et en bytecode) plusieurs instructions sont nécessaires :
new java/lang/StringBuilder
dup
invokespecial java/lang/StringBuilder <init> ()V
Notons que l’instruction invokespecial
dépile la référence empilée par l’instruction new
et que le constructeur d’une classe retourne toujours void
. Pour ne pas perdre la référence nouvellement créée, nous devons la dupliquer, d’où l’utilisation de l’instruction dup
.
Pour tester le bout de code précédent, nous pouvons créer une méthode retournant une instance de StringBuilder :
.method public static getStringBuilder()Ljava/lang/StringBuilder;
new java/lang/StringBuilder
dup
ldc "Hello"
invokespecial java/lang/StringBuilder <init> (Ljava/lang/String;)V
areturn
.methodend
Le test unitaire nous permet de vérifier que l’instance du StringBuilder
contient la chaîne de caractère Hello
.
@Test
public void getStringBuilder() {
final StringBuilder sb = TestObjects.getStringBuilder();
Assert.assertEquals("Hello", sb.toString());
}
Définir un constructeur
A l’instar d’un bloc statique, la représentation bytecode d’un constructeur est une méthode, dont le nom est , dont la valeur de retour est toujours
void
et qui peut avoir les modificateurs public
, protected
ou private
.
Jusqu’à présent, étant donné que nous utilisions uniquement des méthodes statiques, nous n’avions jamais défini de constructeur, mais contrairement à Java qui permet d’avoir un constructeur par défaut (constructeur non défini ne possédant aucun paramètre), en bytecode une classe pouvant être instanciée doit impérativement posséder un constructeur.
De plus, alors qu’en Java l’appel au constructeur de la classe parent est parfois implicite, en bytecode il est toujours explicite. Néanmoins, en bytecode les instructions correspondant à super()
ne doivent pas être nécessairement être les premières. Ceci s’explicite simplement si l’on prend en considération le fait que l’on puisse passer une expression au constructeur d’une classe parent. L’expression étant résolu pour que l’on passe le résultat à la classe parent :
super(a * 2 + b);
Imposer l’appel au constructeur de la superclasse de chaque classe assure que personne ne peut utiliser une classe sans qu’elle soit initialisée correctement. Ceci est crucial pour la sécurité de la JVM. Certaines méthodes dépendent d’une classe ayant été initialisée avant de pouvoir la manipuler en toute sécurité.
En prenant une classe User
dont la structure est la suivante en Java :
public class TestObjects {
public TestObjects() {
super(); // Optionnel
}
}
Nous pouvons la convertir en PJB en utilisant notamment l’instruction invokespecial
:
.class public org/isk/pjb/TestObjects
.method public <init>()V
aload_0
invokespecial java/lang/Object <init> ()V
return
.methodend
.classend
ou en PJBA/Java :
final String OBJECT = "java/lang/Object";
final String TEST_OBJECTS = "org/isk/pjb/TestObjects";
new ClassFileBuilder(ClassFile.MODIFIER_PUBLIC, TEST_OBJECTS)
.newConstructor(Method.MODIFIER_PUBLIC, "()V")
.aload_0()
.invokespecial(OBJECT, "<init>", "()V")
.return_();
Pour rappel, l’élément à l’index 0 des variables locales (d’un cadre) correspond à l’instance courante de la classe (this
).
Appeler une méthode d’instance publique ou protégée
L’instruction invokevirtual
(0xb6) permet d’appeler une méthode d’instance possédant un modificateur public
ou protected
. Cette méthode devant appartenir à une classe (concrète ou abstraite). A l’aide de l’instruction invokevirtual
il est possible d’appeler une méthode
- dans la classe dans laquelle elle est définie ;
- dans une classe enfant ou bien
- dans une classe possédant (sous la forme d’un champ d’instance ou d’une variable locale) une instance de la classe dans laquelle elle est définie.
A l’instar des instructions invokestatic
et invokespecial
, invokevirtual
prend en argument un nombre non signé de deux octets représentant un index dans le pool de constantes de la classe dont l’élément à cet index est de type ConstantMethodRef
.
État de la pile avant → après exécution : ..., [arg1 [, arg2 [...]]] -> [r], ...
, où arg1
et arg2
sont les paramètres de la méthode. Le paramètre au sommet de la pile est le paramètre le plus à droite de la méthode. Et où r
est la valeur retournée par la méthode si le type de retour n’est pas void
.
En PJB, l’instruction invokevirtual
peut être utilisée de la manière suivante :
invokevirtual <nom_de_la_classe>
<nom_de_la_méthode>
<descripteur_de_la_méthode>
Voyons un exemple en partant de la méthode Java suivante :
public String sayHello(String name) {
return "Hello " + name;
}
Comme nous l’avons déjà vu, en bytecode la concaténation de chaînes de caractères passe par un StringBuilder
:
public String sayHello(String name) {
StringBuilder sb = new StringBuilder("Hello ");
sb.append(name);
return sb.toString();
}
Ce qui ce traduit en PJB par :
.method public sayHello(Ljava/lang/String;)Ljava/lang/String;
new java/lang/StringBuilder
dup
ldc "Hello "
invokespecial java/lang/StringBuilder <init> (Ljava/lang/String;)V
aload_1
invokevirtual java/lang/StringBuilder append (Ljava/lang/String;)Ljava/lang/StringBuilder;
invokevirtual java/lang/StringBuilder toString ()Ljava/lang/String;
areturn
.methodend
Pour finir nous vérifions que la méthode sayHello()
retourne la bonne valeur.
@Test
public void sayHello() {
final TestObjects testObjects = new TestObjects();
Assert.assertEquals("Hello John", testObjects.sayHello("John"));
}
Appeler une méthode d’instance privée
L’instruction invokespecial
que nous avons vue précédemment (dans le cas de l’instanciation d’une classe et l’appel à un constructeur parent) permet aussi d’appeler une méthode d’instance possédant un modificateur private
.
Pour changer nous allons créer une méthode delegatedPrivate(int i1, int i2)
qui additionne deux entiers. Cette méthode étant privée elle sera accessible depuis l’extérieur grâce à la méthode delegatePrivate(int i1, int i2)
qui aura pour objectif uniquement de lui passer les deux paramètres.
.class public org/isk/pjb/TestObjects
@ Constructeur
.method public delegatePrivate(II)I
aload_0
iload_1
iload_2
invokespecial org/isk/pjb/TestObjects delegatedPrivate (II)I
ireturn
.methodend
.method private delegatedPrivate(II)I
iload_1
iload_2
iadd
ireturn
.methodend
.classend
Pour tester ces deux méthodes, nous vérifions que le résultat de l’addition est correct.
@Test
public void delegatePrivate() {
final TestObjects testObjects = new TestObjects();
Assert.assertEquals(6, testObjects.delegatePrivate(2, 4));
}
Récupérer et fixer la valeur d’un champ d’instance
Les instructions getfield
(0xb4) et putfield
(0xb5) permettent respectivement de récupérer et de fixer la valeur d’un champ d’instance. Toutes deux prennent en argument un nombre non signé de deux octets représentant un index dans le pool de constantes de la classe pointant vers un élément de type ConstantFieldRef
.
État de la pile avant → après exécution de l’instruction getfield
: ... → v, ...
, ou v
est la valeur récupérée du champ.
État de la pile avant → après exécution de l’instruction putfield
: ..., v → ...
, ou v
est la valeur qui sera assignée au champ.
Comme les champs de classe, les champs d’instance peuvent être définis à l’aide de la directive .field
. Seul la présence/absence du modificateur static
permet de faire la distinction entre les deux.
De même, les règles que nous avons détaillées dans l’article précédent pour les constantes statiques s’appliquent pour les champs d’instance constants.
Dans l’exemple suivant nous définissons quatre champs d’instance dont deux sont constants et publiques (originX
et originY
). Les deux autres sont variables, privés, initialisés dans le constructeur et nous pouvons y accéder à l’aide de setters et getters.
.class public org/isk/pjb/Point
.super java/lang/Object
.field public final originX I = 2
.field public final originY I = 3
.field private x I
.field private y I
.method public <init>(II)V
aload_0
invokespecial java/lang/Object <init> ()V
aload_0
iload_1
putfield org/isk/pjb/Point x I
aload_0
iload_2
putfield org/isk/pjb/Point y I
return
.methodend
.method public getX()I
aload_0
getfield org/isk/pjb/Point x I
ireturn
.methodend
.method public setX(I)V
aload_0
iload_1
putfield org/isk/pjb/Point x I
return
.methodend
@ Idem pour le champ 'y'
.classend
Le test unitaire nous permet de vérifier que les champs sont valorisés correctement.
@Test
public void getAndSetFields() {
final Point point = new Point(4, 5);
Assert.assertEquals(2, point.originX);
Assert.assertEquals(3, point.originY);
Assert.assertEquals(4, point.getX());
Assert.assertEquals(5, point.getY());
point.setX(6);
point.setY(7);
Assert.assertEquals(6, point.getX());
Assert.assertEquals(7, point.getY());
}
Héritage
Comme nous l’avons vu dans la partie 11 Format d’un fichier .class la structure ClassFile
possède un champ superClass
contenant un index dans le pool de constantes de la classe pointant vers un élément de type ConstantClass
. Cet élément pointant vers l’objet de type ConstantUtf8
contenant le nom complètement qualifié de la classe parent de la classe courante.
En Java, une classe qui n’hérite pas explicitement d’une autre, hérite implicitement de la classe java.lang.Object
. Or comme nous l’avons constaté à diverses reprises, un élément implicite en Java est toujours explicite en bytecode. De fait, jusqu’à présent nous avions défini en dur (dans la classe ClassFile
) le parent de toutes les classes que nous avons créé (java/lang/Object
).
Pour définir le parent d’une classe en PJB, il nous suffit d’utiliser une nouvelle directive (.super
) à la suite de la déclaration de la classe.
Notons qu’à partir de maintenant, même si la classe hérite de java.lang.Object
, la directive .super
doit être présente.
Voyons un exemple en reprenant notre classe Point
définie précédemment :
.class public org/isk/pjb/Point3D
.super org/isk/pjb/Point
.field public z I
.method public <init>(III)V
aload_0
iload_1
iload_2
invokespecial org/isk/pjb/Point <init> (II)V
aload_0
iload_3
putfield org/isk/pjb/Point3D z I
return
.methodend
.classend
Le test unitaire nous permet de vérifier que les champs x
et y
déclarés dans la classe parent ont leur valeur fixée, tout comme le champ z
déclaré dans la classe Point3D
.
@Test
public void inheritance() {
final Point3D point = new Point3D(4, 5, 6);
Assert.assertEquals(4, point.getX());
Assert.assertEquals(5, point.getY());
Assert.assertEquals(6, point.z);
}
Classes abstraites
Définir une classe abstraite en bytecode est similaire à ce que l’on fait habituellement en Java. Il nous suffit d’utiliser le modificateur abstract
au niveau de la classe et de toutes les méthodes abstraites. Ces méthodes abstraites ne possédant bien évidemment aucune instruction.
Le thème de l’article d’aujourd’hui n’étant pas la “Conception en Java, les bonnes pratiques” nous allons nous autoriser quelques libertés pour pouvoir réutiliser la classe Point
qui sera héritée par une classe abstraite nommée APoint
, elle-même héritée par une classe CPoint
. Dans la classe APoint
nous déclarons une méthode abstraite move(int x, int y)
et l’implémentons dans la classe CPoint
.
.class public abstract org/isk/pjb/APoint
.super org/isk/pjb/Point
@ Constructeur
# Change les valeurs de x et y
.method public abstract move(II)V
.methodend
.classend
.class public org/isk/pjb/CPoint
.super org/isk/pjb/APoint
@ Constructeur
# Change les valeurs de x et y
.method public move(II)V
aload_0
iload_1
invokevirtual org/isk/pjb/Point setX (I)V
aload_0
iload_2
invokevirtual org/isk/pjb/Point setY (I)V
return
.methodend
.classend
Pour tester que la méthode move()
de la classe Cpoint
correspond bien à l’implémentation de cette même méthode dans la classe APoint
. Nous allons utiliser le type APoint
et appeler la méthode move()
sur son instance.
@Test
public void abstractClass() {
final APoint point = new CPoint(4, 5);
Assert.assertEquals(4, point.getX());
Assert.assertEquals(5, point.getY());
point.move(10, 20);
Assert.assertEquals(10, point.getX());
Assert.assertEquals(20, point.getY());
}
Interfaces
Dans la structure ClassFile
, le champ interfaces
permet de définir les interfaces implémentées par une classe en indiquant l’index dans le pool de constantes de la classe un élément de type ConstantClass
, puisqu’une interface n’est rien de plus qu’une classe avec le modificateur interface
et un ensemble de méthodes abstraites.
En PJB, une interface est définie à l’aide de la directive .interface
faisant suite à la directive .super
.
Nous allons donc créer deux interfaces :
IPoint
définissant les méthodesgetX()
etgetY()
IMove
définissant la méthodemove()
IPoint
héritant de IMove
sera implémentée par la classe CPoint
, qui implémente aussi l’interface java.lang.Comparable. Sachant que CPoint
hérite de APoint
, elle-même héritant de Point
, les méthodes getX()
, getY()
et move()
sont déjà implémentées. Il ne nous reste plus qu’à ajouter la méthode compareTo()
issue de Comparable
, qui pour l’instant retournera toujours 0.
.class public org/isk/pjb/CPoint
.super org/isk/pjb/APoint
.interface org/isk/pjb/IPoint
.interface java/lang/Comparable
@ ...
.method public compareTo(Ljava/lang/Object;)I
bipush 0
ireturn
.methodend
.classend
.class public interface org/isk/pjb/IPoint
.super java/lang/Object
.interface org/isk/pjb/IMove
.method public abstract getX()I
.methodend
.method public abstract getY()I
.methodend
.classend
.class public interface org/isk/pjb/IMove
.super java/lang/Object
.method public abstract move(II)V
.methodend
.classend
Attention, en bytecode, une interface a toujours pour parent java.lang.Object
. De fait, dans l’exemple précédent, pour indiquer que IPoint
hérite de IMove
, nous avons dû définir IMove
comme une interface de IPoint
.
Le test unitaire est quasiment identique au précédent, à la différence que nous utilisons le type IPoint
au lieu de APoint
et nous vérifions que l’instance de IPoint
est aussi de type IMove
.
@Test
public void interfaces() {
final IPoint point = new CPoint(4, 5);
Assert.assertEquals(4, point.getX());
Assert.assertEquals(5, point.getY());
point.move(10, 20);
Assert.assertEquals(10, point.getX());
Assert.assertEquals(20, point.getY());
Assert.assertTrue(point instanceof IMove);
}
Appeler explicitement une méthode d’une classe parent
Il est possible en Java d’appeler une méthode d’une classe parent à l’aide du mot clé super
. Généralement il n’est pas nécessaire de l’utiliser tout comme le mot clé this
. Néanmoins lorsque l’on souhaite redéfinir une méthode tout en exécutant le code de la méthode parent, le mot clé super
est indispensable.
Par exemple :
public class CPoint extends Point {
public void move(int x, int y) {
this.x = x;
this.y = y;
}
}
public class CPoint3D extends CPoint {
private int z;
public void move(int x, int y) {
super.move(x, y);
this.z = 0;
}
}
Même si ce bout de code n’a aucun sens pratique, l’appel super.move(x, y);
implique l’utilisation de l’instruction invokespecial
.
.class public org/isk/pjb/CPoint3D
.super org/isk/pjb/CPoint
.field public z I
.method public <init>(III)V
aload_0
iload_1
iload_2
invokespecial org/isk/pjb/CPoint <init> (II)V
aload_0
iload_3
putfield org/isk/pjb/CPoint3D z I
return
.methodend
.method public move(II)V
aload_0
iload_1
iload_2
invokespecial org/isk/pjb/CPoint move (II)V
aload_0
iconst_0
putfield org/isk/pjb/CPoint3D z I
return
.methodend
.classend
Le test suivant permet de vérifier, une fois encore, que l’on fixe correctement la valeurs de ces champs. Mentionnons que si nous avions utilisé l’instruction invokevirtual
, même en précisant le type org/isk/pjb/CPoint
la méthode exécutée aurait été celle de la classe org/isk/pjb/CPoint3D
créant une méthode récursive sans condition d’arrêt et donc générant une java.lang.StackOverflowError. Nous verrons pourquoi dans un prochain article.
@Test
public void overriding() {
final CPoint3D point = new CPoint3D(1, 2, 3);
Assert.assertEquals(1, point.getX());
Assert.assertEquals(2, point.getY());
Assert.assertEquals(3, point.z);
point.move(20, 30);
Assert.assertEquals(20, point.getX());
Assert.assertEquals(30, point.getY());
Assert.assertEquals(0, point.z);
}
Notons tout de même qu’il y a une différence entre l’utilisation de this
et de super
lorsque l’utilisation du second n’est pas obligatoire (comme dans l’exemple que nous venons de voir) :
- Si une méthode est publique ou protégée et qu’elle est appelée avec
this
ou sans rien le compilateur Java (javac
) utilisera l’instructioninvokevirtual
. - Si une méthode est privée et qu’elle est appelée depuis la classe dans laquelle elle est définie avec
this
ou sans rien, ou si une méthode est publique ou protégée et qu’elle est appelée avecsuper
le compilateur utilisera l’instructioninvokespecial
.
Appeler une méthode d’une interface
Pour terminer notre tour des instructions permettant d’appeler une méthode intéressons-nous à l’instruction invokeinterface
(0xb9) qui comme son nom l’indique permet d’appeler une méthode sur une instance représentant une interface. Elle prend trois arguments :
- un nombre non signé de deux octets représentant un index dans le pool de constantes de la classe dont l’élément à cet index est de type
ConstantMethodRef
. - le nombre d’unités que prennent les paramètres de la méthods (où les types
long
etdouble
prennent deux unités) sur un octet. Cette redondance avec l’information fournie par la méthode au niveau bytecode est historique. - la valeur 0 sur un octet.
État de la pile avant → après exécution : ..., [arg1 [, arg2 [...]]] -> [r], ...
, où arg1
et arg2
sont les paramètres de la méthode. Le paramètre au sommet de la pile est le paramètre le plus à droite de la méthode. Et où r
est la valeur retournée par la méthode si le type de retour n’est pas void
.
En PJB l’instruction doit être utilisée comme les autres instructions invokex
, c’est-à-dire avec seulement le premier argument. Les deux autres étant calculés automatiquement par PJBA.
public class Mover {
final public IMove point;
// Constructeur
public void move(int x, int y) {
this.point.move(x, y);
}
}
La conversion est similaire à de nombreux exemples que nous avons déjà vu.
.class public org/isk/pjb/Mover
.super java/lang/Object
.field public point Lorg/isk/pjb/IMove;
.method public <init>(Lorg/isk/pjb/IMove;)V
aload_0
invokespecial java/lang/Object <init> ()V
aload_0
aload_1
putfield org/isk/pjb/Mover point Lorg/isk/pjb/IMove;
return
.methodend
.method public move(II)V
aload_0
getfield org/isk/pjb/Mover point Lorg/isk/pjb/IMove;
iload_1
iload_2
invokeinterface org/isk/pjb/IMove move (II)V
return
.methodend
.classend
Tout comme le test :
@Test
public void callingInterfaceMethod() {
final CPoint point = new CPoint(1, 2);
final Mover mover = new Mover(point);
Assert.assertEquals(1, point.getX());
Assert.assertEquals(2, point.getY());
mover.move(22, 33);
Assert.assertEquals(22, point.getX());
Assert.assertEquals(33, point.getY());
}
Conversion d’objets
Un peu plus haut nous avons ajouté une méthode compareTo()
qu’il nous faut implémenter. Or étant donné que l’on est encore sur du bytecode compatible Java 1.4 nous ne pouvons pas utiliser les generics. De fait, l’objet passé en paramètre de la méthode est Object
. L’implémentation peut donc se faire de la manière suivante :
public class CPoint implements Comparable {
// ...
public int compareTo(Object o) {
if (o == this) {
return 0;
}
if (o instanceof Point) {
Point oPoint = (Point) o;
int compareX = Integer.compare(super.getX(), oPoint.getX());
if (compareX == 0) {
return Integer.compare(this.getY(), oPoint.getY());
} else {
return compareX;
}
}
return -1;
}
}
Pour pouvoir convertir ce bout de code en bytecode deux nouvelles instructions sont nécessaires :
instanceof
dont l’utilisation est similaire au mot clé du même nom en Javacheckcast
qui vérifie si une référence est d’un certain type ou non.
Voyons-les un peu plus en détail.
instanceof
L’instruction instanceof
(0xc1) détermine si une instance est d’un certain type. Elle prend en argument un nombre signé de deux octets représentant un index dans le pool de constantes de la classe dont l’élément à cet index est de type ConstantClass
correspondant au type avec lequel l’instance sera comparée.
État de la pile avant → après exécution : ..., objref → r, ...
, où objref
est une référence et r
le résultat de la comparaison.
Si l’instance est nulle ou n’est pas du type attendu la valeur 0 est empilée, sinon 1 est empilé.
En PJB, la comparaison peut être écrite de la manière suivante :
instanceof org/isk/pjb/IPoint
ifeq ko
@ ...
checkcast
L’instruction checkcast
(0xc0) permet de vérifier qu’une instance est d’un certain type pour que l’on puisse ensuite utiliser les méthodes de ce type. Il ne s’agit pas d’une véritable conversion comme nous l’avions vu avec les types primitifs.
Si le type de la référence au sommet de la pile ne correspond pas au type attendu une exception de type ClassCastException
est levée.
Cette instruction prend en argument un nombre signé de deux octets représentant un index dans le pool de constantes de la classe dont l’élément à cet index est de type ConstantClass
correspondant au type avec lequel l’instance sera comparée.
État de la pile avant → après exécution : ..., objref → objref, ...
, où objref
est une référence. Contrairement à la plupart des instructions checkcast
ne dépile pas son opérande.
En PJB
Convertissons l’implémentation de la méthode compareTo()
en PJB :
.method public compareTo(Ljava/lang/Object;)I
aload_1
aload_0
if_acmpne notSameRef @ if (o == this)
iconst_0
ireturn @ Même référence
notSameRef:
aload_1
instanceof org/isk/pjb/Point
ifeq notPoint @ if (o instanceof Point)
aload_1
checkcast org/isk/pjb/Point
astore_2
aload_0
invokevirtual org/isk/pjb/Point getX ()I
aload_2
invokevirtual org/isk/pjb/Point getX ()I
invokestatic java/lang/Integer compare (II)I
istore_3
iload_3
ifne notSameX @ if (compareX == 0)
aload_0
invokevirtual org/isk/pjb/Point getY ()I
aload_2
invokevirtual org/isk/pjb/Point getY ()I
invokestatic java/lang/Integer compare (II)I
@ Retourne le résultat de la comparaison entre les x
ireturn
notSameX:
iload_3
@ Retourne le résultat de la comparaison entre les y
ireturn
@ Objet d'un type différent
notPoint:
iconst_m1
ireturn
.methodend
Voyons un des tests, les autres se trouvant à la suite dans le fichier source.
@Test
public void compareTo0() {
final CPoint point = new CPoint(1, 2);
final int result = point.compareTo(null);
Assert.assertEquals(-1, result);
}
Implémentation
L’ajout de nouvelles instructions étant similaire d’un article sur l’autre, nous nous attarderons uniquement sur la classe ClassFileBuilder
et les ajouts dans la grammaire de notre analyseur syntaxique. Et c’est par cette dernière que nous allons commencer.
Évolution de la grammaire de PJB
Pour prendre en compte les classes parents et les interfaces, il nous a été nécessaire d’ajouter deux nouvelles directives :
.super
devant être unique dans une classe et.interface
pouvant avoir zéro ou plus occurrences
Les symboles modifiés sont les suivants :
classContent = super interfaces fields methods (* modifié *)
super = ws superStartIdentifer ws className ws (* nouveau *)
interfaces = {interface} (* nouveau *)
interface = ws interfaceStartIdentifer ws className ws (* nouveau *)
superStartIdentifer = '.super' (* nouveau *)
interfaceStartIdentifer = '.interface' (* nouveau *)
ClassFileBuilder
La classe ClassFileBuilder a vu l’apparition de méthodes nous permettant d’ajouter :
- un constructeur
public MethodBuilder newConstructor(final int methodModifiers, final String methodDescriptor) { return this.newConstructor(methodModifiers, methodDescriptor, true); } public MethodBuilder newConstructor(final int methodModifiers, final String methodDescriptor, final boolean eagerConstruction) { return this.newMethod(methodModifiers, "<init>", methodDescriptor, eagerConstruction); }
- des interfaces
public ClassFileBuilder newInterface(final String fullyQualifiedInterfaceName) { final int interfaceUtf8Index = this.classFile.addConstantUTF8(fullyQualifiedInterfaceName); final int interfaceClassIndex = this.classFile.addConstantClass(interfaceUtf8Index); this.classFile.addInterface(new Interface(interfaceClassIndex)); return this; }
- des méthodes abstraites
public ClassFileBuilder newAbstractMethod(final int methodModifiers, final String methodName, final String methodDescriptor) { this.newMethod(methodModifiers | Method.MODIFIER_ABSTRACT, methodName, methodDescriptor, true); return this; }
Nous avons aussi modifié les constructeurs pour que nous puissions ajouter une classe parent.
public ClassFileBuilder(int classModifiers,
final String fullyQualifiedName,
final String fullyQualifiedParentName) {}
public ClassFileBuilder(final int classModifiers,
final String fullyQualifiedName) {}
Pour conclure, notons que nous avons ajouté la classe AssemblingObjects permettant de générer diverses classes nous permettant de valider l’implémentation des divers éléments et instructions liés à la programmation objet. La classe de test étant ObjectsTest.
What’s next ?
Dans l’article suivant nous nous intéresserons aux tableaux.