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 caractéristique manquant à Java est la compilation conditionelle du C, qui permet de changer un commutateur pour obtenir un comportement différent sans rien changer d'autre au code. La raison pour laquelle cette caractéristique n'a pas été retenue en Java est probablement qu'elle est surtout utilisée en C pour résoudre des problèmes de portabilité : des parties du code sont compilées différemment selon la plateforme pour laquelle le code est compilé. Comme le but de Java est d'être automatiquement portable, une telle caractéristique ne devrait pas être nécessaire.

Il y a cependant d'autres utilisations valables de la compilation conditionnelle. Un usage très courant est le debug de code. Les possibilités de debug sont validées pendant le développement, et invalidées dans le produit livré. Allen Holub (www.holub.com) a eu l'idée d'utiliser les packages pour imiter la compilation conditionnelle. Il l'a utilisée pour créer une version du très utile mécanisme d'affirmation [assertion mechanism] du C, dans lequel on peut dire « ceci devrait être vrai » ou « ceci devrait être faux » et si l'instruction n'est pas d'accord avec cette affirmation, on en sera averti. Un tel outil est très utile pendant la phase de debuggage.

Voici une classe qu'on utilisera pour debugger :

//: com:bruceeckel:tools:debug:Assert.java
// Outil d'affirmation pour debugger
package com.bruceeckel.tools.debug;
public class Assert {
  private static void perr(String msg) {
    System.err.println(msg);
  }
  public final static void is_true(boolean exp) {
    if(!exp) perr("Assertion failed");
  }
  public final static void is_false(boolean exp){
    if(exp) perr("Assertion failed");
  }
  public final static void
  is_true(boolean exp, String msg) {
    if(!exp) perr("Assertion failed: " + msg);
  }
  public final static void
  is_false(boolean exp, String msg) {
    if(exp) perr("Assertion failed: " + msg);
  }
} ///:~

Cette classe encapsule simplement des tests booléens, qui impriment des messages d'erreur en cas d'échec. Dans le chapitre 10, on apprendra à utiliser un outil plus sophistiqué pour traiter les erreurs, appelé traitement d'exceptions [exception handling], mais la méthode perr( ) conviendra bien en attendant.

La sortie est affichée sur la console dans le flux d'erreur standard [standard error stream]en écrivant avec System.err.

Pour utiliser cette classe, ajoutez cette ligne à votre programme :

import com.bruceeckel.tools.debug.*;

Pour supprimer les affirmations afin de pouvoir livrer le code, une deuxième classe Assert est créée, mais dans un package différent :

//: com:bruceeckel:tools:Assert.java
// Suppression de la sortie de l'affirmation
// pour pouvoir livrer le programme.
package com.bruceeckel.tools;
public class Assert {
  public final static void is_true(boolean exp){}
  public final static void is_false(boolean exp){}
  public final static void
  is_true(boolean exp, String msg) {}
  public final static void
  is_false(boolean exp, String msg) {}
} ///:~

Maintenant si on change l'instruction import précédente en :

import com.bruceeckel.tools.*;

le programme n'affichera plus d'affirmations. Voici un exemple :

//: c05:TestAssert.java
// Démonstration de l'outil d'affirmation.
// Mettre en commentaires ce qui suit, et enlever
// le commentaire de la ligne suivante
// pour modifier le comportement de l'affirmation :
import com.bruceeckel.tools.debug.*;
// import com.bruceeckel.tools.*;
public class TestAssert {
  public static void main(String[] args) {
    Assert.is_true((2 + 2) == 5);
    Assert.is_false((1 + 1) == 2);
    Assert.is_true((2 + 2) == 5, "2 + 2 == 5");
    Assert.is_false((1 + 1) == 2, "1 +1 != 2");
  }
} ///:~

En changeant le package qui est importé, on change le code de la version debug à la version de production. Cette technique peut être utilisée pour toutes sortes de code conditionnel.

Avertissement sur les packages

Il faut se rappeler que chaque fois qu'on crée un package, on spécifie implicitement une structure de répertoire en donnant un nom à un package. Le package doit se trouver dans le répertoire indiqué par son nom, qui doit être un répertoire qui peut être trouvé en partant du CLASSPATH. L'utilisation du mot-clé package peut être un peu frustrante au début, car à moins d'adhérer à la règle nom-de-package - chemin-de-répertoire, on obtient un tas de messages mystérieux à l'exécution signalant l'impossibilité de trouver une classe donnée, même si la classe est là dans le même répertoire. Si vous obtenez un message de ce type, essayez de mettre en commentaire l'instruction package, et si ça tourne vous saurez où se trouve le problème.

Les spécificateurs d'accès Java

Quand on les utilise, les spécificateurs d'accès Java public, protected, et private sont placés devant la définition de chaque membre de votre classe, qu'il s'agisse d'un champ ou d'une méthode. Chaque spécificateur d'accès contrôle l'accès pour uniquement cette définition particulière. Ceci est différent du C++, où un spécificateur d'accès contrôle toutes les définitions le suivant jusqu'à ce qu'un autre spécificateur d'accès soit rencontré.

D'une façon ou d'une autre, toute chose a un type d'accès spécifié. Dans les sections suivantes, vous apprendrez à utiliser les différents types d'accès, à commencer par l'accès par défaut.

« Friendly »

Que se passe-t-il si on ne précise aucun spécificateur d'accès, comme dans tous les exemples avant ce chapitre ? L'accès par défaut n'a pas de mot-clé, mais on l'appelle couramment « friendly » (amical). Cela veut dire que toutes les autres classes du package courant ont accès au membre amical, mais pour toutes les classes hors du package le membre apparaît private. Comme une unité de compilation -un fichier- ne peut appartenir qu'à un seul package, toutes les classes d'une unité de compilation sont automatiquement amicales entre elles. De ce fait, on dit aussi que les éléments amicaux ont un accès de package[package access].

L'accès amical permet de grouper des classes ayant des points communs dans un package, afin qu'elles puissent facilement interagir entre elles. Quand on met des classes ensemble dans un package (leur accordant de ce fait un accès mutuel à leurs membres amicaux, c'est à dire en les rendant « amicaux » ) on « possède » le code de ce package. Il est logique que seul le code qu'on possède ait un accès amical à un autre code qu'on possède. On pourrait dire que l'accès amical donne une signification ou une raison pour regrouper des classes dans un package. Dans beaucoup de langages l'organisation des définitions dans des fichiers peut être faite tant bien que mal, mais en Java on est astreint à les organiser d'une manière logique. De plus, on exclura probablement les classes qui ne devraient pas avoir accès aux classes définies dans le package courant.

La classe contrôle quel code a accès à ses membres. Il n'y a pas de moyen magique pour « entrer par effraction ». Le code d'un autre package ne peut pas dire « Bonjour, je suis un ami de Bob ! » et voir les membres protected, friendly, et private de Bob. La seule façon d'accorder l'accès à un membre est de :

  1. Rendre le membre public. Ainsi tout le monde, partout, peut y accéder.
  2. Rendre le membre amical en n'utilisant aucun spécificateur d'accès, et mettre les autres classes dans le même package. Ainsi les autres classes peuvent accéder à ce membre.
  3. Comme on le verra au Chapitre 6, lorsque l'héritage sera présenté, une classe héritée peut avoir accès à un membre protected ou public (mais pas à des membres private). Elle peut accéder à des membres amicaux seulement si les deux classes sont dans le même package. Mais vous n'avez pas besoin de vous occuper de cela maintenant.
  4. Fournir des méthodes « accessor/mutator » (connues aussi sous le nom de méthodes « get/set » ) qui lisent et changent la valeur. Ceci est l'approche la plus civilisée en termes de programmation orientée objet, et elle est fondamentale pour les JavaBeans, comme on le verra dans le Chapitre 13.

public : accès d'interface

Lorsqu'on utilise le mot-clé public, cela signifie que la déclaration du membre qui suit immédiatement public est disponible pour tout le monde, en particulier pour le programmeur client qui utilise la bibliothèque. Supposons qu'on définisse un package dessert contenant l'unité de compilation suivante :

//: c05:dessert:Cookie.java
// Création d'une bibliothèque.
package c05.dessert;
public class Cookie {
  public Cookie() {
   System.out.println("Cookie constructor");
  }
  void bite() { System.out.println("bite"); }
} ///:~

Souvenez-vous, Cookie.java doit se trouver dans un sous-répertoire appelé dessert, en dessous de c05 (qui signifie le Chapitre 5 de ce livre) qui doit être en-dessous d'un des répertoire du CLASSPATH. Ne faites pas l'erreur de croire que Java va toujours considérer le répertoire courant comme l'un des points de départ de la recherche. Si vous n'avez pas un « . » comme un des chemins dans votre CLASSPATH, Java n'y regardera pas.

Maintenant si on crée un programme qui utilise Cookie :

//: c05:Dinner.java
// Utilise la bibliothèque.
import c05.dessert.*;

public class Dinner {
  public Dinner() {
   System.out.println("Dinner constructor");
  }
  public static void main(String[] args) {
    Cookie x = new Cookie();
    //! x.bite(); // Ne peut y accéder
  }
} ///:~

on peut créer un objet Cookie, puisque son constructeur est public et que la classe est public. (Nous allons regarder plus en détail le concept d'une classe public plus tard.) Cependant, le membre bite() est inaccessible depuis Dinner.java car bite() est amical uniquement à l'intérieur du package dessert.

Le package par défaut

Vous pourriez être surpris de découvrir que le code suivant compile, bien qu'il semble ne pas suivre les règles :

//: c05:Cake.java
// Accède à une classe dans
// une unité de compilation séparée.
class Cake {
  public static void main(String[] args) {
    Pie x = new Pie();
    x.f();
  }
} ///:~

Dans un deuxième fichier, dans le même répertoire :

//: c05:Pie.java
// L'autre classe.
class Pie {
  void f() { System.out.println("Pie.f()"); }
} ///:~

On pourrait à première vue considére ces fichiers comme complètement étrangers l'un à l'autre, et cependant Cake est capable de créer l'objet Pie et d'appeler sa méthode f() ! (Remarquez qu'il faut avoir « . » dans le CLASSPATH pour que ces fichiers puissent être compilés.) On penserait normalement que Pie et f() sont amicaux et donc non accessibles par Cake. Ils sont amicaux, cette partie-là est exacte. La raison pour laquelle ils sont accessible dans Cake.java est qu'ils sont dans le même répertoire et qu'ils n'ont pas de nom de package explicite. Java traite de tels fichiers comme faisant partie du « package par défaut » pour ce répertoire, et donc amical pour tous les autres fichiers du répertoire.

private : ne pas toucher !

Le mot-clé private signifie que personne ne peut accéder à ce membre à part la classe en question, dans les méthodes de cette classe. Les autres classes du package ne peuvent accéder à des membres private, et c'est donc un peu comme si on protégeait la classe contre soi-même. D'un autre côté il n'est pas impossible qu'un package soit codé par plusieurs personnes, et dans ce cas private permet de modifier librement ce membre sans que cela affecte une autre classe dans le même package.

L'accès « amical » par défaut dans un package cache souvent suffisamment les choses ; souvenez-vous, un membre « amical » est inaccessible à l'utilisateur du package. C'est parfait puisque l'accès par défaut est celui qu'on utilise normalement (et c'est celui qu'on obtient si on oublie d'ajouter un contrôle d'accès). De ce fait, on ne pensera normalement qu'aux accès aux membres qu'on veut explicitement rendre public pour le programmeur client, et donc on pourrait penser qu'on n'utilise pas souvent le mot-clé private puisqu'on peut s'en passer (ceci est une différence par rapport au C++). Cependant, il se fait que l'utilisation cohérente de private est très importante, particulièrement lors du multithreading (comme on le verra au Chapitre 14).

Voici un exemple de l'utilisation de private :

//: c05:IceCream.java
// Démontre le mot-clé "private"
class Sundae {
  private Sundae() {}
  static Sundae makeASundae() {
    return new Sundae();
  }
}

public class IceCream {
  public static void main(String[] args) {
    //! Sundae x = new Sundae();
    Sundae x = Sundae.makeASundae();
  }
} ///:~

Ceci montre un cas où private vient à propos : on peut vouloir contrôler comment un objet est créé et empêcher quelqu'un d'accéder à un constructeur en particulier (ou à tous). Dans l'exemple ci-dessus, on ne peut pas créer un objet Sundae à l'aide de son constructeur ; il faut plutôt utiliser la méthode makeASundae() qui le fera pour nous [33].

Toute méthode dont on est certain qu'elle n'est utile qu'à cette classe peut être rendue private, pour s'assurer qu'on ne l'utilisera pas ailleurs dans le package, nous interdisant ainsi de la modifier ou de la supprimer. Rendre une méthode private garantit cela.

Ceci est également vrai pour un champ private dans une classe. A moins qu'on ne doive exposer une implémentation sous-jacente (ce qui est beaucoup plus rare qu'on ne pourrait penser), on devrait déclarer tous les membres private. Cependant, le fait que la référence à un objet est private dans une classe ne signifie pas qu'un autre objet ne puisse avoir une référence public à cet objet (voir l'annexe A pour les problèmes au sujet de l'aliasing).

protected : « sorte d'amical »

Le spécificateur d'accès protected demande un effort de compréhension . Tout d'abord, il faut savoir que vous n'avez pas besoin de comprendre cette partie pour continuer ce livre jusqu'à l'héritage (Chapitre 6). Mais pour être complet, voici une brève description et un exemple d'utilisation de protected.

Le mot-clé protected traite un concept appelé héritage, qui prend une classe existante et ajoute des membres à cette classe sans modifier la classe existante, que nous appellerons la classe de base. On peut également changer le comportement des membres d'une classe existants. Pour hériter d'une classe existante, on dit que le nouvelle classe extends (étend) une classe existante, comme ceci :

class Foo extends Bar {

Le reste de la définition de la classe est inchangé.

Si on crée un nouveau package et qu'on hérite d'une classe d'un autre package, les seuls membres accessibles sont les membres public du package d'origine. (Bien sûr, si on effectue l'héritage dans le même package, on a l'accès normal à tous les membres « amicaux » du package.) Parfois le créateur de la classe de base veut, pour un membre particulier, en accorder l'accès dans les classes dérivées mais pas dans le monde entier en général. C'est ce que protected fait. Si on reprend le fichier Cookie.java, la classe suivante ne peut pas accéder au membre « amical » :

//: c05:ChocolateChip.java
// Nepeut pas accéder à un membre amical
// dans une autre classe.
import c05.dessert.*;

public class ChocolateChip extends Cookie {
  public ChocolateChip() {
   System.out.println(
     "ChocolateChip constructor");
  }
  public static void main(String[] args) {
    ChocolateChip x = new ChocolateChip();
    //! x.bite(); // Ne peut pas accéder à bite
  }
} ///:~

Une des particularités intéressantes de l'héritage est que si la méthode bite() existe dans la classe Cookie, alors elle existe aussi dans toute classe héritée de Cookie. Mais comme bite() est « amical » dans un autre package, il nous est inaccessible dans celui-ci. Bien sûr, on pourrait le rendre public, mais alors tout le monde y aurait accès, ce qui ne serait peut-être pas ce qu'on veut. Si on modifie la classe Cookie comme ceci :

public class Cookie {
  public Cookie() {
    System.out.println("Cookie constructor");
  }
  protected void bite() {
    System.out.println("bite");
  }
}

alors bite() est toujours d'accès « amical » dans le package dessert, mais il est aussi accessible à tous ceux qui héritent de Cookie. Cependant, il n'est pas public.

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