Comment fonctionnent les tableaux en Java?

Jusqu’à présent, nous avons fait référence aux classes String et StringBuilder comme une “séquence de caractères”. C’est vrai. Elles sont implémentées en utilisant un tableau de caractères. Un tableau est une zone de mémoire sur le tas avec de l’espace pour un nombre désigné d’éléments. Un String est implémenté comme un tableau avec certaines méthodes que vous pourriez vouloir utiliser spécifiquement pour manipuler des caractères. Un StringBuilder est implémenté comme un tableau où l’objet tableau est remplacé par un nouvel objet tableau plus grand lorsqu’il n’y a plus d’espace pour stocker tous les caractères. Une grande différence est qu’un tableau peut être de n’importe quel type Java. Si nous ne voulions pas utiliser un String pour une raison quelconque, nous pourrions utiliser directement un tableau de primitives char:

char[] lettres;

Ce ne serait pas très pratique car nous perdrions toutes les propriétés spéciales que String nous offre, comme la possibilité d’écrire “Java”. Gardez à l’esprit que lettres est une variable de référence et non une primitive. Le type char est une primitive. Mais char est ce qui va dans le tableau et pas le type du tableau lui-même. Le tableau lui-même est de type char[]. Vous pouvez mentalement lire les crochets ([]) comme “tableau”.

En d’autres termes, un tableau est une liste ordonnée. Il peut contenir des doublons. Dans cette section, nous examinerons la création d’un tableau de primitives et d’objets, le tri, la recherche, les varargs et les tableaux multidimensionnels.

Création d’un Tableau de Primitives

La façon la plus courante de créer un tableau est la suivante: Elle spécifie le type du tableau (int) et la taille (3). Les crochets vous indiquent qu’il s’agit d’un tableau.

int[] nombres = new int[3];

Lorsque vous utilisez cette forme pour instancier un tableau, tous les éléments sont définis à la valeur par défaut pour ce type. Comme vous l’avez appris au Chapitre 1, la valeur par défaut d’un int est 0. Puisque nombres est une variable de référence, elle pointe vers l’objet tableau. Par défaut, la valeur de tous les éléments est 0. Aussi, les index commencent à 0 et comptent, tout comme pour un String.

Une autre façon de créer un tableau est de spécifier tous les éléments avec lesquels il devrait commencer:

int[] plusDeNombres = new int[] {42, 55, 99};

Dans cet exemple, nous créons également un tableau int de taille 3. Cette fois, nous spécifions les valeurs initiales de ces trois éléments au lieu d’utiliser les valeurs par défaut.

Java reconnaît que cette expression est redondante. Puisque vous spécifiez le type du tableau du côté gauche du signe égal, Java connaît déjà le type. Et puisque vous spécifiez les valeurs initiales, il connaît déjà la taille. Java vous permet donc d’écrire ceci comme raccourci:

int[] plusDeNombres = {42, 55, 99};

Cette approche est appelée un tableau anonyme. Il est anonyme car vous ne spécifiez pas le type et la taille.

Enfin, vous pouvez taper les [] avant ou après le nom, et ajouter un espace est facultatif. Cela signifie que toutes ces cinq déclarations font exactement la même chose:

int[] nombreAnimaux;
int []nombreAnimaux2;
int[] nombreAnimaux3;
int nombreAnimaux4[];
int nombreAnimaux5 [];

La plupart des gens utilisent la première. Vous pourriez voir n’importe laquelle de ces formes, alors habituez-vous à voir les crochets à des endroits inhabituels.

Multiples “Tableaux” dans les Déclarations

Quels types de variables de référence pensez-vous que le code suivant crée?

int[] identifiants, types;

La bonne réponse est deux variables de type int[]. Cela semble assez logique. Après tout, int a, b; a créé deux variables int. Qu’en est-il de cet exemple?

int identifiants[], types;

Tout ce que nous avons fait a été de déplacer les crochets, mais cela a changé le comportement. Cette fois, nous obtenons une variable de type int[] et une variable de type int. Java considère cette ligne de code comme ceci: “Ils veulent deux variables de type int. La première s’appelle identifiants[]. Celle-ci est un int[] appelé identifiants. La seconde s’appelle simplement types. Pas de crochets, donc c’est un entier normal.”

Inutile de dire que vous ne devriez pas écrire du code qui ressemble à cela. Mais vous devez le comprendre.

Création d’un Tableau avec des Variables de Référence

Vous pouvez choisir n’importe quel type Java comme type du tableau. Cela inclut les classes que vous créez vous-même. Voyons un type intégré avec String:

String[] insectes = { "criquet", "scarabée", "coccinelle" };
String[] alias = insectes;
System.out.println(insectes.equals(alias)); // true
System.out.println(insectes.toString()); // [Ljava.lang.String;@160bc7c0

Nous pouvons appeler equals() car un tableau est un objet. Il renvoie true en raison de l’égalité de référence. La méthode equals() sur les tableaux ne regarde pas les éléments du tableau. Rappelez-vous, cela fonctionnerait même sur un int[]. Le type int est une primitive; int[] est un objet.

La deuxième instruction print est encore plus intéressante. Qu’est-ce que c’est que [Ljava.lang.String;@160bc7c0? Vous n’avez pas besoin de le savoir, mais [L signifie que c’est un tableau, java.lang.String est le type de référence et 160bc7c0 est le code de hachage. Vous obtiendrez des lettres et des chiffres différents à chaque exécution car il s’agit d’une référence.

Java fournit une méthode qui affiche joliment un tableau: Arrays.toString(insectes) afficherait [criquet, scarabée, coccinelle].

Le tableau n’alloue pas d’espace pour les objets String. Au lieu de cela, il alloue de l’espace pour une référence à l’endroit où les objets sont réellement stockés.

En guise de révision rapide, à quoi pensez-vous que ce tableau pointe?

public class Noms {
    String noms[];
}

La réponse est null. Le code n’a jamais instancié le tableau, c’est donc juste une variable de référence à null. Essayons à nouveau: à quoi pensez-vous que ce tableau pointe?

public class Noms {
    String noms[] = new String[2];
}

C’est un tableau car il a des crochets. C’est un tableau de type String puisque c’est le type mentionné dans la déclaration. Il a deux éléments car la longueur est 2. Chacun des deux emplacements est actuellement null mais a le potentiel de pointer vers un objet String.

Vous vous souvenez du cast du chapitre précédent lorsque vous vouliez forcer un type plus grand dans un type plus petit? Vous pouvez aussi le faire avec des tableaux:

String[] chaines = { "valeurChaine" };
Object[] objets = chaines;
String[] encoreChaines = (String[]) objets;
objets[0] = new StringBuilder(); // NE COMPILE PAS
objets[0] = new StringBuilder(); // Attention!

La ligne 3 crée un tableau de type String. La ligne 4 ne nécessite pas de cast car Object est un type plus large que String. À la ligne 5, un cast est nécessaire car nous passons à un type plus spécifique. La ligne 6 ne compile pas car un String[] n’autorise que des objets String, et StringBuilder n’est pas un String.

La ligne 7 est là où cela devient intéressant. Du point de vue du compilateur, cela va bien. Un objet StringBuilder peut clairement aller dans un Object[]. Le problème est que nous n’avons pas réellement un Object[]. Nous avons un String[] référencé à partir d’une variable Object[]. À l’exécution, le code lance une ArrayStoreException. Le code lancera une exception.

Utilisation d’un Tableau

Maintenant que vous savez comment créer un tableau, essayons d’y accéder:

String[] mammiferes = {"singe", "chimpanzé", "âne"};
System.out.println(mammiferes.length); // 3
System.out.println(mammiferes[0]); // singe
System.out.println(mammiferes[1]); // chimpanzé
System.out.println(mammiferes[2]); // âne

La ligne 1 déclare et initialise le tableau. La ligne 2 nous indique combien d’éléments le tableau peut contenir. Le reste du code imprime le tableau. Notez que les éléments sont indexés à partir de 0. Cela devrait vous être familier depuis String et StringBuilder, qui commencent également à compter à partir de 0. Ces classes comptent également la longueur comme le nombre d’éléments. Remarquez qu’il n’y a pas de parenthèses après length car ce n’est pas une méthode.

String[] mammiferes = {"singe", "chimpanzé", "âne"};
System.out.println(mammiferes.length()); // NE COMPILE PAS

Pour vous assurer que vous comprenez comment length fonctionne, qu’est-ce que cela affiche selon vous?

var oiseaux = new String[6];
System.out.println(oiseaux.length);

La réponse est 6. Même si les six éléments du tableau sont null, il y en a quand même six. L’attribut length ne tient pas compte de ce qui se trouve dans le tableau; il considère uniquement combien d’emplacements ont été alloués.

Il est très courant d’utiliser une boucle pour lire ou écrire dans un tableau. Cette boucle définit chaque élément de nombres à cinq de plus que l’index actuel:

var nombres = new int[10];
for (int i = 0; i < nombres.length; i++)
    nombres[i] = i + 5;

La ligne 1 instancie simplement un tableau avec 10 emplacements. La ligne 2 est une boucle for qui utilise un modèle extrêmement courant. Elle commence à l’index 0, là où commence un tableau. Elle continue, une à la fois, jusqu’à ce qu’elle atteigne la fin du tableau. La ligne 3 définit l’élément actuel de nombres.

Le test vérifiera si vous êtes observateur en essayant d’accéder à des éléments qui ne sont pas dans le tableau. Pouvez-vous dire pourquoi chacun de ces exemples lance une ArrayIndexOutOfBoundsException pour notre tableau de taille 10?

nombres[10] = 3;
nombres[nombres.length] = 5;
for (int i = 0; i <= nombres.length; i++)
    nombres[i] = i + 5;

Le premier essaie de voir si vous savez que les index commencent à 0. Puisque nous avons 10 éléments dans notre tableau, cela signifie que seuls nombres[0] à nombres[9] sont valides. Le deuxième exemple suppose que vous êtes assez intelligent pour savoir que 10 est invalide et le déguise en utilisant le champ length. Cependant, la longueur est toujours supérieure d’un à l’index maximum valide. Enfin, la boucle for utilise incorrectement <= au lieu de <, ce qui est aussi une façon de faire référence à ce dixième élément.

Tri

Java facilite le tri d’un tableau en fournissant une méthode sort — ou plutôt, un tas de méthodes sort. Tout comme StringBuilder vous permettait de passer presque n’importe quoi à append(), vous pouvez passer presque n’importe quel tableau à Arrays.sort().

Arrays nécessite un import. Pour l’utiliser, vous devez avoir l’une des deux instructions suivantes dans votre classe:

import java.util.*; // importer tout le package incluant Arrays
import java.util.Arrays; // importer juste Arrays

Cet exemple simple trie trois nombres:

int[] nombres = { 6, 9, 1 };
Arrays.sort(nombres);
for (int i = 0; i < nombres.length; i++)
    System.out.print(nombres[i] + " ");

Le résultat est 1 6 9, comme vous devriez vous y attendre. Remarquez que nous avons parcouru la sortie pour imprimer les valeurs du tableau. Imprimer directement la variable tableau aurait donné le hachage désagréable [I@2bd9c3e7. Alternativement, nous aurions pu imprimer Arrays.toString(nombres) au lieu d’utiliser la boucle. Cela aurait donné [1, 6, 9].

Essayez à nouveau avec des types String:

String[] chaines = { "10", "9", "100" };
Arrays.sort(chaines);
for (String s : chaines)
    System.out.print(s + " ");

Cette fois, le résultat n’est peut-être pas ce à quoi vous vous attendiez. Ce code affiche 10 100 9. Le problème est que String trie par ordre alphabétique, et 1 est trié avant 9. (Les chiffres sont triés avant les lettres, et les majuscules sont triées avant les minuscules.) Au Chapitre 9, “Collections et Generics”, vous apprendrez à créer des ordres de tri personnalisés en utilisant un comparateur.

Avez-vous remarqué que nous avons glissé la boucle for améliorée dans cet exemple? Puisque nous n’utilisons pas l’index, nous n’avons pas besoin de la boucle for traditionnelle.

Recherche

Java fournit également un moyen pratique de rechercher, mais seulement si le tableau est déjà trié. Le Tableau 4.3 couvre les règles pour la recherche binaire.

ScénarioRésultat
Élément cible trouvé dans le tableau triéIndex de la correspondance
Élément cible non trouvé dans le tableau triéValeur négative montrant un plus petit que le négatif de l’index où une correspondance doit être insérée pour préserver l’ordre trié
Tableau non triéUne surprise; ce résultat n’est pas défini

Essayons ces règles avec un exemple:

int[] nombres = {2,4,6,8};
System.out.println(Arrays.binarySearch(nombres, 2)); // 0
System.out.println(Arrays.binarySearch(nombres, 4)); // 1
System.out.println(Arrays.binarySearch(nombres, 1)); // -1
System.out.println(Arrays.binarySearch(nombres, 3)); // -2
System.out.println(Arrays.binarySearch(nombres, 9)); // -5

Notez que la ligne 1 est un tableau trié. Si ce n’était pas le cas, nous ne pourrions pas appliquer les autres règles. La ligne 2 recherche l’index de 2. La réponse est l’index 0. La ligne 3 recherche l’index de 4, qui est 1.

La ligne 4 recherche l’index de 1. Bien que 1 ne soit pas dans la liste, la recherche peut déterminer qu’il devrait être inséré à l’élément 0 pour préserver l’ordre trié. Puisque 0 signifie déjà quelque chose pour les index de tableau, Java doit soustraire 1 pour nous donner la réponse de -1. La ligne 5 est similaire. Bien que 3 ne soit pas dans la liste, il devrait être inséré à l’élément 1 pour préserver l’ordre trié. Nous négations et soustrayons 1 pour la cohérence, obtenant -1 -1, aussi connu sous le nom de -2. Enfin, la ligne 6 veut nous dire que 9 devrait être inséré à l’index 4. Nous négations et soustrayons à nouveau 1, obtenant -4 -1, aussi connu sous le nom de -5.

À quoi pensez-vous que cela donne dans cet exemple?

int[] nombres = new int[] {3,2,1};
System.out.println(Arrays.binarySearch(nombres, 2));
System.out.println(Arrays.binarySearch(nombres, 3));

Notez qu’à la ligne 1, le tableau n’est pas trié. Cela signifie que la sortie ne sera pas définie.

Comparaison

Java fournit également des méthodes pour comparer deux tableaux afin de déterminer lequel est “plus petit”. D’abord, nous couvrons la méthode compare(), puis nous passons à mismatch(). Ces méthodes sont surchargées pour prendre une variété de paramètres.

Utilisation de compare()

Il y a un tas de règles que vous devez connaître avant d’appeler compare(). Heureusement, ce sont les mêmes règles que vous devez connaître au Chapitre 9 lorsque vous écrivez un Comparator.

D’abord, vous devez apprendre ce que signifie la valeur de retour. Vous n’avez pas besoin de connaître les valeurs de retour exactes, mais vous devez savoir ce qui suit:

  • Un nombre négatif signifie que le premier tableau est plus petit que le second.
  • Un zéro signifie que les tableaux sont égaux.
  • Un nombre positif signifie que le premier tableau est plus grand que le second.

Voici un exemple:

System.out.println(Arrays.compare(new int[] {1}, new int[] {2}));

Ce code imprime un nombre négatif. Il devrait être assez intuitif que 1 est plus petit que 2, ce qui rend le premier tableau plus petit.

Maintenant que vous savez comment comparer une seule valeur, voyons comment comparer des tableaux de différentes longueurs:

  • Si les deux tableaux ont la même longueur et ont les mêmes valeurs à chaque emplacement dans le même ordre, retourner zéro.
  • Si tous les éléments sont les mêmes mais que le second tableau a des éléments supplémentaires à la fin, retourner un nombre négatif.
  • Si tous les éléments sont les mêmes, mais que le premier tableau a des éléments supplémentaires à la fin, retourner un nombre positif.
  • Si le premier élément qui diffère est plus petit dans le premier tableau, retourner un nombre négatif.
  • Si le premier élément qui diffère est plus grand dans le premier tableau, retourner un nombre positif.

Enfin, que signifie plus petit? Voici quelques règles supplémentaires qui s’appliquent ici et à compareTo(), que vous verrez au Chapitre 8, “Lambdas et Interfaces Fonctionnelles”:

  • null est plus petit que n’importe quelle autre valeur.
  • Pour les nombres, l’ordre numérique normal s’applique.
  • Pour les chaînes, une est plus petite si c’est un préfixe d’une autre.
  • Pour les chaînes/caractères, les nombres sont plus petits que les lettres.
  • Pour les chaînes/caractères, les majuscules sont plus petites que les minuscules.

Le Tableau 4.4 montre des exemples de ces règles en action.

Premier tableauSecond tableauRésultatRaison
new int[] {1, 2}new int[] {1}Nombre positifLe premier élément est le même, mais le premier tableau est plus long.
new int[] {1, 2}new int[] {1, 2}ZéroCorrespondance exacte
new String[] {“a”}new String[] {“aa”}Nombre négatifLe premier élément est une sous-chaîne du second.
new String[] {“a”}new String[] {“A”}Nombre positifLes majuscules sont plus petites que les minuscules.
new String[] {“a”}new String[] {null}Nombre positifnull est plus petit qu’une lettre.

Enfin, ce code ne compile pas car les types sont différents. Lors de la comparaison de deux tableaux, ils doivent être du même type de tableau.

System.out.println(Arrays.compare(
    new int[] {1}, new String[] {"a"})); // NE COMPILE PAS

Utilisation de mismatch()

Maintenant que vous êtes familier avec compare(), il est temps d’apprendre mismatch(). Si les tableaux sont égaux, mismatch() retourne -1. Sinon, il retourne le premier index où ils diffèrent. Pouvez-vous déterminer ce que ceux-ci impriment?

System.out.println(Arrays.mismatch(new int[] {1}, new int[] {1}));
System.out.println(Arrays.mismatch(new String[] {"a"},
    new String[] {"A"}));
System.out.println(Arrays.mismatch(new int[] {1, 2}, new int[] {1}));

Dans le premier exemple, les tableaux sont les mêmes, donc le résultat est -1. Dans le deuxième exemple, les entrées à l’élément 0 ne sont pas égales, donc le résultat est 0. Dans le troisième exemple, les entrées à l’élément 0 sont égales, donc nous continuons à chercher. L’élément à l’index 1 n’est pas égal. Ou, plus précisément, un tableau a un élément à l’index 1, et l’autre non. Par conséquent, le résultat est 1.

Pour vous assurer que vous comprenez les méthodes compare() et mismatch(), étudiez le Tableau 4.5. Si vous ne comprenez pas pourquoi toutes les valeurs sont là, veuillez revenir en arrière et étudier à nouveau cette section.

MéthodeQuand les tableaux contiennent les mêmes donnéesQuand les tableaux sont différents
equals()truefalse
compare()0Nombre positif ou négatif
mismatch()-1Index zéro ou positif

Utilisation de Méthodes avec Varargs

Lorsque vous créez un tableau vous-même, cela ressemble à ce que nous avons vu jusqu’à présent. Lorsqu’un tableau est passé à votre méthode, il peut avoir une autre apparence. Voici trois exemples avec une méthode main():

public static void main(String[] args)
public static void main(String args[])
public static void main(String... args) // varargs

Le troisième exemple utilise une syntaxe appelée varargs (arguments variables), que vous avez vue au Chapitre 1. Vous apprendrez à appeler une méthode en utilisant varargs au Chapitre 5, “Méthodes”. Pour l’instant, tout ce que vous devez savoir est que vous pouvez utiliser une variable définie en utilisant varargs comme si c’était un tableau normal. Par exemple, args.length et args[0] sont légaux.

Travailler avec des Tableaux Multidimensionnels

Les tableaux sont des objets, et bien sûr, les composants d’un tableau peuvent être des objets. Il ne faut pas beaucoup de temps, en combinant ces deux faits, pour se demander si les tableaux peuvent contenir d’autres tableaux, et bien sûr, ils le peuvent.

Création d’un Tableau Multidimensionnel

Plusieurs séparateurs de tableau sont tout ce qu’il faut pour déclarer des tableaux avec plusieurs dimensions. Vous pouvez les localiser avec le type ou le nom de variable dans la déclaration, tout comme auparavant:

int[][] vars1; // tableau 2D
int vars2 [][]; // tableau 2D
int[] vars3[]; // tableau 2D
int[] vars4 [], espace [][]; // un tableau 2D ET un tableau 3D

Les deux premiers exemples ne sont pas surprenants et déclarent un tableau bidimensionnel (2D). Le troisième exemple déclare également un tableau 2D. Il n’y a aucune bonne raison d’utiliser ce style autre que pour confondre les lecteurs de votre code. L’exemple final déclare deux tableaux sur la même ligne. En additionnant les crochets, nous voyons que vars4 est un tableau 2D et espace est un tableau 3D. Encore une fois, il n’y a aucune raison d’utiliser ce style autre que pour confondre les lecteurs de votre code.

Vous pouvez spécifier la taille de votre tableau multidimensionnel dans la déclaration si vous le souhaitez:

String [][] rectangle = new String[3][2];

Le résultat de cette instruction est un tableau rectangle avec trois éléments, chacun référençant un tableau de deux éléments. Vous pouvez considérer la plage adressable comme [0][0] à [2][1], mais ne pensez pas à cela comme une structure d’adresses comme [0,0] ou [2,1].

Maintenant, supposons que nous définissons l’une de ces valeurs:

rectangle[0][1] = "défini";

Vous pouvez visualiser le résultat. Ce tableau est peu rempli car il contient beaucoup de valeurs null. Vous pouvez voir que rectangle pointe toujours vers un tableau de trois éléments et que nous avons trois tableaux de deux éléments. Vous pouvez également suivre le chemin depuis la référence jusqu’à la seule valeur pointant vers un String. Vous commencez à l’index 0 dans le tableau supérieur. Puis vous allez à l’index 1 dans le tableau suivant.

Bien que ce tableau soit de forme rectangulaire, un tableau n’a pas besoin de l’être. Considérez celui-ci:

int[][] taillesDifferentes = {{1, 4}, {3}, {9,8,7}};

Nous commençons toujours avec un tableau de trois éléments. Cependant, cette fois, les éléments au niveau suivant sont tous de tailles différentes. L’un est de longueur 2, le suivant de longueur 1, et le dernier de longueur 3. Cette fois, le tableau est de primitives, donc elles sont affichées comme si elles étaient dans le tableau elles-mêmes.

Une autre façon de créer un tableau asymétrique est d’initialiser uniquement la première dimension d’un tableau et de définir la taille de chaque composant du tableau dans une instruction séparée:

int [][] args = new int[4][];
args[0] = new int[5];
args[1] = new int[3];

Cette technique révèle ce que vous obtenez réellement avec Java: des tableaux de tableaux qui, correctement gérés, offrent un effet multidimensionnel.

Utilisation d’un Tableau Multidimensionnel

L’opération la plus courante sur un tableau multidimensionnel est de le parcourir. Cet exemple imprime un tableau 2D:

var deuxD = new int[3][2];
for(int i = 0; i < deuxD.length; i++) {
    for(int j = 0; j < deuxD[i].length; j++)
        System.out.print(deuxD[i][j] + " "); // imprimer élément
    System.out.println(); // temps pour une nouvelle ligne
}

Nous avons deux boucles ici. La première utilise l’index i et parcourt le premier sous-tableau pour deuxD. La seconde utilise une variable de boucle différente, j. Il est important que ces noms de variables soient différents pour que les boucles ne se mélangent pas. La boucle interne examine combien d’éléments se trouvent dans le tableau de deuxième niveau. La boucle interne imprime l’élément et laisse un espace pour la lisibilité. Lorsque la boucle interne est terminée, la boucle externe passe à une nouvelle ligne et répète le processus pour l’élément suivant.

Cet exercice entier serait plus facile à lire avec la boucle for améliorée.

for(int[] inner : deuxD) {
    for(int num : inner)
        System.out.print(num + " ");
    System.out.println();
}

Nous vous accordons que ce n’est pas moins de lignes, mais chaque ligne est moins complexe, et il n’y a pas de variables de boucle ou de conditions de terminaison à mélanger.