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
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, ...
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.
Une classe : c'est le concept le plus important à comprendre. Une classe représente un type de données que vous allez créer. Souvent, ce nouveau type est composé par agrégation d'éléments basés sur de types plus simples : ces éléments sont appelés attributs, nous allons y revenir. Imaginons que vous ayez besoin, pour un nouveau programme, de manipuler des nombres rationnels (des fractions) : comme ce type n'est pas, de base, existant en Java, il nous faut le créer. Ces nombres rationnels seront tous constitués de deux éléments : un numérateur (un entier) et un dénominateur (un autre entier). On appelle ces deux éléments des attributs, nous allons y revenir.
Rational
qui représentera
ce concept de nombre rationnel.
Un objet : on dit qu'un objet est une instance
d'une classe. Par exemple, on peut imaginer dans votre programme que vous
cherchiez à ajouter deux nombres rationnels. Le premier sera stocké dans une variable nommée r1
(on imagine qu'il contienne la valeur
1/3) et le second dans une variable nommée r2
(initialisée à l'état 2/1). Le résultat sera stocké dans une troisième variable nommée
result
. Ces trois variables seront basées sur le type Rational
. Chaque contenu de ces trois variables est donc un objet,
un exemplaire de la classe Rational
.
Un attribut : une donnée membre de la classe. Dit autrement, un attribut est un champ définissant une partie de la structure interne de la
classe. Par exemple, un numérateur est une des deux parties d'un nombre rationnel : on peut donc définir un attribut numerator
à
l'intérieur de la classe Rational
. Chaque attribut permettra de stocker une partie de l'état d'un objet. Par exemple, si l'on considère
la fraction r1
évoquée ci-dessus, l'attribut numerator
sera fixé à 1 et l'attribut denominator
sera
lui fixé à 3. Si l'on considère la fraction r2
, les valeurs seront différentes pour chaque attribut : 2 pour le numérateur et 1 pour
le dénominateur.
Une méthode : une fonction membre de la classe. Les méthodes permettent de déclencher des traitements spécifiques à la classe sur un objet
donné. Lors de l'exécution de la méthode, elle pourra manipuler les valeurs des attributs de l'instance. Par exemple, imaginons une variable
theRational
qui contient un objet de type Rational
initialisé à 4/8. On est d'accord, cette fraction peut être simplifiée
(ce qui devrait calculer 1/2). Cette opération est spécifique à la notion de fraction : on va donc définir une méthode de simplification,
pourquoi pas appelée simplify
, dans la classe Rational
. On pourra ainsi imaginer l'exemple ci-dessous.
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] } } |
Rational
.
Un constructeur : une méthode particulière que déclenche automatiquement lors de l'instanciation (de la création) d'un objet. Une classe peut posséder plusieurs constructeurs, mais un objet donné n'aura pu être produit que par un seul constructeur.
Une propriété : une paire de méthodes (appelés getter et setter) permettant l'accès à un attribut de manière sécurisée. On peut n'avoir qu'une
seule méthode pour une propriété : on parle alors de propriété « read-only » ou « write-only ». Notez aussi qu'une propriété peut
être associée, dans certaines situations subtiles, à plusieurs attributs : on parle alors de propriété calculée. Imaginez une classe Point
représentant une cordonnée exprimée dans un système cartésien (coordonnées x et y). On peut donc considérer deux propriétés appelées
x
et y
. Mais on peut aussi envisager deux propriétés calculées permettant de manipuler la position en coordonnées polaires :
dans ce cas, les propriétés length
et angle
se calculeront à partir des attributs x
et y
.
L'encapsulation : c'est un principe de développement de classe qui consiste à protéger l'état des attributs et à imposer de passer par des méthodes pour modifier les valeurs des attributs. C'est ce principe que je tiens à vous faire comprendre dans ce document. Ce principe est très fortement mis en oeuvre dans l'ensemble des librairies proposées par le Java SE et le Java EE.
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.
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; } |
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 ); } } |
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 ); } } |
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 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; } |
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.
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).
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; } } |
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.
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; } } |
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 ) ); } } |
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é.
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 ); } } |
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) $>
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é.
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] } } |
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.
Un constructeur ne doit pas spécifier de valeurs de retour. Même pas void
.
Un constructeur doit avoir, en Java, le même nom que la classe. Et ceux aux minuscules, majuscules prêt.
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; } } |
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] $>
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; } } |
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() ); } } |
Et voici les résultats produit par notre main
.
$> java fr.koor.poo.Start [0/1] [2/1] [1/3] $>
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; } } |
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; } } |
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() ); } } |
Et voici les résultats produit par notre main
.
$> java fr.koor.poo.Start [0/1] [2/1] [1/3] [35/10] $>
Il y a des règles subtiles à maîtriser au sujet des constructeurs quand on fait de la programmation orientée objet. Les voici.
Si vous ne spécifiez aucun constructeur dans votre classe, le compilateur en produit un qui n'accepte aucun paramètre et qui initialise chaque attribut
à la valeur neutre du type de données qui lui est associé. C'est pour cela qu'au début de ce chapitre nous avons réussi à compiler
Rational r = new Rational()
alors que nous n'avions pas encore introduit le concept de constructeur.
C'est aussi pour cela que nos premiers nombre rationnels étaient initialisés à 0/0.
Si vous codez un constructeur dans votre classe, le constructeur produit par défaut, ne le sera plus. Il peut donc devenir nécessaire de fournir le constructeur à zéro paramètre.
Un constructeur commence toujours par invoquer un constructeur de la classe parente (en lien avec le mot clé super
).
Mais comme nous n'avons pas encore vraiment parler d'héritage, je mets ce point de côté et nous en reparlerons dans le chapitre suivant qui sera
dédié à l'héritage.
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.
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}
).
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 ); } } |
@Override
, car encore un fois elle est liée à l'héritage, concept que nous étudierons
dans le prochain chapitre.
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 ); } } |
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.
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.
Première solution, placez-vous dans votre classe à l'endroit où vous souhaitez générer votre toString
. Commencez à taper toS
puis enclenchez la séquence de touches CTRL+ESPACE
. Normalement Eclipse devrait vous proposer de compléter votre méthode. Dans ce cas,
un appel au toString
parent (celui de la classe mère dont vous héritez) devrait être produit. Nous comprendrons mieux cet appel au
toString
parent quand nous aurons étudié le concept d'héritage.
Seconde solution, placez-vous dans votre classe à l'endroit où vous souhaitez générer votre toString
. Cliquez avec le bouton droit de la
souris, sélectionnez le sous-menu « Source » puis sélectionnez-y l'assistant « Generate toString... ». Cette seconde solution
vous propose de sélectionner les attributs que vous souhaitez voir être présent dans la chaîne de caractères produite.
Voici une capture d'écran montrant ce choix au niveau des attributs à afficher.
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.
La première s'appellera add
et permettra d'ajouter deux nombres rationnels. Il faut comprendre qu'une méthode s'invoque sur une
instance (un objet). Du coup, si on invoque add
sur une instance r1
, il manquera un deuxième rationnel pour réaliser
l'opération d'addition : on passera ce deuxième rationnel en paramètre de la méthode. Elle devra, de plus, renvoyer un nouveau rationnel qui sera
la somme des deux autres. Pour rappel, a/b + c/d == (ad + bc) / db.
La seconde méthode s'appellera eq
(pour equal). Elle permettra de comparer deux rationnels. Pour rappel, plutôt que de chercher à
comparer des valeurs flottantes (on peut avoir des soucis de précision), il est préférable de tenir compte de cette équivalence :
si a/b == c/d alors ad == bc. C'est donc la dernière égalité que je vais implémenter : elle me permettra de comparer des entiers et non des
flottants.
La dernière s'appellera simplify
et elle permettra de réduire la fraction sur laquelle la méthode sera invoquée.
Cette méthode n'aura donc pas de valeur de retour. Pour ce qui est de l'algorithme de simplification de la fraction, je vais l'implémenter via
l'algorithme d'Euclide qui permettra de trouver le PGCD
(Plus Grand Diviseur Commun).
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 ); } } |
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 :-)" ); } } |
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.
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.
identifier : un entier positif qui pourra être utilisé comme clé primaire en cas de persistance en base de données.
firstName : une chaîne de caractères représentant le prénom de la personne. Cet attribut de devra en aucun cas être nul. Il ne pourra pas contenir une chaîne vide. Enfin, un prénom devra être systématiquement en minuscules.
lastName : une chaîne de caractères représentant le nom de famille de la personne. Cet attribut de devra en aucun cas être nul. Il ne pourra pas, non plus, contenir une chaîne vide. Enfin, un nom devra être systématiquement en majuscules.
email : une chaîne de caractère contenant l'adresse email de la personne. Cette adresse électronique devra respecter le format classique d'un email. Elle pourra en aucun cas être nulle.
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 ;-)
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 ); } } |
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 ); } } |
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) $>
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 :