JVM Hardcore – Part 22 – Bytecode – Manipuler des Tableaux
Bien qu’il nous reste encore quelques instructions à étudier, nous arrivons presque à la fin de notre périple et nous sommes à même d’implémenter en bytecode des exemples complets et plus complexes que ceux que nous avons vu jusqu’à présent.
Néanmoins, certains éléments d’un fichier .class et instructions vont nous permettre d’aller encore plus loin comme nous le verrons aujourd’hui avec une vingtaine d’instructions dédiées à la manipulation de tableaux, et dans les deux articles suivants qui traiteront respectivement des exceptions, des classes anonymes et des classes internes.
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 utilisé 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.
Tableaux de primitifs
L’instruction newarray
(0xbc) permet de créer un tableau dont les éléments sont de type primitif. Elle prend en argument un nombre d’un octet correspondant au type du tableau (cf. tableau ci-dessous).
Type | Valeur |
boolean |
4 |
char |
5 |
float |
6 |
double |
7 |
byte |
8 |
short |
9 |
int |
10 |
long |
11 |
État de la pile avant → après exécution : ..., length → ..., arrayref
, où length
est la taille du tableau à créer et arrayref
la référence du tableau nouvellement créé. Si la valeur de length
est inférieure à zéro, une exception de type NegativeArraySizeException est levée.
Voyons comment créer un tableau en PJB. La méthode getNewarrayOfInts()
prend en paramètre la taille du tableau à créer et retourne le tableau de type int
nouvellement créé.
Notons qu’en PJB l’instruction newarray
a pour argument le type du tableau et non la valeur représentant ce type.
.method public static getNewarrayOfInts(I)[I
iload_0
newarray int
areturn
.methodend
Le test unitaire nous permet de vérifier que le tableau créé est du bon type et de la bonne taille.
@Test
public void getNewarrayOfInts() {
final int[] i = Array.getNewarrayOfInts(10);
Assert.assertEquals(10, i.length);
i[9] = 3;
Assert.assertEquals(3, i[9]);
}
Comme nous l’avons vu dans le tableau précédent, il est possible de créer des tableaux de type boolean
, byte
, char
et short
, qui contrairement aux valeurs primitives ne sont pas convertis en int
.
.method public static getNewarrayOfBooleans(I)[Z
iload_0
newarray boolean
areturn
.methodend
Le test unitaire est identique au précédent, à la différence que cette fois nous vérifions que le tableau est de type boolean
.
@Test
public void getNewarrayOfBooleans() {
final boolean[] b = Array.getNewarrayOfBooleans(10);
Assert.assertEquals(10, b.length);
b[9] = true;
Assert.assertTrue(b[9]);
}
Tableaux d’objets
L’instruction anewarray
(0xbd) permet de créer un tableau dont les éléments sont des objets. Elle prend en argument un nombre signé de deux octets représentant un index dans le pool de constantes de la classe. L’élément à cet index devant être du type ConstantClass
correspondant au type du tableau.
État de la pile avant → après exécution : ..., length → ..., arrayref
, où length
est la taille du tableau à créer et arrayref
la référence du tableau nouvellement créé. Si la valeur de length
est inférieure à zéro, une exception de type NegativeArraySizeException est levée.
En PJB, créer un tableau d’objets est similaire à la création d’un tableau de primitifs, à la différence que cette fois nous utilisons le nom complètement qualifié de la classe pour représenter le type du tableau.
.method public static getNewarrayOfStrings(I)[Ljava/lang/String;
iload_0
anewarray java/lang/String
areturn
.methodend
Le test unitaire est donc similaire à ceux que nous avons vus précédemment.
@Test
public void getNewarrayOfStrings() {
final String[] s = Array.getNewarrayOfStrings(10);
Assert.assertEquals(10, s.length);
s[9] = "hello";
Assert.assertEquals("hello", s[9]);
}
Récupérer une valeur à un index d’un tableau
Tous comme les instructions xload
qui permettent de récupérer les valeurs des variables locales du cadre d’une méthode, nous avons les instructions xaload
(notons le a supplémentaire) qui permettent de récupérer des valeurs à un index d’un tableau. Néanmoins, la comparaison s’arrête là puisqu’elles ne s’utilisent pas de la même manière.
Ces instructions n’ont aucun argument, elles utilisent des éléments de la pile.
État de la pile avant → après exécution : ..., arrayref, index → ..., value
, où arrayref
est la référence du tableau auquel nous souhaitons accéder, index
comme son nom l’indique la position dans le tableau à laquelle se trouve la valeur que nous souhaitons récupérer et value
la valeur récupérée. En termes Java, value = arrayref[index]
.
Il existe huit instructions différentes permettant d’accéder aux valeurs de huit types de tableaux que nous pouvons avoir.
Hex | Mnémonique | Description |
---|---|---|
0x2e |
iaload |
Récupère une valeur de type int d’un tableau et l’empile |
0x2f |
laload |
Récupère une valeur de type long d’un tableau et l’empile |
0x30 |
faload |
Récupère une valeur de type float d’un tableau et l’empile |
0x31 |
daload |
Récupère une valeur de type double d’un tableau et l’empile |
0x32 |
aaload |
Récupère la référence d’un objet dans un tableau et l’empile |
0x33 |
baload |
Récupère une valeur de type byte d’un tableau et l’empile |
0x34 |
caload |
Récupère une valeur de type char d’un tableau et l’empile |
0x35 |
saload |
Récupère une valeur de type short d’un tableau et l’empile |
Voyons un exemple avec un tableau de double
.
.method public static daload([DI)D
aload_0 @ arrayref
iload_1 @ index
daload
dreturn @ value
.methodend
Le test unitaire nous confirme que la méthode daload()
retourne bien la valeur à l’index passé en paramètre (2 pour le test).
@Test
public void daload() {
final double[] array = new double[]{1.2, 2.3, 3.4, 4.5};
final double d = Array.daload(array, 2);
Assert.assertEquals(3.4, d, 0.0001);
}
Ajouter des valeurs dans un tableau
Comme les instructions xload
qui ont pour miroir les instruction xstore
, les instructions xaload
ont pour miroir les instructions xastore
présentées dans le tableau suivant.
Hex | Mnémonique | Description |
---|---|---|
0x4f |
iastore |
Stocke une valeur de type int dans un tableau |
0x50 |
lastore |
Stocke une valeur de type long dans un tableau |
0x51 |
fastore |
Stocke une valeur de type float dans un tableau |
0x52 |
dastore |
Stocke une valeur de type double dans un tableau |
0x53 |
aastore |
Stocke la référence d’un objet dans un tableau |
0x54 |
bastore |
Stocke une valeur de type byte dans un tableau |
0x55 |
castore |
Stocke une valeur de type char dans un tableau |
0x56 |
sastore |
Stocke une valeur de type short dans un tableau |
Notons que ces instructions ne prennent pas d’argument.
État de la pile avant → après exécution : ..., arrayref, index, value → ...
, où arrayref
est la référence du tableau auquel nous souhaitons ajouter la valeur, index
la position dans le tableau à laquelle la valeur doit être ajoutée et value
la valeur à ajouter.
Une fois encore le code PJB est très simple. La méthode aastore()
prend en paramètre un tableau de String
, l’index auquel nous souhaitons ajouter la valeur et la valeur.
@ array, index, value
.method public static aastore([Ljava/lang/String;ILjava/lang/String;)V
aload_0 @ array
iload_1 @ index
aload_2 @ value
aastore
return
.methodend
Un tableau étant un objet, lorsqu’il est passé en paramètre d’une méthode, seule sa référence est transférée à la méthode. De fait, pour savoir si ce tableau a subi des modifications, il n’est pas nécessaire que la méthode le retourne. Dans le cas du test, il nous suffit d’utiliser la référence contenue par la variable s
.
@Test
public void aastore() {
final String[] s = new String[10];
Array.aastore(s, 2, "hello");
Assert.assertEquals("hello", s[2]);
}
Initialiser un tableau à une dimension
L’instruction arraylength
(0xbe) permet de récupérer la taille d’un tableau. Elle ne prend aucun argument, mais s’attend à avoir la référence du tableau dont on souhaite connaître la taille au sommet de la pile.
État de la pile avant → après exécution : ..., arrayref → ..., length
, où arrayref
est la référence du tableau duquel nous souhaitons connaître la taille et length
est la taille du tableau.
Avec la même valeur
Dans un premier temps nous allons créer une méthode prenant en paramètre un tableau et la valeur à ajouter à tous les index. Sachant que par défaut tous les éléments d’un tableau de primitifs sont initialisés avec la valeur 0 et null
pour un tableau d’objets.
En Java, il nous suffirait de quelques lignes de codes :
public static void initArraySameValue(int[] array, int value) {
for (int i = 0; i < array.length; i++) {
array[i] = value;
}
}
En revanche en PJB, cela en nécessite un peu pluss :
.method public static initArraySameValue([II)V
aload_0 @ array
arraylength
istore_2 @ array.length
iconst_0
istore_3 @ int i = 0
loop:
iload_3
iload_2
if_icmpge stop @ i < array.length
aload_0 @ array
iload_3 @ i
iload_1 @ value
iastore @ array[i] = value
iinc 3 1 @ i++
goto loop
stop:
return
.methodend
Le test unitaire se contente quant à lui de vérifier la valeur à tous les index.
@Test
public void initArraySameValue() {
final int[] i = new int[5];
Array.initArray(i, 10);
Assert.assertEquals(10, i[0]);
Assert.assertEquals(10, i[1]);
Assert.assertEquals(10, i[2]);
Assert.assertEquals(10, i[3]);
Assert.assertEquals(10, i[4]);
}
Avec des valeurs différentes
Pour l’exemple suivant, nous souhaitons voir comment est compilée l’expression new int[]{1, 2, 3, 4, 5}
, en prenant pour modèle la méthode suivante :
public int[] initArrayDiffValue() {
return new int[]{1, 2, 3, 4, 5};
}
En réalité, cette forme d’initialisation n’est qu’un sucre syntaxique, puisqu’en bytecode nous sommes obligé de créer un tableau et d’ajouter les valeurs une à une à chaque index.
int[] array = new int[5];
array[0] = 1;
array[1] = 2;
array[2] = 3;
array[3] = 4;
array[4] = 5;
Il nous suffit de convertir le code précédent en PJB, qui bien que fastidieux à écrire reste très simple :
.method public static initArrayDiffValue()[I
iconst_5
newarray int @ création du tableau
dup
iconst_0
iconst_1
iastore @ array[0] = 1
dup
iconst_1
iconst_2
iastore @ array[1] = 2
dup
iconst_2
iconst_3
iastore @ array[2] = 3
dup
iconst_3
iconst_4
iastore @ array[3] = 4
dup
iconst_4
iconst_5
iastore @ array[4] = 5
areturn
.methodend
Notons que nous utilisons l’instruction dup
pour dupliquer la référence du tableau de manière à éviter d’ajouter le tableau dans une variable locale pour la récupérer à chaque fois. De fait, nous avons toujours une référence du tableau en bas de la pile.
Le test unitaire vérifie la valeur stockée à chaque index.
@Test
public void initArrayDiffValue() {
final int[] i = Array.initArrayDiffValue();
Assert.assertEquals(1, i[0]);
Assert.assertEquals(2, i[1]);
Assert.assertEquals(3, i[2]);
Assert.assertEquals(4, i[3]);
Assert.assertEquals(5, i[4]);
}
Tableaux à plusieurs dimensions
L’instruction multianewarray
(0xc5) permet de créer un tableau à plusieurs dimensions. Elle prend deux arguments :
- le premier est un nombre non signé de deux octets représentant un index dans le pool de constantes de la classe. L’élément à cet index devant être du type
ConstantClass
correspondant au type du tableau. - le second est un nombre non signé d’un octet représentant le nombre de dimensions du tableau. De fait, nous ne pouvons pas avoir des tableaux de plus de 255 dimensions.
La taille de chaque dimension est indiquée par une valeur dans la pile.
État de la pile avant → après exécution : ..., length1, [length2, ...] → ..., arrayref
, où length1
, length2
, … sont les tailles de chaque dimension. La valeur au sommet de la pile est la taille de la dimension la plus à droite (int[length1][length2]
).
.method public static getMultianewarray(II)[[Ljava/lang/Object;
iload_0
iload_1
multianewarray [[Ljava/lang/Object; 2
areturn
.methodend
Le test unitaire nous permet de vérifier que le tableau créé a bien deux dimensions et que l’on peut insérer des objets.
@Test
public void multianewarray() {
final Object[][] o = Array.getMultianewarray(5, 10);
Assert.assertEquals(5, o.length);
Assert.assertEquals(10, o[0].length);
o[2][4] = "hello";
Assert.assertEquals("hello", o[2][4]);
}
Récupérer un sous-tableau d’un tableau à plusieurs dimensions
Un tableau à plusieurs dimensions est en réalité une série de sous-tableaux que nous pouvons récupérer à l’aide de l’instruction aaload
.
@ array, index
.method public static getSubArray([[II)[I
aload_0
iload_1
aaload @ get sub array
areturn
.methodend
Pour tester la méthode getSubArray()
, nous créons un tableau à deux dimensions nommé i
, dans lequel nous ajoutons un sous-tableau (subArray
) à l’index 2. Nous vérifions ensuite que le sous-tableau retourné est égal à subArray
.
@Test
public void getSubArray() {
final int[] subArray = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
final int[][] i = new int[5][10];
i[2] = subArray;
final int[] actuals = Array.getSubArray(i, 2);
Assert.assertArrayEquals(subArray, actuals);
}
Ajouter une valeur à un tableau à plusieurs dimensions
Étant donnée qu’un tableau à plusieurs dimensions est une série de sous-tableaux, pour pouvoir ajouter une valeur il est tout d’abord nécessaire de récupérer chaque sous-tableau.
En Java, nous pouvons décomposer l’ajout de la valeur en deux étapes :
public static void setValue(int[][]array, int index1, int index2, value) {
int[] subarray = array[index1];
subarray[index2] = value;
}
Nous pouvons traduire ceci en PJB de la manière suivante :
@ array, index1, index2, value
.method public static setValue([[IIII)V
aload_0
iload_1
aaload @ récupère un sous-tableau
iload_2
iload_3
iastore @ ajoute une valeur
return
.methodend
Le test unitaire vérifie que la valeur 1256 a bien été ajoutée aux index [2][3].
@Test
public void setValueMultiArray() {
final int[][] i = new int[5][10];
Array.setValue(i, 2, 3, 1256);
Assert.assertEquals(1256, i[2][3]);
}
Récupérer une valeur d’un tableau à plusieurs dimensions
Pour récupérer une valeur d’un tableau à plusieurs dimensions nous devons récupérer tout d’abord le sous-tableau dans lequel se trouve la valeur pour ensuite la retourner.
@ array, index1, index2
.method public static getValue([[III)I
aload_0
iload_1
aaload @ récupère le sous-tableau
iload_2
iaload @ ajoute une valeur
ireturn
.methodend
Le test unitaire permet de vérifier que la valeur à l’index [2][3] est bien celle que nous avons ajoutée (10).
@Test
public void getValueMultiArray() {
final int[][] array = new int[5][10];
array[2][3] = 10;
final int i = Array.getValue(array, 2, 3);
Assert.assertEquals(10, i);
}
Initialiser un tableau à deux dimensions
Initialiser un tableau à plusieurs dimensions en Java nécessite d’imbriquer autant de boucles qu’il y a de dimensions. Voyons un exemple avec deux dimensions :
public static void init2DArray(int[][] array, int value) {
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
array[i][j] = value;
}
}
}
Ceci n’a absolument rien d’extraordinaire. En revanche en PJB, bien que nous ayons déjà vu toutes les instructions utilisées ci-dessous, le code est un peu plus complexe.
.method public static init2DArray([[II)V
aload_0
arraylength
istore_2 @ Taille dimension 1
iconst_0
istore_3 @ index_1 = 0
loop_1:
iload_3
iload_2
if_icmpge end_loop_1
aload_0
iload_3
aaload
arraylength
istore 4 @ Taille dimension 2
iconst_0
istore 5 @ index_2 = 0
loop_2:
iload 5
iload 4
if_icmpge end_loop_2
aload_0
iload_3
aaload
iload 5
iload_1
iastore @ ajoute une valaur
iinc 5 1 @ iinc index_2
goto loop_2
end_loop_2:
iinc 3 1 @ iinc index_1
goto loop_1
end_loop_1:
return
.methodend
Pour tester que toutes les cases de notre tableau possèdent une valeur, nous allons tester avec un tableau de 3×2 :
@Test
public void init2DArray() {
final int[][] i = new int[3][2];
Array.init2DArray(i, 10);
Assert.assertEquals(10, i[0][0]);
Assert.assertEquals(10, i[0][1]);
Assert.assertEquals(10, i[1][0]);
Assert.assertEquals(10, i[1][1]);
Assert.assertEquals(10, i[2][0]);
Assert.assertEquals(10, i[2][1]);
}
L’initialisation suivante new int[][]{{1, 1}, {2, 2}, {3, 3}}
étant similaire à à celle d’un tableau à une dimension son implémentation en PJB peut faire office d’exercice pour le lecteur.
Implémentation
Bien que nous ayons vu de nombreuses instructions, seule l’instruction multianewarray
nécessite une nouvelle classe associée :
- NoArgInstruction :
xaload, xastore, arraylength
- ByteArgInstruction :
newarray
- ShortArgInstruction :
anewarray
- MultianewarrayInstruction :
multianewarray
Associées à la classe MultianewarrayInstruction
, les classes MultianewarrayInstructionFactory et MultianewarrayMetaInstruction ont aussi étaient ajoutées. Tout comme les types ArgsTypeARRAY_TYPE et ARRAY_MULTIDIM
.
De plus, outre l’adaptation des classes Disassembler, HexDumper, PjbDumper et PjbParser, la méthode getArrayType(), permettant d’analyser le type du tableau à créer (argument de l’instruction newarray
) a été ajoutée dans la classe NameTokenizer
.
Et pour terminer, l’ensemble des instructions ont été ajoutées aux classes Instructions, MetaInstructions et MethodBuilder.
What’s next ?
Dans l’article suivant nous nous intéresserons aux exceptions.