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 

Lorsqu'on définit une classe (et tout ce que l'on fait en Java consiste à définir des classes, fabriquer des objets à partir de ces classes et envoyer des messages à ces objets) on peut mettre deux types d'éléments dans ces classes : des données membres de la classe (aussi appelées champs) et des fonctions membres de la classe (habituellement appelées méthodes). Une donnée membre est un objet de n'importe quel type avec lequel on peut communiquer via sa référence. Il peut aussi s'agir d'un des types primitifs (dans ce cas, ce n'est pas une référence). S'il s'agit d'une référence à un objet, il faut initialiser cette référence pour la connecter à un objet réel (en utilisant new comme indiqué précédemment) grâce à une fonction particulière appelée un constructeur (entièrement décrit dans le chapitre 4). S'il s'agit d'un type primitif il est possible de l'initialiser directement lors de sa définition dans la classe (comme on le verra plus tard, les références peuvent aussi être initialisées lors de la définition).

Chaque objet met ses données membres dans sa zone de mémoire propre, les données membres ne sont pas partagées entre les objets. Voici un exemple de classe avec des données membres :

class DataOnly {
  int i;
  float f;
  boolean b;
}

Cette classe ne fait rien mais on peut créer un objet :

DataOnly d = new DataOnly();

On peut affecter des valeurs aux données membres mais il faut d'abord savoir comment faire référence à un membre d'un objet. Ceci s'effectue en indiquant le nom de la référence à l'objet, suivi par un point, suivi par le nom du membre dans l'objet :

objectReference.member

Par exemple :

d.i = 47;
d.f = 1.1f;
d.b = false;

Il est aussi possible que l'objet puisse contenir d'autres objets qui contiennent des données que l'on souhaite modifier. Pour cela il suffit de continuer à « associer les points ». Par exemple :

myPlane.leftTank.capacity = 100;

La classe DataOnly ne peut pas faire grand chose à part contenir des données car elle n'a pas de fonctions membres (méthodes). Pour comprendre comment celles-ci fonctionnent il faut d'abord comprendre les notions de paramètres et de valeurs de retour, qui seront brièvement décrites.

Valeurs par défaut des membres primitifs

Quand une donnée d'un type primitif est membre d'une classe on est assuré qu'elle a une valeur par défaut si on ne l'initialise pas :

Type primitif Valeur par défaut
boolean false
char `\u0000' (null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d

Par prudence, il faut remarquer que les valeurs par défaut sont celles que Java garantit quand la variable est utilisée comme un membre d'une classe. Ceci assure que les variables membres de type primitif sont toujours initialisées (parfois C++ ne le fait pas), ce qui supprime une source de bugs. Toutefois, cette valeur initiale peut ne pas être correcte ou même légale pour le programme qui est écrit. Il est préférable de toujours initialiser explicitement les variables.

Cette garantie ne s'applique pas aux variables « locales » -- celles qui ne sont pas des champs d'une classe. Ainsi, si dans la définition d'une fonction, on a :

int x;

Alors x aura une valeur arbitraire (comme en C et C++), il ne sera pas initialisé automatiquement à zéro. On a la responsabilité d'affecter une valeur appropriée avant d'utiliser x. Si on oublie de le faire, Java est sans aucun doute mieux conçu que C++ sur ce point : on obtient une erreur de compilation qui dit que la variable pourrait ne pas être initialisée (avec beaucoup de compilateurs C++ on a des avertissements concernant les variables non initialisées, mais avec Java ce sont des erreurs).

Méthodes, paramètres et valeurs de retour

Jusqu'à présent le terme fonction a été employé pour désigner une sous-routine nommée. Le terme qui est plus généralement employé en Java est méthode, en tant que « moyen de faire quelque chose ». Il est possible, si on le souhaite, de continuer à raisonner en terme de fonctions. Il s'agit simplement d'une différence de syntaxe, mais à partir de maintenant on utilisera « méthode » plutôt que fonction, dans ce livre.

Les méthodes en Java définissent les messages qu'un objet peut recevoir. Dans cette partie on verra à quel point il est simple de définir une méthode.

Les éléments fondamentaux d'une méthode sont le nom, les paramètres, le type de retour et le corps. Voici la forme de base :

returnType methodName( /* liste de paramètres */ ) {
  /* corps de la méthode */
}

Le type de retour est le type de la valeur qui est retournée par la méthode après son appel. La liste de paramètres donne le type et le nom des informations qu'on souhaite passer à la méthode. L'association du nom de la méthode et de la liste de paramètres identifie de façon unique la méthode.

En Java, les méthodes ne peuvent être créées que comme une composante d'une classe. Une méthode ne peut être appelée que pour un objet [22] et cet objet doit être capable de réaliser cet appel de méthode. Si on essaye d'appeler une mauvaise méthode pour un objet, on obtient un message d'erreur lors de la compilation. On appelle une méthode pour un objet en nommant l'objet suivi d'un point, suivi du nom de la méthode et de sa liste d'arguments, comme ça : objectName.methodName(arg1, arg2, arg3). Par exemple, si on suppose qu'on a une méthode f( ) qui ne prend aucun paramètre et qui retourne une valeur de type int. Alors, si on a un objet appelé a pour lequel f( ) peut être appelé, on peut écrire :

int x = a.f();

Le type de la valeur de retour doit être compatible avec le type de x.

On appelle généralement envoyer un message à un objet cet acte d'appeler une méthode. Dans l'exemple précédent le message est f() et l'objet est a. La programmation orientée objet est souvent simplement ramenée à « envoyer des messages à des objets ».

La liste de paramètres

La liste de paramètres de la méthode spécifie quelles informations on passe à la méthode. Comme on peut le supposer, ces informations -- comme tout le reste en Java -- sont sous la forme d'objets. On doit donc indiquer dans la liste de paramètres les types des objets à transmettre et les noms à employer pour chacun. Comme dans toutes les situation où on a l'impression de manipuler des objets, en Java on passe effectivement des références name="fnB23">[23]. Toutefois, le type de la référence doit être correct. Si le paramètre est censé être un objet de type String, ce qu'on transmet doit être de ce type.

Considérons une méthode qui prend un objet de classe String en paramètre. Voici la définition qui doit être mise à l'intérieur de la définition d'une classe pour qu'elle soit compilée :

int storage(String s) {
  return s.length() * 2;
}

Cette méthode indique combien d'octets sont nécessaires pour contenir une String donnée (chaque char dans une String fait 16 bits, ou deux octets, pour permettre les caractères Unicode). Le paramètre est de type String et il est appelé s. Une fois que s est passé à une méthode, il peut être traité comme n'importe quel autre objet (on peut lui envoyer des messages). Ici, on appelle la méthode length( ) qui est une des méthodes de la classe String ; elle retourne le nombre de caractères que contient la chaîne.

On peut aussi voir l'utilisation du mot-clef return qui fait deux choses. D'abord, il signifie « quitte la méthode, j'ai terminé ». Ensuite, si la méthode retourne une valeur, cette valeur est placée juste après la déclaration du return. Dans le cas présent, la valeur de retour est produite en évaluant l'expression s.length( ) * 2.

On peut retourner des valeurs de tous les types qu'on souhaite, mais si on souhaite ne rien retourner du tout on peut le faire en indiquant que la méthode retourne void. Voici quelques exemples :

boolean flag() { return true; }
float naturalLogBase() { return 2.718f; }
void nothing() { return; }
void nothing2() {}

Quand le type de retour est void, alors le mot-clef return n'est utilisé que pour sortir de la méthode, il n'est donc pas nécessaire quand on atteint la fin de la méthode. On peut retourner d'une méthode à n'importe quel endroit mais si on a indiqué un type de retour qui n'est pas void alors le compilateur imposera (avec des messages d'erreur) un retour avec une valeur d'un type approprié sans tenir compte de l'endroit auquel le retour se produit.

À ce point, on peut penser qu'un programme n'est qu'un paquet d'objets avec des méthodes qui prennent d'autres objets en paramètres pour transmettre des messages à ces autres objets. C'est effectivement l'essentiel de ce qui se passe mais dans les chapitres suivants on verra comment faire le travail de bas niveau en prenant des décisions au sein d'une méthode. Pour ce chapitre, envoyer des messages est suffisant.

Construction d'un programme Java

Il y a plusieurs autres éléments à comprendre avant de voir le premier programme Java.

Visibilité des noms

Un problème commun à tous les langages de programmation est le contrôle des noms. Si on utilise un nom dans un module du programme et si un autre programmeur utilise le même nom dans un autre module, comment distingue-t-on un nom d'un autre et comment empêche-t-on les « collisions » de noms ? En C c'est un problème particulier car un programme est souvent un océan de noms incontrôlable. Les classes C++ (sur lesquelles les classes Java sont basées) imbriquent les fonctions dans les classes de telle sorte qu'elles ne peuvent pas entrer en collision avec les noms de fonctions imbriqués dans d'autres classes. Toutefois, C++ autorise toujours les données et les fonctions globales, donc les collisions sont toujours possibles. Pour résoudre ce problème, C++ a introduit les domaines de noms (namespace) en utilisant des mots-clefs supplémentaires.

Java a pu éviter tout cela en employant une approche originale. Pour générer sans ambiguïté un nom pour une bibliothèque, le spécificateur utilisé n'est pas très différent d'un nom de domaine Internet. En fait, les créateurs de Java veulent qu'on utilise son propre nom de domaine Internet inversé, puisqu'on est assuré que ceux-ci sont uniques. Puisque mon nom de domaine est BruceEckel.com, ma bibliothèque d'utilitaires (utility) pour mes marottes (foibles) devrait être appelée com.bruceeckel.utility.foibles. Après avoir inversé le nom de domaine, les points sont destinés à représenter des sous-répertoires.

Dans Java 1.0 et Java 1.1 les extensions de domaines com, edu, org, net, etc. étaient mises en lettres capitales par convention, ainsi la bibliothèque serait : COM.bruceeckel.utility.foibles. Toutefois, au cours du développement de Java 2, on s'est rendu compte que cela causait des problèmes et par conséquent les noms de packages sont entièrement en lettres minuscules.

Ce mécanisme signifie que tous les fichiers existent automatiquement dans leur propre domaine de nom et toutes les classe contenues dans un fichier donné doivent avoir un identificateur unique. Ainsi, on n'a pas besoin d'apprendre de particularités spécifiques au langage pour résoudre ce problème -- le langage s'en occupe à votre place.

Utilisation d'autres composantes

Lorsqu'on souhaite utiliser une classe prédéfinie dans un programme, le compilateur doit savoir comment la localiser. Bien entendu, la classe pourrait déjà exister dans le même fichier source que celui d'où elle est appelée. Dans ce cas on utilise simplement la classe -- même si la classe n'est définie que plus tard dans le fichier. Java élimine le problème des « référence anticipées », il n'y a donc pas à s'en préoccuper.

Qu'en est-il des classes qui existent dans un autre fichier ? On pourrait penser que le compilateur devrait être suffisamment intelligent pour aller simplement la chercher lui même, mais il y a un problème. Imaginons que l'on veuille utiliser une classe ayant un nom spécifique mais qu'il existe plus d'une classe ayant cette définition (il s'agit probablement de définitions différentes). Ou pire, imaginons que l'on écrive un programme et qu'en le créant on ajoute à sa bibliothèque une nouvelle classe qui entre en conflit avec le nom d'une classe déjà existante.

Pour résoudre ce problème il faut éliminer les ambiguïtés potentielles. Ceci est réalisé en disant exactement au compilateur Java quelles classes on souhaite, en utilisant le mot-clef import. import dit au compilateur d'introduire un package qui est une bibliothèque de classes (dans d'autres langages, une bibliothèque pourrait comporter des fonctions et des données au même titre que des classes mais il faut se rappeler que tout le code Java doit être écrit dans des classes).

La plupart du temps on utilise des composantes des bibliothèques Java standard qui sont fournies avec le compilateur. Avec celles-ci il n'y a pas à se tracasser à propos des longs noms de domaines inversés ; il suffit de dire, par exemple :

import java.util.ArrayList;

pour dire au compilateur que l'on veut utiliser la classe Java ArrayList. Toutefois, util contient de nombreuses classes et on pourrait vouloir utiliser plusieurs d'entre elles sans les déclarer explicitement. Ceci est facilement réalisé en utilisant '*' pour indiquer un joker :

import java.util.*;

Il est plus courant d'importer une collection de classes de cette manière que d'importer les classes individuellement.

Le mot-clef static

Normalement, quand on crée une classe, on décrit ce à quoi ressemblent les objets de cette classe et comment ils se comportent. Rien n'existe réellement avant de créer un objet de cette classe avec new ; à ce moment la zone de données est créée et les méthodes deviennent disponibles.

Mais il y a deux situation pour lesquelles cette approche n'est pas suffisante. L'une, si on veut avoir une zone de stockage pour des données spécifiques, sans tenir compte du nombre d'objets créés, ou même si aucun objet n'a été créé. L'autre, si on a besoin d'une méthode qui n'est associée à aucun objet particulier de la classe. C'est à dire si on a besoin d'une méthode qui puisse être appelée même si aucun objet n'a été créé. On peut obtenir ces deux effets avec le mot-clef static. Dire que quelque chose est static signifie que la donnée ou la méthode n'est pas spécifiquement rattachée à un objet instance de cette classe. Donc, même si aucun objet de cette classe n'a jamais été créé il est possible d'appeler une méthode static ou d'accéder à une donnée static. Avec des données et des méthodes non static ordinaires il faut connaître l'objet spécifique avec lequel elles fonctionnent. Bien entendu, étant donné que les méthodes static n'ont pas besoin qu'un objet soit créé avant d'être utilisées, elles ne peuvent pas accéder directement à des membres ou des méthodes non static en appelant ces autres membres sans faire référence à un objet nommé (puisque les membres et méthodes non static doivent être rattachés à un objet spécifique).

Certains langages orientés objet emploient les expressions données de classe et méthodes de classe, ce qui signifie que les données et les méthodes n'existent que pour la classe en tant que tout et pas pour des objets particuliers de la classe. Parfois la littérature Java utilise aussi ces expressions.

Pour rendre statique une méthode ou une donnée membre il suffit de mettre le mot-clef static avant la définition. Par exemple, le code suivant crée une donnée membre static et l'initialise :

class StaticTest {
    static int i = 47;
}

Maintenant, même en créant deux objet StaticTest, il n'y aura qu'une seule zone de stockage pour StaticTest.i. Tous les objets partageront le même i. Considérons :

StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();

à ce point, st1.i et st2.i ont la même valeur 47 puisqu'elles font référence à la même zone mémoire.

Il y a deux façons de faire référence à une variable static. Comme indiqué ci-dessus, il est possible de la nommer via un objet, en disant par exemple st2.i. Il est aussi possible d'y faire référence directement par le nom de la classe, ce qui ne peut pas être fait avec un membre non static (c'est le moyen de prédilection pour faire référence à une variable static puisque cela met en évidence la nature static de la variable).

StaticTest.i++;

L'opérateur ++ incrémente la variable. À ce point, st1.i et st2.i auront tous deux la valeur 48.

Une logique similaire s'applique aux méthodes statiques. On peut faire référence à une méthode statique soit par l'intermédiaire d'un objet, comme on peut le faire avec n'importe quelle méthode, ou avec la syntaxe spécifique supplémentaire ClassName.method( ). Une méthode statique est définie de façon similaire :

class StaticFun {
  static void incr() { StaticTest.i++; }
}

On peut voir que la méthode incr( ) de StaticFun incrémente la donnée static i. On peut appeler incr( ) de façon classique, par le biais d'un objet :

StaticFun sf = new StaticFun();
sf.incr();

Ou, parce que incr( ) est une méthode statique, il est possible de l'appeler directement par sa classe :

StaticFun.incr();

Alors que static, lorsqu'il est appliqué à une donnée membre, change sans aucun doute la façon dont la donnée est créée (une pour chaque classe par opposition à une pour chaque objet dans le cas des données non statiques), lorsqu'il est appliqué à une méthode, le changement est moins significatif. Un cas important d'utilisation des méthodes static est de permettre d'appeler cette méthode sans créer d'objet. C'est essentiel, comme nous le verrons, dans la définition de la méthode main( ) qui est le point d'entrée pour exécuter une application.

Comme pour toute méthode, une méthode statique peut créer ou utiliser des objets nommés de son type, ainsi les méthodes statiques sont souvent utilisées comme « berger » pour un troupeau d'instances de son propre type.

Votre premier programme Java

Voici enfin notre premier programme . Il commence par écrire une chaîne de caractères, puis il écrit la date en utilisant la classe Date de la bibliothèque standard de Java. Il faut remarquer qu'un style de commentaire supplémentaire est introduit ici : le '//' qui est un commentaire jusqu'à la fin de la ligne.

// HelloDate.java
import java.util.*;
public class HelloDate {
  public static void main(String[] args) {
    System.out.println("Hello, it's: ");
    System.out.println(new Date());
  }
}

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