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 |
Bien qu'il soit basé sur C++, Java est un langage orienté objet plus « pur ».
C++ et Java sont tous les deux des langages hybrides, mais dans Java, les concepteurs ont pensé que l'hybridation est moins importante qu'elle ne l'est en C++. Un langage hybride autorise plusieurs styles de programmation : C++ est hybride pour assurer la compatibilité avec le langage C. Comme C++ est une extension du langage C, il contient un grand nombre des particularités indésirables de ce langage, ce qui peut rendre certains aspects du C++ particulièrement embrouillés.
Le langage Java suppose qu'on ne veut faire que de la programmation orientée objet (POO). Ceci signifie qu'avant de pouvoir commencer il faut tourner sa vision des choses vers un monde orienté objets (à moins qu'elle ne le soit déjà). L'avantage de cet effort préliminaire est la capacité à programmer dans un langage qui est plus simple à apprendre et à utiliser que beaucoup d'autres langages de POO. Dans ce chapitre nous verrons les composantes de base d'un programme Java et nous apprendrons que tout dans Java est objet, même un programme Java.
Chaque langage de programmation a ses propres façons de manipuler les données. Parfois le programmeur doit être constamment conscient du type des manipulations en cours. Manipulez-vous l'objet directement, ou avez-vous affaire à une sorte de représentation indirecte (un pointeur en C ou C++) qui doit être traité avec une syntaxe particulière ?
Tout ceci est simplifié en Java. On considère tout comme des objets, ainsi il n'y a qu'une seule syntaxe cohérente qui est utilisée partout. Bien qu'on traite tout comme des objets, les identificateurs qui sont manipulés sont en réalité des « références » vers des objets [21]. On pourrait imaginer cette situation comme une télévision (l'objet) avec une télécommande (la référence). Tant qu'on conserve cette référence, on a une liaison vers la télévision, mais quand quelqu'un dit « change de chaîne » ou « baisse le volume », ce qu'on manipule est la référence, qui en retour modifie l'objet. Si on veut se déplacer dans la pièce tout en contrôlant la télévision, on emporte la télécommande/référence, pas la télévision.
De plus, la télécommande peut exister par elle même sans télévision. C'est à dire que le fait d'avoir une référence ne signifie pas nécessairement qu'un objet y soit associé. Ainsi, si on veut avoir avoir un mot ou une phrase, on crée une référence sur une String :
Mais on a seulement créé la référence, pas un objet. À ce point, si on décidait d'envoyer un message à s, on aurait une erreur (lors de l'exécution) parce que s n'est pas rattachée à quoi que ce soit (il n'y a pas de télévision). Une pratique plus sûre est donc de toujours initialiser une référence quand on la crée :
Toutefois, ceci utilise une caractéristique spéciale de Java : les chaînes de caractères peuvent être initialisées avec du texte entre guillemets. Normalement, on doit utiliser un type d'initialisation plus général pour les objets.
Quand on crée une référence, on veut la connecter à un nouvel objet. Ceci se fait, en général, avec le mot-clef new. new veut dire « fabrique moi un de ces objets ». Ainsi, dans l'exemple précédent, on peut dire :
Ceci ne veut pas seulement dire « fabrique moi un nouvel objet String », mais cela donne aussi une information sur comment fabriquer l'objet String en fournissant une chaîne de caractères initiale.
Bien sûr, String n'est pas le seul type qui existe. Java propose une pléthore de types tout prêts. Le plus important est qu'on puisse créer ses propres types. En fait, c'est l'activité fondamentale en programmation Java et c'est ce qu'on apprendra à faire dans la suite de ce livre.
Il est utile de visualiser certains aspects de comment les choses sont arrangées lorsque le programme tourne, en particulier comment la mémoire est organisée. Il y a six endroits différents pour stocker les données :
Il y a un ensemble de types qui sont soumis à un traitement particulier ; ils peuvent être considérés comme les types « primitifs » fréquemment utilisés en programmation. La raison de ce traitement particulier est que la création d'un objet avec new -- en particulier une simple variable -- n'est pas très efficace parce que new place les objets sur le segment. Pour ces types, Java a recours à l'approche retenue en C et en C++. Au lieu de créer la variable en utilisant new, une variable « automatique », qui n'est pas une référence, est créée. La variable contient la valeur et elle est placée sur la pile, ce qui est beaucoup plus efficace.
Java fixe la taille de chacun des types primitifs. Ces tailles ne changent pas d'une architecture de machine à une autre, comme c'est le cas dans la plupart des langages. L'invariance de la taille de ces types est l'une des raisons pour lesquelles Java est si portable.
Type primitif | Taille | Minimum | Maximum | Type wrapper |
boolean | - | - | - | Boolean |
char | 16-bit | Unicode 0 | Unicode 216- 1 | Character |
byte | 8-bit | -128 | +127 | Byte |
short | 16-bit | -215 | +215-1 | Short |
int | 32-bit | -231 | +231-1 | Integer |
long | 64-bit | -263 | +263-1 | Long |
float | 32-bit | IEEE754 | IEEE754 | Float |
double | 64-bit | IEEE754 | IEEE754 | Double |
void | - | - | - | Void |
Tous les types numériques sont signés, il est donc inutile d'aller chercher après des types non signés.
Les types de données primitifs sont aussi associés à des classes « wrapper ». Ceci signifie que pour faire un objet non primitif sur le segment pour représenter ce type primitif il faut utiliser le wrapper associé. Par exemple :
On peut aussi utiliser :
Les raisons pour lesquelles on fait ceci seront indiquées dans un prochain chapitre.
Java contient deux classes pour effectuer des opérations arithmétiques de grande précision : BigInteger et BigDecimal. Bien que ceux-ci soient dans la même catégorie que les classes « wrapper », aucun d'eux n'a d'analogue primitif.
Chacune de ces classes a des méthodes qui fournissent des opérations analogues à celles qu'on peut faire sur les types primitifs. C'est à dire qu'avec un BigInteger ou un BigDecimal on peut faire tout ce qu'on peut faire avec un int ou un float, seulement il faut utiliser des appels de méthodes au lieu des opérateurs. Par ailleurs, comme elles en font plus, les opérations sont plus lentes. On échange la vitesse contre la précision.
BigInteger sert aux entiers de précision arbitraire. C'est à dire qu'ils permettent de représenter des valeurs entières de n'importe quelle taille sans perdre aucune information au cours des opérations.
BigDecimal sert aux nombres à virgule fixe de précision arbitraire ; par exemple, on peut les utiliser pour des calculs monétaires précis.
Il faut se reporter à la documentation en ligne pour obtenir des détails sur les constructeurs et méthodes utilisables avec ces deux classes.
Pratiquement tous les langages de programmation gèrent les tableaux. Utiliser des tableaux en C ou C++ est dangereux car ces tableaux ne sont que des blocs de mémoire. Si un programme accède à un tableau en dehors de son bloc mémoire, ou s'il utilise la mémoire avant initialisation (erreurs de programmation fréquentes) les résultats seront imprévisibles.
Un des principaux objectifs de Java est la sécurité, aussi, un grand nombre des problèmes dont souffrent C et C++ ne sont pas répétés en Java. On est assuré qu'un tableau Java est initialisé et qu'il ne peut pas être accédé en dehors de ses bornes. La vérification des bornes se fait au prix d'un petit excédent de mémoire pour chaque tableau ainsi que de la vérification de l'index lors de l'exécution, mais on suppose que le gain en sécurité et en productivité vaut la dépense.
Quand on crée un tableau d'objets, on crée en réalité un tableau de références, et chacune de ces références est automatiquement initialisée à une valeur particulière avec son propre mot clé : null. Quand Java voit null, il reconnaît que la référence en question ne pointe pas vers un objet. Il faut affecter un objet à chaque référence avant de l'utiliser et si on essaye d'utiliser une référence encore à null, le problème sera signalé lors de l'exécution. Ainsi, les erreurs typiques sur les tableaux sont évitées en Java.
On peut aussi créer des tableaux de variables de type primitif. À nouveau, le compilateur garantit l'initialisation car il met à zéro la mémoire utilisée par ces tableaux.
Les tableaux seront traités plus en détails dans d'autres chapitres.
Dans la plupart des langages de programmation, le concept de durée de vie d'une variable monopolise une part significative des efforts de programmation. Combien de temps une variable existe-t-elle ? S'il faut la détruire, quand faut-il le faire ? Des erreurs sur la durée de vie des variables peuvent être la source de nombreux bugs et cette partie montre comment Java simplifie énormément ce problème en faisant le ménage tout seul.
La plupart des langages procéduraux ont le concept de portée. Il fixe simultanément la visibilité et la durée de vie des noms définis dans cette portée. En C, C++ et Java, la portée est fixée par l'emplacement des accolades {}. Ainsi, par exemple :
Une variable définie dans une portée n'est accessible que jusqu'à la fin de cette portée.
L'indentation rend le code Java plus facile à lire. Étant donné que Java est un langage indépendant de la mise en page, les espaces, tabulations et retours chariots supplémentaires ne changent pas le programme.
Il faut remarquer qu'on ne peut pas faire la chose suivante, bien que cela soit autorisé en C et C++ :
Le compilateur annoncera que la variable x a déjà été définie. Ainsi, la faculté du C et du C++ à « cacher » une variable d'une portée plus étendue n'est pas autorisée parce que les concepteurs de Java ont pensé que ceci mène à des programmes confus.
Les objets Java n'ont pas la même durée de vie que les variables primitives. Quand on crée un objet Java avec new, il existe toujours après la fin de la portée. Ainsi, si on fait :
la référence s disparaît à la fin de la portée. Par contre l'objet String sur lequel s pointait occupe toujours la mémoire. Dans ce bout de code, il n'y a aucun moyen d'accéder à l'objet parce que son unique référence est hors de portée. Dans d'autres chapitres on verra comment la référence à un objet peut être transmise et dupliquée dans un programme.
Il s'avère que du simple fait qu'un objet créé avec new reste disponible tant qu'on le veut, tout un tas de problèmes de programmation du C++ disparaissent tout simplement en Java. Il semble que les problèmes les plus durs surviennent en C++ parce que le langage ne fournit aucune aide pour s'assurer que les objets sont disponibles quand on en a besoin. Et, encore plus important, en C++ on doit s'assurer qu'on détruit bien les objets quand on en a terminé avec eux.
Ceci amène une question intéressante. Si Java laisse les objets traîner, qu'est-ce qui les empêche de complètement remplir la mémoire et d'arrêter le programme ? C'est exactement le problème qui surviendrait dans un programme C++. C'est là qu'un peu de magie apparaît. Java a un ramasse-miettes qui surveille tous les objets qui ont été créés avec new et qui arrive à deviner lesquels ne sont plus référencés. Ensuite il libère la mémoire de ces objets de façon à ce que cette mémoire puisse être utilisée pour de nouveaux objets. Ceci signifie qu'il ne faut jamais s'embêter à récupérer la mémoire soi-même. On crée simplement les objets, et quand on n'en a plus besoin, ils disparaissent d'eux même. Ceci élimine toute une classe de problèmes de programmation : les soi-disant « fuites de mémoire » qui arrivent quand un programmeur oublie de libérer la mémoire.
Si tout est objet, qu'est-ce qui définit à quoi ressemble une classe particulière d'objets et comment elle se comporte ? Autrement dit, qu'est-ce qui constitue le type d'un objet ? On pourrait s'attendre à avoir un mot-clef appelé « type », et cela serait parfaitement sensé. Historiquement, toutefois, la plupart des langages orientés objet ont utilisé le mot-clef class qui signifie « je vais décrire à quoi ressemble un nouveau type d'objet ». Le mot-clef class (qui est si commun qu'il ne sera plus mis en gras dans la suite de ce livre) est suivi par le nom du nouveau type. Par exemple :
Ceci introduit un nouveau type, on peut alors créer un objet de ce type en utilisant new :
Dans ATypeName, le corps de la classe ne consiste qu'en un commentaire (les étoiles et barres obliques et ce qu'il y a à l'intérieur, ce qui sera décrit ultérieurement dans ce chapitre), donc il n'y pas grand chose à faire avec. En fait, on ne peut pas lui dire de faire quoi que ce soit (c'est à dire qu'on ne peut pas lui transmettre de message intéressant) tant qu'on n'y définit pas de méthodes.