Accueil Nos publications Blog JVM Hardcore – Part 21 – Bytecode – Manipuler des Objets

JVM Hardcore – Part 21 – Bytecode – Manipuler des Objets

academic_duke
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

Source

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

Source

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

Source

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

Source

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

Source

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

Source

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

Source

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

Source

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

Source

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

Source

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

Source

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

Source

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

Source

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éthodes getX() et getY()
  • IMove définissant la méthode move()

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

Source

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

Source

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

Source

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

Source

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’instruction invokevirtual.
  • 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 avec super le compilateur utilisera l’instruction invokespecial.

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 et double 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

Source

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

Source

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 Java
  • checkcast 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

Source

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

Source

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 *)

Source

ClassFileBuilder

La classe ClassFileBuilder a vu l’apparition de méthodes nous permettant d’ajouter :

  1. 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);
    }
    

Source

  1. 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;
    }
    

Source

  1. 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;
    }
    

Source

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) {}

Source

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.