Penser en Java 2nde édition | - | Sommaire | Préface | Avant-propos | Chapitre : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Annexe : A B C D | Tables des matières | - | Thinking in Java |
La bibliothèque standard Java Arrays propose aussi une méthode fill(), mais celle-ci est relativement triviale : elle ne fait que dupliquer une certaine valeur dans chaque cellule, ou dans le cas d'objets, copier la même référence dans chaque cellule. En utilisant Arrays2.print(), les méthodes Arrays.fill() peuvent être facilement illustrées :
On peut soit remplir un tableau complètement, soit - comme le montrent les deux dernières instructions - une certaine plage d'indices. Mais comme il n'est possible de ne fournir qu'une seule valeur pour le remplissage dans Arrays.fill(), les méthodes Arrays2.fill() sont bien plus intéressantes.
La bibliothèque standard Java propose une méthode static, System.arraycopy(), qui réalise des copies de tableau bien plus rapidement qu'une boucle for. System.arraycopy() est surchargée afin de gérer tous les types. Voici un exemple qui manipule des tableaux d'int :
arraycopy() accepte comme arguments le tableau source, le déplacement dans le tableau source à partir duquel démarrer la copie, le tableau destination, le déplacement dans le tableau destination à partir duquel démarrer la copie, et le nombre d'éléments à copier. Bien entendu, toute violation des frontières du tableau générera une exception.
L'exemple montre bien qu'on peut copier des tableaux de scalaires comme des tableaux d'objets. Cependant, dans le cas de la copie de tableaux d'objets, seules les références sont copiées - il n'y a pas duplication des objets eux-mêmes. C'est ce qu'on appelle une copie superficielle (voir l'Annexe A).
Arrays fournit la méthode surchargée equals() pour comparer des tableaux entiers. Encore une fois, ces méthodes sont surchargées pour chacun des types de base, ainsi que pour les Objects. Pour être égaux, les tableaux doivent avoir la même taille et chaque élément doit être équivalent (au sens de la méthode equals()) à l'élément correspondant dans l'autre tableau (pour les types scalaires, la méthode equals() de la classe d'encapsulation du type concerné est utilisé ; par exemple, Integer.equals() est utilisé pour les int). Voici un exemple :
Au début du programme, a1 et a2 sont identiques, donc le résultat est « true » ; puis l'un des éléments est changé donc la deuxième ligne affichée est « false ». Dans le dernier cas, tous les éléments de s1 pointent sur le même objet, alors que s2 contient cinq objets différents. Cependant, l'égalité de tableaux est basée sur le contenu (via Object.equals()) et donc le résultat est « true ».
L'une des fonctionnalités manquantes dans les bibliothèques Java 1.0 et 1.1 sont les opérations algorithmiques - y compris les simples tris. Ceci était relativement frustrant pour quiconque s'attendait à une bibliothèque standard conséquente. Heureusement, Java 2 a corrigé cette situation, au moins pour le problème du tri.
Le problème posé par l'écriture d'une méthode de tri générique est que le tri doit réaliser des comparaisons basées sur le type réel de l'objet. Bien sûr, l'une des approches consiste à écrire une méthode de tri différente pour chaque type, mais cela va à l'encontre du principe de réutilisabilité du code pour les nouveaux types.
L'un des buts principaux de la conception est de « séparer les choses qui changent de celles qui ne bougent pas » ; ici, le code qui reste le même est l'algorithme général de tri, alors que la manière de comparer les objets entre eux est ce qui change d'un cas d'utilisation à l'autre. Donc au lieu de coder en dur le code de comparaison dans différentes procédures de tri, on utilise ici la technique des callbacks. Avec un callback, la partie du code qui varie d'un cas à l'autre est encapsulé dans sa propre classe, et la partie du code qui ne change pas appellera ce code pour réaliser les comparaisons. De cette manière, il est possible de créer différents objets pour exprimer différentes sortes de comparaisons et de les passer au même code de tri.
Dans Java 2, il existe deux manières de fournir des fonctionnalités de comparaison. La méthode naturelle de comparaison constitue la première, elle est annoncée dans une classe en implémentant l'interface java.lang.Comparable. C'est une interface très simple ne disposant que d'une seule méthode, compareTo(). Cette méthode accepte un autre Object comme argument, et renvoie une valeur négative si l'argument est plus grand que l'objet courant, zéro si ils sont égaux, ou une valeur positive si l'argument est plus petit que l'objet courant.
Voici une classe qui implémente Comparable et illustre la comparaison en utilisant la méthode Arrays.sort() de la bibliothèque standard Java :
Lorsque la fonction de comparaison est définie, il vous incombe de décider du sens à donner à la comparaison entre deux objets. Ici, seules les valeurs i sont utilisées dans la comparaison, les valeurs j sont ignorées.
La méthode static randInt() produit des valeurs positives entre zéro et 100, et la méthode generator() produit un objet implémentant l'interface Generator, en créant une classe interne anonyme (cf. Chapitre 8). Celui-ci génére des objets CompType en les initialisant avec des valeurs aléatoires. Dans main(), le générateur est utilisé pour remplir un tableau de CompType, qui est alors trié. Si Comparable n'avait pas été implémentée, une erreur de compilation aurait été générée lors d'un appel à sort().
Dans le cas où une classe n'implémente pas Comparable, ou qu'elle l'implémente d'une manière qui ne vous satisfait pas (c'est à dire que vous souhaitez une autre fonction de comparaison pour ce type), il faut utiliser une autre approche pour comparer des objets. Cette approche nécessite de créer une classe séparée qui implémente l'interface Comparator, comportant les deux méthodes compare() et equals(). Cependant, sauf cas particuliers (pour des raisons de performance notamment), il n'est pas nécessaire d'implémenter equals() car chaque classe dérive implicitement de Object, qui fournit déjà cette méthode. On peut donc se contenter de la méthode Object.equals() pour satisfaire au contrat imposé par l'interface.
La classe Collections (que nous étudierons plus en détails par la suite) dispose d'un Comparator qui inverse l'ordre de tri. Ceci peut facilement être appliqué à CompType :
L'appel à Collections.reverseOrder() produit une référence sur le Comparator.
Voici un deuxième exemple dans lequel un Comparator compare des objets CompType en se basant cette fois sur la valeur de leur j plutôt que sur celle de i :
La méthode compare() doit renvoyer un entier négatif, zéro ou un entier positif selon que le premier argument est respectivement plus petit, égal ou plus grand que le second.
Avec les méthodes de tri intégrées, il est maintenant possible de trier n'importe quel tableau de scalaires ou d'objets implémentant Comparable ou disposant d'une classe Comparator associée. Ceci comble un énorme trou dans les bibliothèques de Java - croyez-le ou non, Java 1.0 ou 1.1 ne fournissait aucun moyen de trier des Strings ! Voici un exemple qui génére des objets String aléatoirement et les trie :
Il est bon de noter que le tri effectué sur les Strings est lexicographique, c'est à dire que les mots commençant par des majuscules apparaissent avant ceux débutant par une minuscule (typiquement les annuaires sont triés de cette façon). Il est toutefois possible de redéfinir ce comportement et d'ignorer la casse en définissant une classe Comparator. Cette classe sera placée dans le package « util » à des fins de réutilisation :
Chaque String est convertie en minuscules avant la comparaison. La méthode compareTo() de String fournit ensuite le comparateur désiré.
Voici un exemple d'utilisation d'AlphabeticComparator :
L'algorithme de tri utilisé dans la bibliothèque standard de Java est conçu pour être optimal suivant le type d'objets triés : un Quicksort pour les scalaires, et un tri-fusion stable pour les objets. Vous ne devriez donc pas avoir à vous soucier des performances à moins qu'un outil de profilage ne vous démontre explicitement que le goulot d'étranglement de votre programme soit le processus de tri.
Une fois un tableau trié, il est possible d'effectuer une recherche rapide sur un item en utilisant Arrays.binarySearch(). Il est toutefois très important de ne pas utiliser binarySearch() sur un tableau non trié ; le résultat en serait imprévisible. L'exemple suivant utilise un RandIntGenerator pour remplir un tableau et produire des valeurs à chercher dans ce tableau :