IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
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

  Chapitre 2 - Tout est « objet »

pages : 1 2 3 4 

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.

Les objets sont manipulés avec des références

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 :

String s;

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 :

String s = "asdf";

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.

Vous devez créer tous 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 :

String s = new String("asdf");

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.

Où réside la mémoire ?

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 :

  1. Les registres. C'est le stockage le plus rapide car il se trouve à un endroit différent des autres zones de stockage : dans le processeur. Toutefois, le nombre de registres est sévèrement limité, donc les registres sont alloués par le compilateur en fonction de ses besoins. On n'a aucun contrôle direct et il n'y a même aucune trace de l'existence des registres dans les programmes.
  2. La pile. Elle se trouve dans la RAM (random access memory) mais elle est prise en compte directement par le processeur via son pointeur de pile. Le pointeur de pile est déplacé vers le bas pour créer plus d'espace mémoire et déplacé vers le haut pour libérer cet espace. C'est un moyen extrêmement efficace et rapide d'allouer de la mémoire, supplanté seulement par les registres. Le compilateur Java doit connaître, lorsqu'il crée le programme, la taille et la durée de vie exacte de toutes les données qui sont rangées sur la pile, parce qu'il doit générer le code pour déplacer le pointeur de pile vers le haut et vers le bas. Cette contrainte met des limites à la flexibilité des programmes, donc, bien qu'il y ait du stockage Java sur la pile -- en particulier les références aux objets -- les objets Java eux même ne sont pas placés sur la pile.
  3. Le segment. C'est une réserve de mémoire d'usage général (aussi en RAM) où résident tous les objets java. La bonne chose à propos du segment est que, contrairement à la pile, le compilateur n'a pas besoin de savoir de combien de place il a besoin d'allouer sur le segment ni combien de temps cette place doit rester sur le segment.Ainsi, il y a une grande flexibilité à utiliser la mémoire sur le segment. Lorsqu'on a besoin de créer un objet, il suffit d'écrire le code pour le créer en utilisant new et la mémoire est allouée sur le segment lorsque le programme s'exécute. Bien entendu il y a un prix à payer pour cette flexibilité : il faut plus de temps pour allouer de la mémoire sur le segment qu'il n'en faut pour allouer de la mémoire sur la pile (c'est à dire si on avait la possibilité de créer des objets sur la pile en Java, comme on peut le faire en C++).
  4. La mémoire statique. « Statique » est utilisé ici dans le sens « à un endroit fixe » (bien que ce soit aussi dans la RAM). La mémoire statique contient les données qui sont disponibles pendant tout le temps d'exécution du programme. On peut utiliser le mot-clef static pour spécifier qu'un élément particulier d'un objet est statique, mais les objets Java par eux-mêmes ne sont jamais placés dans la mémoire statique.
  5. Les constantes. Les valeurs des constantes sont souvent placées directement dans le code du programme, ce qui est sûr puisqu'elles ne peuvent jamais changer. Parfois les constantes sont isolées de façon à pouvoir être optionnellement placées dans une mémoire accessible en lecture seulement (ROM).
  6. Stockage hors RAM. Si les données résident entièrement hors du programme, elles peuvent exister même quand le programme ne tourne pas, en dehors du contrôle du programme. Les deux exemples de base sont les flots de données, pour lesquels les données sont transformées en flots d'octets, généralement pour être transmises vers une autre machine, et les objets persistants, pour lesquels les objets sont placés sur disque de façon à ce qu'ils conservent leur état même après que le programme soit terminé. L'astuce avec ces types de stockage est de transformer les objets en quelque chose qui peut exister sur l'autre support, tout en pouvant être ressuscité en un objet normal en mémoire, lorsque c'est nécessaire. Java fournit des outils pour la persistance légère, et les versions futures pourraient fournir des solutions plus complètes pour la persistance.

Cas particulier : les types primitifs

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 :

char c = 'x';Character C =
  new Character(c);

On peut aussi utiliser :

Character C = new
  Character('x');

Les raisons pour lesquelles on fait ceci seront indiquées dans un prochain chapitre.

Nombres de grande précision

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.

Tableaux en Java

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.

Vous n'avez jamais besoin de détruire un objet

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.

Notion de portée

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 :

{
  int x = 12;
  /* seul x est accessible */
  {
    int q = 96;
    /* x & q sont tous les deux accessibles */
  }
  /* seul x est accessible */
  /* q est « hors de portée » */
}

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++ :

{
  int x = 12;
  {
    int x = 96; /* illegal */
  }
}

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.

Portée des objets

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 :

{
  String s = new String("a string");
} /* fin de portée */

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.

Créer de nouveaux types de données : class

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 :

class ATypeName { /* le corps de la classe vient ici */ }

Ceci introduit un nouveau type, on peut alors créer un objet de ce type en utilisant new :

ATypeName a = new ATypeName();

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.

Ce livre a été écrit par Bruce Eckel ( télécharger la version anglaise : Thinking in java )
Ce chapitre a été traduit par Raczy ( groupe de traduction )
télécharger la version francaise (PDF) | Commandez le livre en version anglaise (amazon) | télécharger la version anglaise
pages : 1 2 3 4 
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