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 5 - Cacher l'implémentation

pages : 1 2 3 

Une règle principale en conception orientée objet est de « séparer les choses qui changent des choses qui ne changent pas ».

Ceci est particulièrement important pour les bibliothèques [libraries]. Les utilisateurs (programmeurs clients) de cette bibliothèque doivent pouvoir s'appuyer sur la partie qu'ils utilisent, et savoir qu'ils n'auront pas à réécrire du code si une nouvelle version de la bibliothèque sort. Inversement, le créateur de la bibliothèque doit avoir la liberté de faire des modifications et améliorations avec la certitude que le code du programmeur client ne sera pas affecté par ces modifications.

On peut y parvenir à l'aide de conventions. Par exemple le programmeur de bibliothèque doit admettre de ne pas enlever des méthodes existantes quand il modifie une classe dans la bibliothèque, puisque cela casserait le code du programmeur. La situation inverse est plus délicate. Dans le cas d'un membre de données, comment le créateur de bibliothèque peut-il savoir quels membres de données ont été accédés par les programmeurs clients ? C'est également vrai avec les méthodes qui ne sont qu'une partie de l'implémentation d'une classe et qui ne sont pas destinées à être utilisées directement par le programmeur client. Mais que se passe-t-il si le créateur de bibliothèque veut supprimer une ancienne implémentation et en mettre une nouvelle ? Changer un de ces membres pourrait casser le code d'un programmeur client. De ce fait la marge de manoeuvre du créateur de bibliothèque est plutôt étroite et il ne peut plus rien changer.

Pour corriger ce problème, Java fournit des spécificateurs d'accès pour permettre au créateur de bibliothèque de dire au programmeur client ce qui est disponible et ce qui ne l'est pas. Les niveaux de contrôles d'accès, depuis le « plus accessible » jusqu'au « moins accessible » sont public, protected (protégé), « friendly » (amical, qui n'a pas de mot-clé), et private (privé). Le paragraphe précédent semble montrer que, en tant que concepteur de bibliothèque, on devrait tout garder aussi « privé » [private] que possible, et n'exposer que les méthodes qu'on veut que le programmeur client utilise. C'est bien ce qu'il faut faire, même si c'est souvent non intuitif pour des gens qui programment dans d'autres langages (particulièrement en C) et qui ont l'habitude d'accéder à tout sans restrictions. Avant la fin de ce chapitre vous devriez être convaincus de l'importance du contrôle d'accès en Java.

Le concept d'une bibliothèque de composants et le contrôle de qui peut accéder aux composants de cette bibliothèque n'est cependant pas complet. Reste la question de savoir comment les composants sont liés entre eux dans une unité de bibliothèque cohérente. Ceci est contrôlé par le mot-clé package en Java, et les spécificateurs d'accès varient selon que la classe est dans le même package ou dans un autre package. Donc, pour commencer ce chapitre, nous allons apprendre comment les composants de bibliothèques sont placés dans des packages. Nous serons alors capables de comprendre la signification complète des spécificateurs d'accès.

package : l'unité de bibliothèque

Un package est ce qu'on obtient lorsqu'on utilise le mot-clé import pour apporter une bibliothèque complète, tel que

import java.util.*;

Cette instruction apporte la bibliothèque complète d'utilitaires qui font partie de la distribution Java standard. Comme par exemple la classe ArrayList est dans java.util, on peut maintenant soit spécifier le nom complet java.util.ArrayList (ce qui peut se faire sans l'instruction import), ou on peut simplement dire ArrayList (grâce à l'instruction import).

Si on veut importer une seule classe, on peut la nommer dans l'instruction import :

import java.util.ArrayList;

Maintenant on peut utiliser ArrayList sans précision. Cependant, aucune des autres classes de java.util n'est disponible.

La raison de tous ces imports est de fournir un mécanisme pour gérer les « espaces de nommage » [name spaces]. Les noms de tous les membres de classe sont isolés les uns des autres. Une méthode f() dans une classe A ne sera pas en conflit avec une f() qui a la même signature (liste d'arguments) dans une classe B. Mais qu'en est-il des noms des classes ? Que se passe-t-il si on crée une classe stack qui est installée sur une machine qui a déjà une classe stack écrite par quelqu'un d'autre ? Avec Java sur Internet, ceci peut se passer sans que l'utilisateur le sache, puisque les classes peuvent être téléchargées automatiquement au cours de l'exécution d'un programme Java.

Ce conflit de noms potentiel est la raison pour laquelle il est important d'avoir un contrôle complet sur les espaces de nommage en Java, et d'être capable de créer des noms complètement uniques indépendamment des contraintes d'Internet.

Jusqu'ici, la plupart des exemples de ce livre étaient dans un seul fichier et ont été conçus pour un usage en local, et ne se sont pas occupés de noms de packages (dans ce cas le nom de la classe est placé dans le « package par défaut » ). C'est certainement une possibilité, et dans un but de simplicité cette approche sera utilisée autant que possible dans le reste de ce livre. Cependant, si on envisage de créer des bibliothèques ou des programmes amicaux [friendly] vis-à-vis d'autres programmes sur la même machine, il faut penser à se prémunir des conflits de noms de classes.

Quand on crée un fichier source pour Java, il est couramment appelé une unité de compilation [compilation unit](parfois une unité de traduction [translation unit]). Chaque unité de compilation doit avoir un nom se terminant par .java, et dans l'unité de compilation il peut y avoir une classe public qui doit avoir le même nom que le fichier (y compris les majuscules et minuscules, mais sans l'extension .java ). Il ne peut y avoir qu'une seule classe public dans chaque unité de compilation, sinon le compilateur sortira une erreur. Le reste des classes de cette unité de compilation, s'il y en a, sont cachées du monde extérieur parce qu'elles ne sont pas public, elles sont des classes « support » pour la classe public principale.

Quand on compile un fichier .java, on obtient un fichier de sortie avec exactement le même nom mais avec une extension .class pour chaque classe du fichier .java. De ce fait on peut obtenir un nombre important de fichiers .class à partir d'un petit nombre de fichiers .java. Si vous avez programmé avec un langage compilé, vous avez sans doute remarqué que le compilateur génère un fichier de forme intermédiaire (généralement un fichier « obj » ) qui est ensuite assemblé avec d'autres fichiers de ce type à l'aide d'un éditeur de liens [linker] (pour créer un fichier exécutable) ou un « gestionnaire de bibliothèque » [librarian](pour créer une bibliothèque). Ce n'est pas comme cela que Java travaille ; un programme exécutable est un ensemble de fichiers .class, qui peuvent être empaquetés [packaged] et compressés dans un fichier JAR (en utilisant l'archiveur Java jar). L'interpréteur Java est responsable de la recherche, du chargement et de l'interprétation de ces fichiers [32].

Une bibliothèque est aussi un ensemble de ces fichiers classes. Chaque fichier possède une classe qui est public (on n'est pas obligé d'avoir une classe public, mais c'est ce qu'on fait classiquement), de sorte qu'il y a un composant pour chaque fichier. Si on veut dire que tous ces composants (qui sont dans leurs propres fichiers .java et .class séparés) sont reliés entre eux, c'est là que le mot-clé package intervient.

Quand on dit :

package mypackage;

au début d'un fichier (si on utilise l'instruction package, elle doit apparaître à la première ligne du fichier, commentaires mis à part), on déclare que cette unité de compilation fait partie d'une bibliothèque appelée mypackage. Ou, dit autrement, cela signifie que le nom de la classe public dans cette unité de compilation est sous la couverture du nom mypackage, et si quelqu'un veut utiliser ce nom, il doit soit spécifier le nom complet, soit utiliser le mot-clé import en combinaison avec mypackage (utilisation des choix donnés précédemment). Remarquez que la convention pour les noms de packages Java est de n'utiliser que des lettres minuscules, même pour les mots intermédiaires.

Par exemple, supposons que le nom du fichier est MyClass.java. Ceci signifie qu'il peut y avoir une et une seule classe public dans ce fichier, et que le nom de cette classe doit être MyClass (y compris les majuscules et minuscules) :

package mypackage;
public class MyClass {
  // . . .

Maintenant, si quelqu'un veut utiliser MyClass ou, aussi bien, une des autres classes public de mypackage, il doit utiliser le mot-clé import pour avoir à sa disposition le ou les noms définis dans mypackage. L'autre solution est de donner le nom complet :

mypackage.MyClass m = new mypackage.MyClass();

Le mot-clé import peut rendre ceci beaucoup plus propre :

import mypackage.*;
// . . .
MyClass m = new MyClass();

Il faut garder en tête que ce que permettent les mots-clés package et import, en tant que concepteur de bibliothèque, c'est de diviser l'espace de nommage global afin d'éviter le conflit de nommages, indépendamment de combien il y a de personnes qui vont sur Internet écire des classes en Java.

Créer des noms de packages uniques

On pourrait faire remarquer que, comme un package n'est jamais réellement « empaqueté » [packaged] dans un seul fichier, un package pourrait être fait de nombreux fichiers .class et les choses pourraient devenir un peu désordonnées. Pour éviter ceci, une chose logique à faire est de placer tous les fichiers .class d'un package donné dans un même répertoire ; c'est-à-dire utiliser à son avantage la structure hiérarchique des fichiers définie par le système d'exploitation. C'est un des moyens par lequel Java traite le problème du désordre ; on verra l'autre moyen plus tard lorsque l'utilitaire jar sera présenté.

Réunir les fichiers d'un package dans un même répertoire résoud deux autres problèmes : créer des noms de packages uniques, et trouver ces classes qui pourraient être enfouies quelque part dans la structure d'un répertoire. Ceci est réalisé, comme présenté dans le chapitre 2, en codant le chemin où se trouvent les fichiers .class dans le nom du package. Le compilateur force cela ; aussi, par convention, la première partie du nom de package est le nom de domaine Internet du créateur de la classe, renversé. Comme les noms de domaines Internet sont garantis uniques, si on suit cette convention, il est garanti que notre nom de package sera unique et donc qu'il n'y aura jamais de conflit de noms (c'est-à-dire, jusqu'à ce qu'on perde son nom de domaine au profit de quelqu'un qui commencerait à écrire du code Java avec les mêmes noms de répertoires). Bien entendu, si vous n'avez pas votre propre nom de domaine vous devez fabriquer une combinaison improbable (telle que votre prénom et votre nom) pour créer des noms de packages uniques. Si vous avez décidé de publier du code Java, cela vaut la peine d'effectuer l'effort relativement faible d'obtenir un nom de domaine.

La deuxième partie de cette astuce consiste à résoudre le nom de package à l'intérieur d'un répertoire de sa machine, de manière à ce que lorsque le programme Java s'exécute et qu'il a besoin de charger le fichier .class (ce qu'il fait dynamiquement, à l'endroit du programme où il doit créer un objet de cette classe particulière, ou la première fois qu'on accède à un membre static de la classe), il puisse localiser le répertoire où se trouve le fichier .class.

L'interpréteur Java procède de la manière suivante. D'abord il trouve la variable d'environnement CLASSPATH (positionnée à l'aide du système d'exploitation, parfois par le programme d'installation qui installe Java ou un outil Java sur votre machine). CLASSPATH contient un ou plusieurs répertoires qui sont utilisés comme racines de recherche pour les fichiers .class. En commençant à cette racine, l'interpréteur va prendre le nom de package et remplacer chaque point par un « slash » pour construire un nom de chemin depuis la racine CLASSPATH (de sorte que package foo.bar.baz devient foo\bar\baz ou foo/bar/baz ou peut-être quelque chose d'autre, selon votre système d'exploitation). Ceci est ensuite concaténé avec les diverses entrées du CLASSPATH. C'est là qu'il recherche le fichier .class portant le nom correspondant à la classe qu'on est en train d'essayer de créer (il recherche aussi certains répertoires standards relativement à l'endroit où se trouve l'interpréteur Java).

Pour comprendre ceci, prenons mon nom de domaine, qui est bruceeckel.com. En l'inversant, com.bruceeckel établit le nom global unique pour mes classes (les extensions com, edu, org, etc... étaient auparavant en majuscules dans les packages Java, mais ceci a été changé en Java 2 de sorte que le nom de package est entièrement en minuscules). Je peux ensuite subdiviser ceci en décidant de créer un répertoire simplement nommé simple, de façon à obtenir un nom de package :

package com.bruceeckel.simple;

Maintenant ce nom de package peut être utilisé comme un espace de nommage de couverture pour les deux fichiers suivants :

//: com:bruceeckel:simple:Vector.java
// Création d'un package.
package com.bruceeckel.simple;
public class Vector {
  public Vector() {
    System.out.println(
      "com.bruceeckel.util.Vector");
  }
} ///:~

Lorsque vous créerez vos propres packages, vous allez découvrir que l'instruction package doit être le premier code non-commentaire du fichier. Le deuxième fichier ressemble assez au premier :

//: com:bruceeckel:simple:List.java
// Création d'un package.
package com.bruceeckel.simple;
public class List {
  public List() {
    System.out.println(
      "com.bruceeckel.util.List");
  }
} ///:~

Chacun de ces fichiers est placé dans le sous-répertoire suivant dans mon système :

C:\DOC\JavaT\com\bruceeckel\simple

Dans ce nom de chemin, on peut voir le nom de package com.bruceeckel.simple, mais qu'en est-il de la première partie du chemin ? Elle est prise en compte dans la variable d'environnement CLASSPATH, qui est, sur ma machine :

CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT

On voit que le CLASSPATH peut contenir plusieurs chemins de recherche.

Il y a toutefois une variante lorsqu'on utilise des fichiers JAR. Il faut mettre également le nom du fichier JAR dans le classpath, et pas seulement le chemin où il se trouve. Donc pour un JAR nommé grape.jar le classpath doit contenir :

CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar

Une fois le classpath défini correctement, le fichier suivant peut être placé dans n'importe quel répertoire :

//: c05:LibTest.java
// Utilise la bibliothèque.
import com.bruceeckel.simple.*;
public class LibTest {
  public static void main(String[] args) {
    Vector v = new Vector();
    List l = new List();
  }
} ///:~

Lorsque le compilateur rencontre l'instruction import, il commence à rechercher dans les répertoires spécifiés par CLASSPATH, recherchant le sous-répertoire com\bruceeckel\simple, puis recherchant les fichiers compilés de noms appropriés (Vector.class pour Vector et List.class pour List). Remarquez que chacune des classes et méthodes utilisées de Vector et List doivent être public.

Positionner le CLASSPATH a posé tellement de problèmes aux utilisateurs débutants de Java (ça l'a été pour moi quand j'ai démarré) que Sun a rendu le JDK un peu plus intelligent dans Java 2. Vous verrez, quand vous l'installerez, que vous pourrez compiler et exécuter des programmes Java de base même si vous ne positionnez pas de CLASSPATH. Pour compiler et exécuter le package des sources de ce livre (disponible sur le CD ROM livré avec ce livre, ou sur www.BruceEckel.com), vous devrez cependant faire quelques modifications de votre CLASSPATH (celles-ci sont expliquées dans le package de sources).

Collisions

Que se passe-t-il si deux bibliothèques sont importées à l'aide de * et qu'elles contiennent les mêmes noms ? Par exemple, supposons qu'un programme fasse ceci :

import com.bruceeckel.simple.*;
import java.util.*;

Comme java.util.* contient également une classe Vector, ceci crée une collision potentielle. Cependant, tant qu'on n'écrit pas le code qui cause effectivement la collision, tout va bien ; c'est une chance, sinon on pourrait se retrouver à écrire une quantité de code importante pour éviter des collisions qui n'arriveraient jamais.

La collision se produit si maintenant on essaye de créer un Vector :

Vector v = new Vector();

A quelle classe Vector ceci se réfère-t-il ? Le compilateur ne peut pas le savoir, et le lecteur non plus. Le compilateur se plaint et nous oblige à être explicite. Si je veux le Vector Java standard, par exemple, je dois dire :

java.util.Vector v =new java.util.Vector();

Comme ceci (avec le CLASSPATH) spécifie complètement l'emplacement de ce Vector, il n'y a pas besoin d'instruction import java.util.*, à moins que j'utilise autre chose dans java.util.

Une bibliothèque d'outils personnalisée

Avec ces connaissances, vous pouvez maintenant créer vos propres bibliothèques d'outils pour réduire ou éliminer les duplications de code. On peut par exemple créer un alias pour System.out.println( ) pour réduire la frappe. Ceci peut faire partie d'un package appelé tools :

//: com:bruceeckel:tools:P.java
// Les raccourcis P.rint & P.rintln
package com.bruceeckel.tools;
public class P {
  public static void rint(String s) {
    System.out.print(s);
  }
  public static void rintln(String s) {
    System.out.println(s);
  }
} ///:~

On peut utiliser ce raccourci pour imprimer une String , soit avec une nouvelle ligne (P.rintln( )) , ou sans (P.rint( )).

Vous pouvez deviner que l'emplacement de ce fichier doit être dans un répertoire qui commence à un des répertoire du CLASSPATH, puis continue dans com/bruceeckel/tools. Après l'avoir compilé, le fichier P.class peut être utilisé partout sur votre système avec une instruction import :

//: c05:ToolTest.java
// Utilise la bibliothèque tools
import com.bruceeckel.tools.*;
public class ToolTest {
  public static void main(String[] args) {
    P.rintln("Available from now on!");
    P.rintln("" + 100); // Le force à être une String
    P.rintln("" + 100L);
    P.rintln("" + 3.14159);
  }
} ///:~

Remarquez que chacun des objets peut facilement être forcé dans une représentation String en les plaçant dans une expression String ; dans le cas ci-dessus, le fait de commencer l'expression avec une String vide fait l'affaire. Mais ceci amène une observation intéressante. Si on appelle System.out.println(100), cela fonctionne sans le transformer [cast] en String. Avec un peu de surcharge [overloading], on peut amener la classe P à faire ceci également (c'est un exercice à la fin de ce chapitre).

A partir de maintenant, chaque fois que vous avez un nouvel utilitaire intéressant, vous pouvez l'ajouter au répertoire tools (ou à votre propre répertoire util ou tools).

Ce livre a été écrit par Bruce Eckel ( télécharger la version anglaise : Thinking in java )
Ce chapitre a été traduit par Phillipe Boite ( 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 
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