Participer au site avec un Tip
Rechercher
 

Améliorations / Corrections

Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.

Emplacement :

Description des améliorations :

Introduction à la POO et principe d'encapsulation

Outil de vérification d'expressions régulières Mise en oeuvre du concept d'héritage


Accès rapide :
La vidéo
Un peu de vocabulaire
Mise en oeuvre de notre première classe
L'encapsulation
Le concept de constructeur
La surcharge de constructeurs
La délégation de constructeurs
Quelques règles à connaître au sujet des constructeurs
Assistance à la génération de constructeurs proposée par Eclipse
Afficher un objet Java
La méthode toString
Appel de la méthode toString
Assistance Eclipse à la production de la méthode toString
Quelques autres méthodes spécifiques à notre classe Rational
Travaux pratiques
Le sujet
La correction

La vidéo

Cette vidéo vous présente différents principes de programmation orientée objet. Parmi ces principes nous traiterons de l'encapsulation, de la notion de propriétés (getter/setter), de constructeurs, ...


Introduction à la POO et principe d'encapsulation

Un peu de vocabulaire

Avant de commencer à coder, j'aimerai poser quelques points de terminologie en lien avec la programmation orientée objet. Cela nous permettra de bien nous comprendre et surtout d'utiliser le vocabulaire adapté. Ceux d'entre vous qui connaissent déjà les concepts de programmation orientée objet dans un autre langage peuvent directement passer à la section suivante. Voici donc les principaux points de vocabulaire que je souhaite introduire.

si je choisis de vous parler de nombres rationnels, ce n'est pas juste comme çà. En effet, ce type est souvent choisit pour introduire l'objet et plus précisément le principe d'encapsulation : c'est un cas d'école. La raison en est simple, toutes les combinaisons d'entiers (pour le numérateur et le dénominateur) ne sont pas autorisées. La division par zéro est mathématiquement interdite. Comment garantir que nous n'arriverons pas à produire de tels rationnels ? C'est tout l'objectif de ce chapitre ou, étape par étape, je vais vous faire coder une classe Rational qui représentera ce concept de nombre rationnel.
les termes d'instance et d'objet, sont des synonymes est vous pouvez utiliser l'un à la place de l'autre sans risque.
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
public class Demo {

    public static void main( String [] args ) {
    
        Rational theRational = new Rational( 4, 8 );    // On créé l'objet
        theRational.simplify();                         // On le simplifie
        System.out.println( theRational );              // On l'affiche => [1/2]
    
    }

}
Exemple d'utilisation d'une méthode sur une classe de manipulation de nombres rationnels
ne cherchez pas encore à démarrer ce programme, car nous n'avons pas encore codé la classe Rational.
bien que définie sur la classe, une méthode s'invoque sur une instance (un objet), contrairement à une méthode statique (concept déjà étudié précédemment) qui elle s'invoque directement à partir de la classe.
pour de nombreux développeurs, même expérimentés, les concepts d'attributs et de propriétés sont équivalents. C'est une erreur de penser çà ! Le problème du langage Java est qu'on utilise plus volontiers la terminologie de « getter/setter » en lieu et place de celle de propriétés, du coup l'erreur est plus facile.
certains autres langages de programmation, je pense notamment à C# ou Python, proposent une syntaxe spécifique pour les propriétés. En Java, on utilise simplement des conventions de nommages (cette convention est appelée Java Beans) sur les méthodes associées aux propriétés (et donc les getters et les setters). Mais cela pourrait peut-être changer sur les futures versions de Java : il y a des demandes d'évolution en ce sens.

Ces quelques points de terminologie étant posés, il est maintenant temps de passer à une mise en pratique. Comme indiqué ci-dessus, nous allons utiliser le concept de fractions dans notre mise en oeuvre.

Mise en oeuvre de notre première classe

Il est donc temps de commencer à coder notre classe Rational. Il est important de comprendre que je vais travailler étape par étape. Les premières versions de notre classe seront donc très loin d'être parfaites. A chaque étape, nous discuterons des améliorations effectuées.

Donc notre classe Rational est constituée de deux entiers : le numerator et le denominator. Dans un premier temps, je vais déclarer ces deux attributs comme étant publiques. Il est important de noter la classe Rational comme étant publique, elle aussi. En effet, si vous souhaitez utiliser cette classe en dehors du fichier Rational.java, ce qui va être notre cas, il est impératif d'ajouter ce mot clé.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
package fr.koor.poo;

public class Rational {

    public int numerator;
    public int denominator;

}
Une classe très simpliste
si on s'arrête à ce niveau, la classe produite est similaire à une structure, concept présent dans le langage C. On est donc encore loin de pouvoir prétendre faire de la programmation objet.
pour produire cette classe, vous pouvez utiliser l'assistant « New class » proposé par Eclipse. Je vous conseille d'utiliser la notion de package en sachant que nous reviendrons dans un prochain chapitre sur ce sujet.

On souhaite maintenant tester cette classe. Pour ce faire, on ajoute une seconde classe qui ne contiendra que notre main, le point d'entrée de notre programme. Voici un exemple d'utilisation de la classe précédemment définie.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
package fr.koor.poo;

public class Start {

    public static void main( String [] args ) {
    
        Rational r = new Rational();
        r.numerator = 1;
        r.denominator = 3;
        
        // Affiche [1/3]
        System.out.printf( "[%d/%d]\n", r.numerator, r.denominator );
    
    }

}
Utilisation de notre classe de fraction
lors de la déclaration de la variable r, il est important de l'initialiser avec la séquence new Rational(). En l'absence de cette valeur d'initialisation (par exemple Rational r;), r sera initialisé à null. Du coup, vous n'aurez pas d'objet et une erreur sera produite à l'exécution du programme.

Le problème avec notre classe, en l'état, c'est qu'elle permet la modification des attributs sans qu'aucun test ne soit réalisé. On peut donc aussi lancer ce main.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
package fr.koor.poo;

public class Start {

    public static void main( String [] args ) {
    
        Rational r = new Rational();
        r.numerator = 1;
        r.denominator = 0;
        
        // Do something
        
        double result = r.numerator / r.denominator;
        System.out.printf( "[%d/%d] => %d\n", r.numerator, r.denominator, result );
    
    }

}
Utilisation de notre classe de fraction

Démarrons ce programme !

$> java fr.koor.poo.Start
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at fr.koor.poo.Start.main(Start.java:13)
$> 

Comprenez que ce code est très problématique. Ce qui pose problème, ce n'est pas tant que le développeur puisse faire n'importe quoi (ici, r.denominator = 0; en ligne 9), mais plutôt le fait qu'on détecte le problème bien plus tard (lors de la division par 0 en ligne 13).

Effectivement, les discussions qui vont suivre n'empêcheront pas un développeur de faire des bêtises. Mais ce qu'il faut, c'est tout mettre en oeuvre afin de détecter les incohérences le plus tôt possible. Imaginez que le commentaire en ligne 11 corresponde à une très grosse section de code équivalente à plusieurs secondes d'exécution : si on détecte un problème plusieurs secondes après, il sera très difficile de trouver l'origine du problème et cela pourra vous prendre plusieurs heures de debug. Plus on détecte une incohérence tôt, moins cela nous coûtera cher en termes de temps de debug.

L'encapsulation

L'encapsulation consiste à cacher l'état interne d'un objet et d'imposer de passer par des méthodes permettant un accès sécurisé à l'état de l'objet. Pour mettre en oeuvre l'encapsulation, la première étape consiste à privatiser les attributs. Pour ce faire, un mot clé spécial vous est proposé : private.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
package fr.koor.poo;

public class Rational {

    private int numerator;
    private int denominator;

}
Privatisation des attributs

Ensuite, il faut fournir les méthodes d'accès sécurisées : ce qu'on appelle généralement de propriétés en programmation orientée objet et assez souvent getters/setters en Java. Un getter permet l'accès en lecture à un attribut alors qu'un setter permet de demander un changement d'état. Ces méthodes sont très conventionnées en Java, à tel point que des outils de génération sont proposés dans notre IDE favori (pour nous, Eclipse).

Pour profiter de ces générateurs, commencer par définir les attributs privés de la classe, puis placez-vous à l'endroit souhaité pour produire les getter/setter, cliquez avec le bouton droit de la souris, sélectionnez le sous-menu « Sources » puis « Generate Getters/Setters... ». La capture d'écran suivante vous montre le menu en question.

Génération des getters et des setters

Une boîte de dialogue doit apparaître. Cochez-y les attributs pour lesquels vous souhaitez générer un couple getter/setter (en notant bien que vous pourriez ne générer qu'une seule de deux méthodes).

Génération des getters et des setters - suite

Et voici le code produit par cet assistant.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
package fr.koor.poo;

public class Rational {

    private int numerator;
    private int denominator;
    
    
    public int getNumerator() {
        return numerator;
    }
    
    public void setNumerator( int numerator ) {
        this.numerator = numerator;
    }
    
    
    public int getDenominator() {
        return denominator;
    }
    
    public void setDenominator( int denominator ) {
        this.denominator = denominator;
    }
    
}
Génération des getters/setters

Une autre possibilité aurait pu être utilisée pour générer les getter/setter. Quand vous êtes dans l'éditeur Java, si vous commencez à taper getN ou setN, puis si vous appuyez simultanément sur CTRL+ESPACE, l'outil va partir automatiquement à la recherche des attributs commençant par n : s'il en trouve au moins un, il vous suggérera de produire la méthode d'accès associée, comme en atteste la capture d'écran suivante.

Génération des getters et des setters via le CTRL+Espace

L'étape d'après consiste à rajouter les contrôles au niveau des setters. Dans le cas de notre classe Rational, les choses sont simples : seul le dénominateur nécessite une vérification. Je vous propose le code suivant :

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
package fr.koor.poo;

public class Rational {

    private int numerator;
    private int denominator;
    
    
    public int getNumerator() {
        return numerator;
    }
    
    public void setNumerator( int numerator ) {
        this.numerator = numerator;
    }
    
    
    public int getDenominator() {
        return denominator;
    }
    
    public void setDenominator( int denominator ) {
        if ( denominator == 0 ) throw new RuntimeException( "denominator cannot be zero" );
        this.denominator = denominator;
    }
    
}
Ajout des contrôles de validité
l'instruction throw, présente en ligne 23, permet de déclencher une exception (une erreur) de type java.lang.RuntimeException. Nous reviendrons dans un futur chapitre sur la gestion des exceptions et pour l'heure nous accepterons la ligne de code telle quelle.

Peut-être qu'à ce stade vous vous posez une question : comme le numérateur n'est assujetti à aucune contrainte, ne serait-il pas mieux de ne pas le protéger via un couple getter/setter ? C'est une très bonne question et la réponse n'est pas si évidente que çà. En premier lieu vous pourriez penser qui si aucune contrainte n'est à vérifier alors il vaut mieux supprimer les méthodes d'accès afin de gagné en performance (on peut partir de l'idée qu'un appel de méthode n'est pas gratuit). A cette argumentation, je répondrais que, primo la JVM repère ces méthodes d'accès et essaye (sous certaines conditions) de les optimiser en supprimant les appels (procédé d' « inlining » connu des développeurs C++). Mais, secundo, je rajouterai que le point principal ne réside pas dans l'optimisation de votre code.

Effectivement, le point majeur de la réflexion doit tourner sur les aspects de maintenance et d'évolutivité de votre programme. Il est vrai que mathématiquement parlant, la définition d'un rationnel est assez stable dans le temps, mais imaginez un autre type de données quelconque. Il se peut qu'un jour les contraintes ne soient plus les mêmes (les choses changent avec le temps). Si vous avez laissé un attribut public, on peut imaginer 5000 lignes de code qui dans votre programme (du coup, un gros programme) accède directement à cet attribut. Du coup, s'il faut maintenant intégrer une nouvelle contrainte sur l'attribut, il faudra modifier les 5000 lignes de code pour plutôt invoquer le nouveau setter associé : l'évolution va vous prendre beaucoup de temps. Si par contre, on avait anticipé sur les changements dès le début, on aurait imposé la propriété (getter/setter) et tout le code aurait dû être écrit en invoquant ces méthodes. L'intégration de la nouvelle contrainte aurait donc pris beaucoup moins de temps. C'est notamment pour cette raison qu'on vous demande que systématiquement privatiser vos attributs et de proposer des propriétés pour l'accès à ces attributs.

Comme indiqué précédemment, quasiment toute la librairie Java SE et Java EE s'appuie sur le concept. A part les constantes, qui elles sont non modifiables (donc aucun besoin de propriétés pour les constantes), il n'y a quasiment aucun attribut public sur les classes des librairies standards. En tout cas, je vous mets au défi de m'en trouver cinq. Bonne chance. Par contre, la majorité des méthodes des librairies standards commencent par ... get ou set ;-)

Voici un exemple d'utilisation de la classe javax.swing.JButton : elle représente un bouton graphique. On se doute, maintenant, que nous n'avons pas accès aux attributs de cette classe. Par contre, un très grand nombre de propriétés vous sont offertes.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;

import javax.swing.JButton;
            
public class Demo {

    public static void main( String [] args ) {
    
        JButton aButton = new JButton();
        aButton.setText( "Click me" );
        aButton.setForeground( Color.RED );
        aButton.setSize( new Dimension( 140, 30 ) );
        aButton.setLocation( new Point( 10, 10 ) );    
    
    }

}
Quelques propriétés sur un bouton graphique

Et si jamais, vous tentiez d'être plus fort que le compilateur en cherchant à modifier en direct un attribut de la classe JButton, voici le message d'erreur qui vous serait retourné.

errorMessage

Maintenant que cela est clair, reprenons l'exemple précédent de manipulation de nombres rationnels et remplaçons-y les accès aux attributs par des appels de setters. Voici le code modifié.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
package fr.koor.poo;

public class Start {

    public static void main( String [] args ) {
    
        Rational r = new Rational();
        r.setNumerator( 1 );
        r.setDenominator( 0 );
        
        // Do something
        
        double result = r.getNumerator() / r.getDenominator();
        System.out.printf( "[%d/%d] => %d\n", r.getNumerator(), r.getDenominator(), result );
    
    }

}
Utilisation de notre classe de fraction

Si on lance ce test, voici le message d'erreur qui sera produit.

$> java fr.koor.poo.Start
Exception in thread "main" java.lang.RuntimeException: denominator cannot be zero
    at fr.koor.poo.Rational.setDenominator(Rational.java:23)
    at fr.koor.poo.Start.main(Start.java:9)
$> 
ce message d'erreur se lit du bas vers le haut. Donc le main, en ligne 9 a invoqué la méthode setDenominator de la classe Rational qui, en ligne 23, a produit une erreur de type java.lang.RuntimeException avec le message comme quoi un dénominateur nul n'est pas autorisé.

Le concept de constructeur

Notre classe avance bien. Pour autant, il y a encore un problème important qu'il faut qu'on corrige. Effectivement, même si vous affectez des valeurs cohérentes à vos rationnels, que se passe-t-il si l'on accède à l'objet avant de l'avoir initialisé ? Pour en avoir le coeur net, je vous propose de lancer ce main de test.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
package fr.koor.poo;

public class Start {

    public static void main( String [] args ) {
    
        Rational r = new Rational();
        System.out.printf( "[%d/%d]\n", r.getNumerator(), r.getDenominator() );    // ???

        r.setNumerator( 1 );
        r.setDenominator( 3 );
        System.out.printf( "[%d/%d]\n", r.getNumerator(), r.getDenominator() );     // [1/3]
    
    }

}
Utilisation de notre classe de fraction

Regardez bien la ligne 8 : elle cherche à afficher l'état de l'objet r avant qu'on lui affecte l'état 1/3. Voici le résultat produit par ce programme.

$> java fr.koor.poo.Start
[0/0]
[1/3]
$> 

Mince ! Notre objet est dans un état incohérent. Le problème auquel on fait face, c'est qu'on a séparé deux aspects qui n'auraient dû faire qu'un : à savoir l'instanciation (réalisée par le new) et l'initialisation. Cette dernière aurait dû être faite par ... le constructeur. Mais qu'est-ce qu'un constructeur ?

Comme nous l'avons dit en début de ce document, un constructeur est une méthode qui se déclenche automatiquement lors de la construction d'un objet. Malgré son nom, un constructeur ne se doit pas de construire, mais plutôt d'initialiser l'objet. Pour information, dans certains autres langages de programmation orienté objet, le constructeur doit s'appeler init ou encore initialize, voir __init__.

Pour coder un constructeur, vous devez respecter certaines règles.

Voici un exemple de définition de constructeur pour notre classe de manipulation de fractions.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
package fr.koor.poo;

public class Rational {

    private int numerator;
    private int denominator;
    
    // Le constructeur !!! Pas de void et il s'appelle Rational !
    public Rational() {
        this.setNumerator( 0 );
        this.setDenominator( 1 );
    }
    
    
    public int getNumerator() {
        return numerator;
    }
    
    public void setNumerator( int numerator ) {
        this.numerator = numerator;
    }
    
    
    public int getDenominator() {
        return denominator;
    }
    
    public void setDenominator( int denominator ) {
        if ( denominator == 0 ) throw new RuntimeException( "denominator cannot be zero" );
        this.denominator = denominator;
    }
    
}
Notre premier constructeur
le fait que le constructeur rappelle les propriétés (les setters) est une bonne pratique. En cas de changement de spécifications dans notre classe on détectera des incohérences plus facilement. Vous comprendrez mieux ce point dans quelques minutes.

Grâce à notre constructeur, tout nouveau Rational sera forcément initialisé à l'état 0/1 (ce qui est cohérent, du point de vue des mathématiques). L'initialisation est maintenant solidaire de la construction (appelle à l'opérateur new) de vos objets. On peut donc relancer notre classe Start et voici les résultats produits : c'est beaucoup mieux.

$> java fr.koor.poo.Start
[0/1]
[1/3]
$> 

La surcharge de constructeurs

J'ai une bonne nouvelle pour vous ! Une classe peut contenir plusieurs constructeurs. Dit autrement, vous pourrez produire vos rationnels de différentes manières. Cette possibilité, de définir plusieurs méthodes de même nom, mais de signatures différentes s'appelle la surcharge.

Dans l'exemple ci-dessous, la surcharge est utilisée au niveau de mes constructeurs et je vous propose trois manières différentes de créer un nombre rationnel.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
package fr.koor.poo;

public class Rational {

    private int numerator;
    private int denominator;
    
    public Rational() {
        this.setNumerator( 0 );
        this.setDenominator( 1 );
    }
    
    public Rational( int numerator ) {
        this.setNumerator( numerator );
        this.setDenominator( 1 );
    }
    
    public Rational( int numerator, int denominator ) {
        this.setNumerator( numerator );
        this.setDenominator( denominator );
    }
    
    
    public int getNumerator() {
        return numerator;
    }
    
    public void setNumerator( int numerator ) {
        this.numerator = numerator;
    }
    
    
    public int getDenominator() {
        return denominator;
    }
    
    public void setDenominator( int denominator ) {
        if ( denominator == 0 ) throw new RuntimeException( "denominator cannot be zero" );
        this.denominator = denominator;
    }
    
}
Surcharge de constructeurs
Cette fois ci on comprend mieux l'intérêt de passer par les getters/setters au niveau de vos constructeurs. Cela évite aussi de dupliquer les tests sur les données d'entrée de vos constructeurs. On gagne en maintenabilité de vos programmes.

Voici un petit exemple d'utilisation de ces constructeurs : chaque création de rationnel fait appel à un de nos trois constructeurs. Le choix du constructeur à invoquer sera, bien entendu, déterminé en fonction du nombre de paramètres passé au constructeur et de leurs types respectifs.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
package fr.koor.poo;

public class Start {

    public static void main( String [] args ) {
    
        Rational r1 = new Rational();
        System.out.printf( "[%d/%d]\n", r1.getNumerator(), r1.getDenominator() );

        Rational r2 = new Rational( 2 );
        System.out.printf( "[%d/%d]\n", r2.getNumerator(), r2.getDenominator() );

        Rational r3 = new Rational( 1, 3 );
        System.out.printf( "[%d/%d]\n", r3.getNumerator(), r3.getDenominator() );
    
    }

}
Utilisation de nos constructeurs

Et voici les résultats produit par notre main.

$> java fr.koor.poo.Start
[0/1]
[2/1]
[1/3]
$> 

La délégation de constructeurs

Notez que les trois constructeurs actuels de notre classe de manipulation de nombre rationnels font, dans une certaine mesure, un peu la même chose. Quand c'est le cas, vous pouvez aller un cran plus loin et utiliser la délégation de constructeurs. C'est la capacité qu'a un constructeur à repasser la main à un autre constructeur plus générique. La délégation d'obtient en utilisant le mot clé this immédiatement suivi d'une paire de parenthèses et des paramètres a passer à l'autre constructeur. Voici un exemple d'utilisation.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
package fr.koor.poo;

public class Rational {

    private int numerator;
    private int denominator;
    
    public Rational() {
        this( 0, 1 );           // Une délégation de constructeur
    }
    
    public Rational( int numerator ) {
        this( numerator, 1 );   // Une autre
    }
    
    public Rational( int numerator, int denominator ) {
        this.setNumerator( numerator );
        this.setDenominator( denominator );
    }
    
    
    public int getNumerator() {
        return numerator;
    }
    
    public void setNumerator( int numerator ) {
        this.numerator = numerator;
    }
    
    
    public int getDenominator() {
        return denominator;
    }
    
    public void setDenominator( int denominator ) {
        if ( denominator == 0 ) throw new RuntimeException( "denominator cannot be zero" );
        this.denominator = denominator;
    }
    
}
délégation de constructeurs
cette modification n'entraine aucune modification particulière sur notre main qui devrait continuer à fonctionner de même.

Par contre, si deux constructeurs donnés ne partagent pas le même algorithme d'initialisation des instances, alors la délégation ne sera pas le bon choix et seule la surcharge de constructeurs pourra être exploitée. Par exemple, si l'on cherche à produire une fraction à partir d'un flottant, il nous faudra alors un algorithme spécifique. En voici un exemple.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
package fr.koor.poo;

public class Rational {

    // --- Nos deux attributs ---
    private int numerator;
    private int denominator;
    
    // --- Quatre constructeurs (avec surcharges et délégations) ---
    public Rational() {
        this( 0, 1 );           // Une délégation de constructeur
    }
    
    public Rational( int numerator ) {
        this( numerator, 1 );   // Une autre
    }
    
    public Rational( int numerator, int denominator ) {
        this.setNumerator( numerator );
        this.setDenominator( denominator );
    }
    
    public Rational( double value ) {
        this.denominator = 1;
        // On teste s'il y a des chiffres après la virgule
        while( value != (int) value ) {
            // on multiplie chacune des deux parties par 10
            value *= 10;
            this.denominator *= 10;
        }
        // value devient notre numérateur
        this.numerator = (int) value;
    }
    
    // --- Nos deux propriétés (getters/setters) ---
    public int getNumerator() {
        return numerator;
    }
    
    public void setNumerator( int numerator ) {
        this.numerator = numerator;
    }
    
    
    public int getDenominator() {
        return denominator;
    }
    
    public void setDenominator( int denominator ) {
        if ( denominator == 0 ) throw new RuntimeException( "denominator cannot be zero" );
        this.denominator = denominator;
    }
    
}
Surcharge de constructeurs

Voici un exemple d'utilisation de notre dernier constructeur.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
package fr.koor.poo;

public class Start {

    public static void main( String [] args ) {
    
        Rational r1 = new Rational();
        System.out.printf( "[%d/%d]\n", r1.getNumerator(), r1.getDenominator() );

        Rational r2 = new Rational( 2 );
        System.out.printf( "[%d/%d]\n", r2.getNumerator(), r2.getDenominator() );

        Rational r3 = new Rational( 1, 3 );
        System.out.printf( "[%d/%d]\n", r3.getNumerator(), r3.getDenominator() );

        Rational r4 = new Rational( 3.5 );
        System.out.printf( "[%d/%d]\n", r4.getNumerator(), r4.getDenominator() );
    
    }

}
Utilisation de nos constructeurs

Et voici les résultats produit par notre main.

$> java fr.koor.poo.Start
[0/1]
[2/1]
[1/3]
[35/10]
$> 
oui, notre dernier rationnel n'est pas simplifié. Le numérateur et le dénominateur sont tous les deux multiples de 5 : on pourrait donc réduire la fraction. Nous reviendrons sur cette opération un peu plus loin dans ce document.

Quelques règles à connaître au sujet des constructeurs

Il y a des règles subtiles à maîtriser au sujet des constructeurs quand on fait de la programmation orientée objet. Les voici.

Assistance à la génération de constructeurs proposée par Eclipse

Le premier assistant est très simple à obtenir. Si vous êtes localisé dans une classe qui ne contient pas encore de constructeur, dans ce cas placez-vous à l'endroit souhaité pour créer votre constructeur et appuyez simultanément sur CTRL+ESPACE. Normalement, le premier assistant proposé correspond à l'ajout d'un constructeur vide à zéro paramètre. Il ne vous restera plus qu'à coder son corps (entre les deux accolades).

D'autres assistants de génération de constructeurs sont accessibles à partir de menu « Source » (cliquez, dans votre classe Java, avec le bouton droit de la souris pour l'obtenir). A l'intérieur de ce menu, vous devriez y voir les assistants en questions.

Assistance Eclipse à la production de constructeurs

Afficher un objet Java

On continue à bien avancer sur notre classe, mais pour autant les codes précédents ne sont pas encore pleinement satisfaisants. Effectivement, la manière donc nous avons affiché nos rationnels n'est pas très conventionnelles. Ce qui gêne c'est que chacun des quatre affichages re-spécifie le format d'affichage de nos valeurs : il se peut donc que, dans un gros programme, certains affichages soient fait d'une manière (pourquoi pas [n/d]) et que d'autres affichages soient réalisés d'une autre manière (pourquoi pas {n,d}).

La méthode toString

Pour plus de cohérence, il pourrait être intéressant de fournir directement dans la classe Rational une méthode de génération de la chaîne de caractères associée à l'objet. Ainsi, il ne resterait plus qu'à afficher la chaîne produite pour chaque rationnel et tous les affichages seraient donc similaires. C'est tellement une bonne idée, que la manière d'atteindre cet objectif est standardisée en Java : on parle de la méthode toString.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
package fr.koor.poo;

public class Rational {

    // --- Nos deux attributs ---
    private int numerator;
    private int denominator;
    
    // --- Quatre constructeurs (avec surcharges et délégations) ---
    public Rational() {
        this( 0, 1 );           // Une délégation de constructeur
    }
    
    public Rational( int numerator ) {
        this( numerator, 1 );   // Une autre
    }
    
    public Rational( int numerator, int denominator ) {
        this.setNumerator( numerator );
        this.setDenominator( denominator );
    }
    
    public Rational( double value ) {
        this.denominator = 1;
        // On teste s'il y a des chiffres après la virgule
        while( value != (int) value ) {
            // on multiplie chacune des deux parties par 10
            value *= 10;
            this.denominator *= 10;
        }
        // value devient notre numérateur
        this.numerator = (int) value;
    }
    
    // --- Nos deux propriétés (getters/setters) ---
    public int getNumerator() {
        return numerator;
    }
    
    public void setNumerator( int numerator ) {
        this.numerator = numerator;
    }
    
    
    public int getDenominator() {
        return denominator;
    }
    
    public void setDenominator( int denominator ) {
        if ( denominator == 0 ) throw new RuntimeException( "denominator cannot be zero" );
        this.denominator = denominator;
    }
    
    // --- Quelques méthodes de la classe ---

    @Override 
    public String toString() {
        return String.format( "[%d/%d]", this.numerator, this.denominator );
    }
    
}
Définition de la méthode toString
nous reviendrons ultérieurement sur l'annotation @Override, car encore un fois elle est liée à l'héritage, concept que nous étudierons dans le prochain chapitre.

Appel de la méthode toString

A savoir, si vous faites directement un System.out.println( r1 ) (et si r1 est un objet de type Rational), alors la méthode toString sera automatiquement invoquée. C'est pratique. Voici quelques manières de produire une chaîne de caractères de représentation de vos rationnels.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
package fr.koor.poo;

public class Start {

    public static void main( String [] args ) {
    
        Rational r1 = new Rational();
        System.out.printf( r1 );

        Rational r2 = new Rational( 2 );
        System.out.printf( r2 );

        Rational r3 = new Rational( 1, 3 );
        System.out.printf( r3.toString() );

        Rational r4 = new Rational( 3.5 );
        String message = "r4 == " + r4;
        System.out.printf( message );
    
    }

}
Utilisation de notre méthode toString

En ligne 8 et 11, nous avons deux appels implicites à la méthode toString. Vous n'avez rien de plus à faire pour obtenir l'appel.

En ligne 14, nous avons un appel explicite à notre méthode toString. Elle est invoquée sur l'instance r3 : elle s'appliquera sur les attributs de cet objet.

En ligne 17, nous avons de nouveau un appel implicite. L'opérateur + cherche à concaténer une chaîne de caractère avec un rationnel et il ne sait pas faire cela. Pour s'en sortir, il invoque automatiquement le toString, ce qui lui permettra de concaténer deux chaînes de caractères et çà, il sait le faire.

Assistance Eclipse à la production de la méthode toString

Ce besoin, de produire une chaîne de caractères de représentation de vos objets, est quasiment systématique à tel point, que même Eclipse propose des assistants de production de la méthode toString. Vous avez deux manières de demander une assistance.

Assistant de génération de la méthode toString

Quelques autres méthodes spécifiques à notre classe Rational

Jusqu'à présent, nous avons vu des éléments assez classiques et qui seront présents dans quasiment toutes vos classes. Mais, bien entendu, une classe doit aussi posséder des méthodes (des traitements) qui lui sont spécifiques. En guise de démonstration, je vous propose de rajouter trois méthodes pour vos rationnels.

Voici le code de la classe Rational intégrant ces nouvelles méthodes.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
package fr.koor.poo;

public class Rational {

    // --- Nos deux attributs ---
    private int numerator;
    private int denominator;
    
    // --- Quatre constructeurs (avec surcharges et délégations) ---
    public Rational() {
        this( 0, 1 );           // Une délégation de constructeur
    }
    
    public Rational( int numerator ) {
        this( numerator, 1 );   // Une autre
    }
    
    public Rational( int numerator, int denominator ) {
        this.setNumerator( numerator );
        this.setDenominator( denominator );
        this.simplify();
    }
    
    public Rational( double value ) {
        this.denominator = 1;
        // On teste s'il y a des chiffres après la virgule
        while( value != (int) value ) {
            // on multiplie chacune des deux parties par 10
            value *= 10;
            this.denominator *= 10;
        }
        // value devient notre numérateur
        this.numerator = (int) value;
        this.simplify();
    }
    
    // --- Nos deux propriétés (getters/setters) ---
    public int getNumerator() {
        return numerator;
    }
    
    public void setNumerator( int numerator ) {
        this.numerator = numerator;
    }
    
    
    public int getDenominator() {
        return denominator;
    }
    
    public void setDenominator( int denominator ) {
        if ( denominator == 0 ) throw new RuntimeException( "denominator cannot be zero" );
        this.denominator = denominator;
    }
    
    // Quelques méthodes de la classe
    
    public Rational add( Rational r2 ) {
        int numerator = this.numerator * r2.denominator + this.denominator * r2.numerator;
        int denominator = this.denominator * r2.denominator;
        return new Rational( numerator, denominator );
    }
    
    public boolean eq( Rational r2 ) {
        return this.numerator * r2.denominator == this.denominator * r2.numerator;
    }
    
    // Pour les détails sur l'algorithme d'Euclide pour le calcul du PGCD
    // https://fr.wikipedia.org/wiki/Algorithme_d%27Euclide#Description_de_l'algorithme
    public void simplify() {
        int a;
        int b;
        
        if ( this.numerator > this.denominator ) {
            a = this.numerator;
            b = this.denominator;
        } else {
            a = this.denominator;
            b = this.numerator;
        }
        
        int rest;
        while( (rest = a % b) != 0 ) {
            a = b;
            b = rest;
        }
        
        this.numerator /= b;
        this.denominator /= b;
    }    
    
    
    @Override 
    public String toString() {
        return String.format( "[%d/%d]", this.numerator, this.denominator );
    }
    
}
Notre classe Rational
Remarquez bien qu'en lignes 21 et 34, je force un appel à la méthode simplify afin d'être toujours certains d'avoir la fraction la plus simplifiée possible lors de la construction d'un objet. Comme la méthode add finit par produire une nouvelle instance de Rational comme résultat de la méthode, je suis donc certain que la fraction est simplifiée.

Et voici un programme de test permettant de vérifier le bon fonctionnement de nos méthodes.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
package fr.koor.poo;

public class Start {

    public static void main( String [] args ) {
        
        Rational r1 = new Rational( 1, 3 );
        Rational r2 = new Rational( 2, 1 );
        Rational result = r1.add( r2 );
        System.out.println( result );
        
        if ( ! result.eq( new Rational( 7, 3 ) ) ) {
            throw new RuntimeException( 
                    "Certainnement un pb d'addition ou de comparaison" );
        }
    
        Rational r3 = new Rational( 3.5 );
        System.out.println( r3 );
        if ( ! r3.eq( new Rational( 7, 2 ) ) ) {
            throw new RuntimeException( 
                    "Certainnement un pb de simplification" );
        }
     
        System.out.println( "Tout semble OK :-)" );
    }

}
Tests de notre classe Rational

En guise d'exercice et en vous basant sur ce que je viens de vous montrer, je vous propose de coder quelques méthodes complémentaires. D'abord, les opérations arithmétiques manquantes : sub, mult et div. Puis les opérations de comparaisons manquantes : ne (not equal), lt (less than), le (less or equal), gt (greater than) et ge (greater or equal). Cela devrait être assez rapide.

Travaux pratiques

Le sujet

Je vous demande de me coder une nouvelle classe. Cette classe, que nous allons appeler Person, permettra de représenter la notion de ... et oui, de personne. Du coup, une personne sera constituée des attributs suivants : notez que pour chaque attribut, je vous donne un ensemble de contraintes/tests à vérifier. En cas de violation des contraintes vous devrez produire des exceptions.

Vous devez fournir dans votre classe les propriétés attendues, des constructeurs et une méthode toString. Si vous avez d'autres idées de méthodes, n'hésitez surtout pas. Et n'oubliez pas d'utiliser votre IDE pour générer un maximum de code.

A vous de jouer et essayez de ne pas regarder la correction trop vite ;-)

La correction

Voici le code de la classe demandée.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
package fr.koor.poo;

import java.util.regex.Pattern;

public class Person {

    // On définie une expression régulière compilé une fois pour toute.
    // Elle est partagée par toutes les instances de Person
    private static final Pattern EMAIL_PATTERN = Pattern.compile( 
            "^[\\w.-]+@[\\w.-]+[a-z]{2,}$" 
    );
    
    private int identifier;
    private String firstName;
    private String lastName;
    private String email;
    
    
    public Person() {
        this( 0, "john", "doe", "unknown@anywhere.unk" );
    }
       
    public Person( int identifier, String firstName, String lastName, String email ) {
        this.setIdentifier( identifier );
        this.setFirstName( firstName );
        this.setLastName( lastName );
        this.setEmail( email );
    }


    public int getIdentifier() {
        return identifier;
    }
    
    public void setIdentifier( int identifier ) {
        if ( identifier < 0 )  {
            throw new RuntimeException( "identifier must be positive" );
        }
        this.identifier = identifier;
    }
    
    public String getFirstName() {
        return firstName;
    }
    
    public void setFirstName( String firstName ) {
        if ( firstName == null ) {
            throw new NullPointerException( "firstName cannot be null" );
        }
        firstName = firstName.trim();   // Pour supprimer les blancs inutiles
        if ( firstName.equals( "" ) ) {
            throw new RuntimeException( "firstName cannot be empty" );
        }
        this.firstName = firstName.toLowerCase();
    }
    
    public String getLastName() {
        return lastName;
    }
    
    public void setLastName( String lastName ) {
        if ( lastName == null ) {
            throw new NullPointerException( "lastName cannot be null" );
        }
        lastName = lastName.trim();   // Pour supprimer les blancs inutiles
        if ( lastName.equals( "" ) ) {
            throw new RuntimeException( "lastName cannot be empty" );
        }
        this.lastName = lastName.toUpperCase();
    }
    
    public String getEmail() {
        return email;
    }
    
    public void setEmail( String email ) {
        if ( email == null ) {
            throw new NullPointerException( "email cannot be null" );
        }
        if ( ! EMAIL_PATTERN.matcher( email ).matches() ) {
            throw new RuntimeException( "email parameter not match with classical pattern" );
        }
        this.email = email;
    }
    
    @Override public String toString() {
        return String.format( "%d: %s %s @ %s",
                this.identifier,
                this.firstName,
                this.lastName,
                this.email );
    }
    
}
La classe Person
si la gestion de l'expression régulière n'est pas claire pour vous, je vous renvoie vers les chapitres dédiés à ce sujet.

Maintenant, voici un exemple d'utilisation de la classe Person.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
package fr.koor.poo;

public class Start {

    public static void main( String [] args ) {
        
        Person john = new Person();
        Person jason = new Person( 1, "Jason", "Bourne", "supremacy@cia.us" );
        Person james = new Person( 7, "James", "Bond", "007@mi6.uk" );
        
        System.out.println( john );
        System.out.println( jason );
        System.out.println( james );
        
        Person nimportnaouak = new Person( 0, "bidon", "kapabonmail", "zut" );
        System.out.println( nimportnaouak );
        
    }

}
Notre classe Start pour faire quelques tests

Et pour finir, voici les résultats produits par les codes précédents.

$> java fr.koor.poo.Start
0: john DOE @ unknown@anywhere.unk
1: jason BOURNE @ supremacy@cia.us
7: james BOND @ 007@mi6.uk
Exception in thread "main" java.lang.RuntimeException: email parameter not match with classical pattern
    at fr.koor.poo.Person.setEmail(Person.java:81)
    at fr.koor.poo.Person.<init>(Person.java:27)
    at fr.koor.poo.Start.main(Start.java:15)
$> 


Outil de vérification d'expressions régulières Mise en oeuvre du concept d'héritage