Exercice Java : POO, encapsulation et immuabilité

Aujourd’hui j’inaugure un genre nouveau avec de petites exercices en rapport avec Java, dans l’objectif de mieux comprendre les rouages et les particularités du langage.

Nous allons donc voir qu’avec une mauvaise conception d’une classe toute simple, il est possible de « casser » le principe encapsulation, si chère à la POO, et qui permet à une instance de classe de protéger ses attributs d’éventuelles modifications externes…


Prenons donc la classe suivante dont le code complet se trouve ci-dessous :


import java.util.Calendar;
import java.util.Date;
 
public final class MyObject {
 
  private final Date date;
  private final Number number;
 
  public MyObject(Date date, Number number) {
    // On vérifie que la date soit plus récente que le 1 janvier 2000 :
    Calendar cal = Calendar.getInstance();
    cal.set(2000, java.util.Calendar.JANUARY, 1);
    if (cal.before(date)) {
      throw new IllegalArgumentException("'date' doit être supérieur à l'an 2000");
    }
    this.date = date;
   
    // On vérifie que l'objet Number soit bien un nombre positif
    if (number.intValue() < 0) {
      throw new IllegalArgumentException("'number' doit correspondre à un nombre positif !");
    }
    this.number = number;
  }
 
 
  public void hello() {
    System.out.println("Date   : " + this.date);
    System.out.println("Number : " + this.number.intValue());
    System.out.println();
  }
}

Il s’agit donc d’une classe finale (qui ne peut donc pas être redéfinie), qui dispose de deux attributs à visibilités privés. Ces deux attributs sont également finaux (ils ne peuvent pas être réassignés par la suite), et leurs valeurs sont vérifiées lors de la construction de l’objet, selon les règles suivantes :

  • Le premier attribut, de type java.util.Date, doit correspondre à une date venant après le 1er janvier 2000.
  • Le second attribut, de type java.lang.Number, doit correspondre à un nombre avec une valeur entière positive.

Bien entendu, la classe ne possède pas de méthode permettant de modifier ses attributs, mais simplement une méthode hello() qui permet d’afficher la valeur de ces attributs.

Enfin, le code s’exécutera dans un environnement protégé par un SecurityManager, ce qui fait qu’on ne pourra utiliser la méthode « barbare » setAccessible(true) de l’API de Réflection pour enfreindre les règles d’encapsulations.

L’objectif sera donc de modifier la valeur des attributs de la classe MyObject et de leurs donner des valeurs incorrects, le tout sans toucher au code de cette dernière.

L’exercice se basera sur le code suivant :


public class Main {
 
  // La méthode main() ne doit pas être modifié !
  public static void main(String[] args) throws Exception {
    // On défini un SecurityManager :
    System.setSecurityManager(new SecurityManager());
    // Et on appelle la méthode exercice() :
    exercice();
  }
 
  // Exemple de base :
  public static void exercice() {
    Date date = new Date(); // Date actuelle
    Number number = new Integer(100); // 100
 
    MyObject obj = new MyObject(date, number);
 
    obj.hello();
   
    obj.hello();
  }
}

Donc les questions sont :

  1. En modifiant uniquement le code de la méthode exercice(), comment faire en sorte que les deux appels à obj.hello() affichent des valeurs différentes pour la même instance de l’objet ?
    On pourra pour cela utiliser toutes les classes et méthodes de l’API standard, et créer autant de nouvelles classes ou méthodes que neccessaire.

    L’idéal serait d’obtenir un résultat de la forme suivante :

    
    
    Date   : Mon Nov 03 17:19:08 CET 2008
    Number : 100
     
    Date   : Thu Jan 01 01:00:00 CET 1970
    Number : -100
  2. Existe-t-il dans l’API standard des classes qui ne posent pas ce genre de problème lorsqu’elles sont utilisées comme attribut ?
  3. Comment corriger la classe MyObject pour corriger ce problème ?

Alors ? Avez-vous trouvé l’origine du problème ?


Note 1 : Il faut bien sûr appeler la méthode hello() de la classe MyObject !
Note 2 : La date du premier janvier 1970 correspond tout simplement à un new Date(0L)
Note 3 : La solution pour la date est la plus évidente, mais dans les deux cas la solution est très proche.

14 réflexions au sujet de « Exercice Java : POO, encapsulation et immuabilité »

  1. Avatar de benwitbenwit

    En fait, il manque juste entre les deux exemples un texte « on devrait avoir celui-ci » qui ferait le pendant à « Par exemple au lieu d’avoir ceci » car suite à ta réponse, j’ai relu le début et c’est super limpide.

  2. adiguba Auteur de l’article

    benwit > Non tu as raison : la seconde méthode correspond au code correct puisqu’on n’accède qu’une seule fois aux données. C’est ce que je voulais dire je me suis mal exprimé ;)

    Sinon pour de nouveaux jeux il faudrait que j’y penses, mais je recommence petit à petit à blogguer un peu ;)

    a++

  3. Avatar de benwitbenwit

    A propos de ton dernier post dans ce billet,

    Dans le premier exemple, d’accord puisque tu appelles deux fois de suite la méthode du Number dont tu ne connais pas l’implémentation (RandomNumber par exemple)

    Mais dans le deuxième, tu n’appelles qu’une fois la méthode du Number dont tu ne connais pas l’implémentation donc ça ne devrait pas causer de soucis ?
    Si dans cette deuxième méthode, tu avais enlèver le « this » dans le test, je suis de nouveau d’accord, ça peut poser problème.

    Quelque chose m’aurait échappé ?

    A quand les nouveaux jeux ? ;o)

  4. adiguba Auteur de l’article

    Pour compléter la réponse de Hikage : lorsqu’on récupère un type non-immuable, on doit d’abord faire une copie AVANT de vérifier la valeur, puis de vérifier la valeur de cette copie.

    Par exemple au lieu d’avoir ceci :

    &nbsp;<br />
    &nbsp;   if (number.intValue() &lt; 0) {  &nbsp;<br />
    &nbsp;     throw new IllegalArgumentException("'number' doit correspondre à un nombre positif !");  &nbsp;<br />
    &nbsp;   }  &nbsp;<br />
    &nbsp;   this.number = new Integer(number.intValue());&nbsp;<br />

    on devrait avoir celui-ci :

    &nbsp;<br />
    &nbsp;   this.number = new Integer(number.intValue());&nbsp;<br />
    &nbsp;   if (this.number.intValue() &lt; 0) {  &nbsp;<br />
    &nbsp;     throw new IllegalArgumentException("'number' doit correspondre à un nombre positif !");  &nbsp;<br />
    &nbsp;   }&nbsp;<br />

    Car pour une classe muable, rien ne garantie que deux appels successif de la même méthode renverront bien le même résultat.

    En effet on pourrait avoir par exemple une implémentation comme celle-ci :

    &nbsp;<br />
    class RandomNumber extends Number {&nbsp;<br />
    &nbsp;&nbsp;@Override&nbsp;<br />
    &nbsp;&nbsp;public int intValue() {&nbsp;<br />
    &nbsp;&nbsp;&nbsp;&nbsp;// Nombre aléatoire entre -5000 et 5000&nbsp;<br />
    &nbsp;&nbsp;&nbsp;&nbsp;return (int) ( (Math.random() * 10000) - 5000 );&nbsp;<br />
    &nbsp;&nbsp;}&nbsp;<br />
    &nbsp;&nbsp;<br />
    &nbsp;&nbsp;// ...&nbsp;<br />
    }

    En clair si on veut protéger ses attributs, on ne devrait jamais faire confiance aux classes muables ;)

    a++

  5. Avatar de lunatixlunatix

    c’est sur que String immutable, ça aide bien ;)
    a chaque fois que je passe findbugs sur un projet, j’ai le dilemme…

    mais effectivement, je suis d’accord avec toi djo.mos, si je devais designer une api, je serai plus attentif a ca.

  6. adiguba Auteur de l’article

    Lunatix > Là est toute la question ;)

    S’il y a peu de bugs de ce genre, c’est surtout pour deux raisons :

    • Les types primitifs ne sont pas concerné par ce problème.
    • La plupart des classes conteneurs de base sont immuable, et n’ont donc pas besoin de copie de protection.

    Si la classe String n’était pas immuable, on rencontrerait ce genre de problème plus souvent ;)

    a++

  7. Avatar de djo.mosdjo.mos

    lunatix> pareil pour moi.
    Mais je crois que ça ne s’applique pas à ceux qui développent des applications prêts à porter si on peut dire, plutôt aux développeurs d’APIs qui seront utilisés par d’autres programmeurs …

  8. Avatar de lunatixlunatix

    et au fait ? vous pratiquez la protection via copie des attributs mutables dans vos projets ? j’avoue que j’hésite souvent, j’ai finalement rarement vu de bugs provoqués a cause d’une modification intempestive de variable.

  9. adiguba Auteur de l’article

    1. Oui c’est tout à fait cela. Dès lors qu’on utilise un type non-immuable comme attribut, on doit s’attendre à ce qu’il puisse être modifié depuis l’extérieur de la classe.

    • Date est mutable via sa méthode setTime().
    • Number n’est pas directement mutable (aucun accesseur), mais comme elle peut être dérivée et elle n’est donc pas immuable. Ses classes filles peuvent très bien changer le comportement de ses méthodes, et c’est le cas plus précisément de la classe AtomicInteger

    Le code de la méthode exercice() pourrait donc ressembler à cela :

    &nbsp;&nbsp;  public static void exercice() { &nbsp;<br />
    &nbsp;&nbsp;    Date date = new Date(); // Date actuelle &nbsp;<br />
    &nbsp;&nbsp;    AtomicInteger number = new AtomicInteger(100); // 100 &nbsp;<br />
    &nbsp;&nbsp; &nbsp;<br />
    &nbsp;&nbsp;    MyObject obj = new MyObject(date, number); &nbsp;<br />
    &nbsp;&nbsp; &nbsp;<br />
    &nbsp;&nbsp;    obj.hello();&nbsp;<br />
    &nbsp;&nbsp;    &nbsp;<br />
    &nbsp;&nbsp;    date.setTime(0L); // 1er janvier 1970&nbsp;<br />
    &nbsp;&nbsp;    number.set(-100);&nbsp;<br />
    &nbsp;&nbsp;    &nbsp;<br />
    &nbsp;&nbsp;    obj.hello(); &nbsp;<br />
    &nbsp;&nbsp;  }

    2. Plus précisément que les classes filles de Number, il faut utiliser des types immuables, c’est à dire dont la valeur ne varie jamais au cours de l’exécution. Par exemple : Integer, Double, String,…

    Pour rappel la valeur d’un type immuable est déterminé lors de sa création, et ne peut pas être modifié par la suite…

    3. C’est presque cela… puisqu’avec ce code il est encore possible de passer outre les vérifications du constructeur (c’est un peu tordu mais c’est possible).

    a++

  10. Avatar de HikageHikage

    1. Concernant la Date, cette classe n’est pas immuable et peut etre modifier via setTime.
    Concernant le champ Number, il suffit de sous classer la classe Number pour en faire une classe non immuable. Il sera donc possible de modifier le comportement de la méthode intValue();

    2. Si l’on utilise les classes filles de Number, il n’y a pas ce problème. Celle-ci sont finales et immuables. Donc impossible de changer le comportement de celles-ci sans utiliser l’API Reflection.

    3. Concernant la manière de corriger MyObject, il suffira de ne pas stocké directement la référence passée dans le constructeur mais de faire une copie :

    
    
    &nbsp;<br />
    &nbsp; public MyObject(Date date, Number number) { &nbsp;<br />
    &nbsp;   // On vérifie que la date soit plus récente que le 1 janvier 2000 : &nbsp;<br />
    &nbsp;   Calendar cal = Calendar.getInstance(); &nbsp;<br />
    &nbsp;   cal.set(2000, java.util.Calendar.JANUARY, 1); &nbsp;<br />
    &nbsp;   if (cal.before(date)) { &nbsp;<br />
    &nbsp;     throw new IllegalArgumentException("'date' doit être supérieur à l'an 2000"); &nbsp;<br />
    &nbsp;   } &nbsp;<br />
    &nbsp;   this.date = new Date(date.getTime());&nbsp;<br />
    &nbsp;   &nbsp;<br />
    &nbsp;   // On vérifie que l'objet Number soit bien un nombre positif &nbsp;<br />
    &nbsp;   if (number.intValue() &lt; 0) { &nbsp;<br />
    &nbsp;     throw new IllegalArgumentException("'number' doit correspondre à un nombre positif !"); &nbsp;<br />
    &nbsp;   } &nbsp;<br />
    &nbsp;   this.number = createNumberCopy(number);&nbsp;<br />
    &nbsp; } &nbsp;<br />
    &nbsp;<br />
    &nbsp;<br />

Laisser un commentaire