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 |
ressemble exactement à la clause d'initialisation statique moins le mot-clé static. Cette syntaxe est nécessaire pour permettre l'initialisation de classes internes anonymes (voir Chapitre 8).
L'initialisation des tableaux en C est laborieuse et source d'erreurs. C++ utilise l'initialisation d'aggregats pour rendre cette opération plus sûre [31]. Java n'a pas d'« aggregats » comme C++, puisque tout est objet en Java. Java possède pourtant des tableaux avec initialisation.
Un tableau est simplement une suite d'objets ou de types de base, tous du même type et réunis ensemble sous un même nom. Les tableaux sont définis et utilisés avec l'opérateur d'indexation [ ] (crochets ouvrant et fermant). Pour définir un tableau il suffit d'ajouter des crochets vides après le nom du type :
Les crochets peuvent également être placé après le nom de la variable :
Cela correspond aux attentes des programmeurs C et C++. Toutefois, la première syntaxe est probablement plus sensée car elle annonce le type comme un « tableau de int. » Ce livre utilise cette syntaxe.
Le compilateur ne permet pas de spécifier la taille du tableau à sa définition. Cela nous ramène à ce problème de « référence. » A ce point on ne dispose que d'une référence sur un tableau, et aucune place n'a été allouée pour ce tableau. Pour créer cet espace de stockage pour le tableau, il faut écrire une expression d'initialisation. Pour les tableaux, l'initialisation peut apparaître à tout moment dans le code, mais on peut également utiliser un type spécial d'initialisation qui doit alors apparaître à la déclaration du tableau. Cette initalisation spéciale est un ensemble de valeurs entre accolades. L'allocation de l'espace de stockage pour le tableau (l'équivalent de new) est prise en charge par le compilateur dans ce cas. Par exemple :
Mais pourquoi voudrait-on définir une référence sur tableau sans tableau ?
Il est possible d'affecter un tableau à un autre en Java, on peut donc écrire :
Cette expression effectue en fait une copie de référence, comme le montre la suite :
On peut voir que a1 a une valeur initiale tandis qu' a2 n'en a pas ; a2 prend une valeur plus tard— dans ce cas, vers un autre tableau.
Maintenant voyons quelque chose de nouveau : tous les tableaux ont un membre intrinsèque (qu'ils soient tableaux d'objets ou de types de base) que l'on peut interroger — mais pas changer — ; il donne le nombre d'éléments dans le tableau. Ce membre s'appelle length (longueur). Comme les tableaux en Java , comme C et C++, commencent à la case zero, le plus grand nombre d'éléments que l'on peut indexer est length - 1. Lorsqu'on dépasse ces bornes, C et C++ acceptent cela tranquillement et la mémoire peut être corrompue ; ceci est la cause de bogues infâmes. Par contre, Java empêche ce genre de problèmes en générant une erreur d'exécution (une exception, le sujet du Chapitre 10) lorsque le programme essaye d'accéder à une valeur en dehors des limites. Bien sûr, vérifier ainsi chaque accès coûte du temps et du code ; comme il n'y a aucun moyen de désactiver ces vérifications, les accès tableaux peuvent être une source de lenteur dans un programme s'ils sont placés à certains points critiques de l'exécution. Les concepteurs de Java ont pensé que cette vitesse légèrement réduite était largement contrebalancée par les aspects de sécurité sur Internet et la meilleure productivité des programmeurs.
Que faire quand on ne sait pas au moment où le programme est écrit, combien d'éléments vont être requis à l'exécution ? Il suffit d'utiliser new pour créer les éléments du tableau. Dans ce cas, new fonctionne même pour la création d'un tableau de types de base (new ne peut pas créer un type de base) :
Comme la taille du tableau est choisie aléatoirement (en utilisant la méthode pRand( )), il est clair que la création du tableau se passe effectivement à l'exécution. De plus, on peut voir en exécutant le programme que les tableaux de types primitifs sont automatiquement initialisés avec des valeurs “vides” (pour les nombres et les char, cette valeur est zéro, pour les boolean, cette valeur est false).
Bien sûr le tableau pourrait aussi avoir été défini et initialisé sur la même ligne :
Lorsque l'on travaille avec un tableau d'objets non primitifs, il faut toujours utiliser new. Encore une fois, le problème des références revient car ce que l'on crée est un tableau de références. Considérons le type englobant Integer, qui est une classe et non un type de base :
Ici, même apès que new ait été appelé pour créer le tableau :
c'est uniquement un tableau de références, et l'initialisation n'est pas complète tant que cette référence n'a pas elle-même été initialisée en créeant un nouvel objet Integer :
Oublier de créer l'objet produira une exception d'exécution dès que l'on accédera à l'emplacement.
Regardons la formation de l'objet String à l'intérieur de print. On peut voir que la référence vers l'objet Integer est automatiquement convertie pour produire une String représentant la valeur à l'intérieur de l'objet.
Il est également possible d'initialiser des tableaux d'objets en utilisant la liste délimitée par des accolades. Il y a deux formes :
C'est parfois utile, mais d'un usage plus limité car la taille du tableau est déterminée à la compilation. La virgule finale dans la liste est optionnelle. (Cette fonctionnalité permet une gestion plus facile des listes longues.)
La deuxième forme d'initialisation de tableaux offre une syntaxe pratique pour créer et appeler des méthodes qui permet de donner le même effet que les listes à nombre d'arguments variable de C ( “varargs” en C). Ces dernières permettent le passage d'un nombre quelconque de paramètres, chacun de type inconnu. Comme toutes les classes héritent d'une classe racine Object (un sujet qui sera couvert en détail tout au long du livre), on peut créer une méthode qui prend un tableau d'Object et l'appeler ainsi :
A ce niveau, il n'y a pas grand chose que l'on peut faire avec ces objets inconnus, et ce programme utilise la conversion automatique vers String afin de faire quelque chose d'utile avec chacun de ces Objects. Au chapitre 12, qui explique l'identification dynamique de types (RTTI), nous verrons comment découvrir le type exact de tels objets afin de les utiliser à des fins plus intéressantes.
Java permet de créer facilement des tableaux multidimensionnels :
Le code d'affichage utilise length ; de cette façon il ne force pas une taille de tableau fixe.
Le premier exemple montre un tableau multidimensionnel de type primitifs. Chaque vecteur du tableau est délimité par des accolades :
Chaque paire de crochets donne accès à la dimension suivante du tableau.
Le deuxième exemple montre un tableau à trois dimensions alloué par new. Ici le tableau entier est alloué en une seule fois :
Par contre, le troisième exemple montre que les vecteurs dans les tableaux qui forment la matrice peuvent être de longueurs différentes :
Le premier new crée un tableau avec un longueur aléatoire pour le premier élément et le reste de longeur indéterminée. Le deuxième new à l'intérieur de la boucle for remplit les éléments mais laisse le troisième index indéterminé jusqu'au troisième new.
On peut voir à l'exécution que les valeurs des tableaux sont automatiquement initialisées à zéro si on ne leur donne pas explicitement de valeur initiale.
Les tableaux d'objets non primitifs fonctionnent exactement de la même manière, comme le montre le quatrième exemple, qui présente la possibilité d'utiliser new dans les accolades d'initialisation :
Le cinquième exemple montre comment un tableau d'objets non primitifs peut être construit pièce par pièce :
L'expression i*j est là uniquement pour donner une valeur intéressante à l' Integer.
Le mécanisme apparemment sophistiqué d'initialisation que l'on appelle constructeur souligne l'importance donnée à l'initialisation dans ce langage. Quand Stroustrup était en train de créer C++, une des premières observations qu'il fit à propos de la productivité en C était qu'une initialisation inappropriée des variables cause de nombreux problèmes de programmation. Ce genre de bogues est difficile à trouver. Des problèmes similaires se retrouvent avec un mauvais nettoyage. Parce que les constructeurs permettent de garantir une initialisation et un nettoyage correct (le compilateur n'autorisera pas la création d'un objet sans un appel valide du constructeur), le programmeur a un contrôle complet en toute sécurité.
En C++, la destruction est importante parce que les objets créés avec new doivent être détruits explicitement .En Java, le ramasse-miettes libère automatiquement la mémoire pour tous les objets, donc la méthode de nettoyage équivalente en Java n'est pratiquement jamais nécessaire. Dans les cas où un comportement du style destructeur n'est pas nécessaire, le ramasse-miettes de Java simplifie grandement la programmation et ajoute une sécurité bien nécessaire à la gestion mémoire. Certains ramasse-miettes peuvent même s'occuper du nettoyage d'autres ressources telles que les graphiques et les fichiers. Cependant, le prix du ramasse-miettes est payé par une augmentation du temps d'exécution, qu'il est toutefois difficile d'évaluer à cause de la lenteur globale des interpréteurs Java au moment de l'écriture de cet ouvrage. Lorsque cela changera, il sera possible de savoir si le coût du ramasse-miettes posera des barrières à l'utilisation de Java pour certains types de programmes (un des problèmes est que le ramasse-miettes est imprévisible).
Parce que Java garantit la construction de tous les objets, le constructeur est, en fait, plus conséquent que ce qui est expliqué ici. En particulier, quand on crée de nouvelles classes en utilisant soit la composition, soit l'héritage la garantie de construction est maintenue et une syntaxe supplémentaire est nécessaire. La composition, l'héritage et leurs effets sur les constructeurs sont expliqués un peu plus loin dans cet ouvrage.