Java - Chapitre 4
L'objet dans toute sa splendeur
L'objet dans toute sa splendeur
Après le chapitre 3, vous avez vu ce qu'était un objet en soit, la façon dont on le manipule (en général) et ses applications. La seule chose, c'est que vous n'avez pas de quoi en faire un monstre de programmation par ce simple chapitre. Si je m'empresse de l'écrire assez rapidement c'est d'ailleurs pour pouvoir vous donner des applications ultra-concrêtes.
Je vous ai bassiné avec une phrase du genre "Mais ça nous le verrons lorsque nous aurons fait l'héritage", et bien jouez hautbois résonnez musettes, vous allez le voir tout de suite !
:arrow:I. Héritage
En programmation beaucoup de structures sont en arbre. C'est à dire que chaque chose implique plusieurs autres choses qui lui sont similaires. Imaginez qu'un père engendre deux fils, c'est exactement la même chose (héritage ? on va butter le père ?). Nous verrons que les applications sont multiples et tout à fait naturelles.
Traduisons ce concept dans la programmation objet maintenant. Un objet appartient à une classe, jusque là tout va bien. Supposez maintenant que cette classe soit fille d'une classe mère généralisant cette première... mmh ça ne doit surement pas être clair. Prenez la classe "pièce d'une maison", vous aurez les classes filles suivantes : "chambre", "salle à manger", "salle de bain", "chiottes", ... Ce sont toutes des pièces de maison tout d'abord, ensuite elle sont spécialisées. Je peux vous faire une centaine d'exemple comme ça, les DS au bahut, les profs, les magazines, les CDs (je dis tout ce que je trouve autour de moi, y'a des bouteilles de bierres aussi...).
Le tout est un peu flou, voyons cela sur un schéma de principe :
Les petits nenfants en même temps ce sont des membres de la famille, i.e. ils sont reconnus dans la même classe que le père, mais en plus ils savent tout ce que sait le père ET ils ont leur propre connaissance. Ensuite je pense que le schéma parle de lui-même.
Je vais prendre l'exemple des joueurs de foot puisqu'il est très facile à implémenter pour un premier coup. Je définis ma classe Joueur ainsi :
- Code: Tout sélectionner
public class Joueur {
public Terrain terrain;
public int numero;
public int position[] = new int[2]; // position sur le terrain en x,y
public Joueur(Terrain terrain, int numero) {
this.terrain = terrain;
this.numero = numero;
}
// On suppose les Set / Get programmés
...
}
J'ai donc défini de façon légitime un joueur sur un terrain (que je suppose avoir défini de façon très simple (deux dimensions)). Maintenant penchons nous sur les prochaines lignes de code :
- Code: Tout sélectionner
public class Attaquant extends Joueur { // Attaquant est un enfant de joueur
public int equipe;
public Attaquant(Terrain terrain, int numero, int equipe) {
super(terrain,numero);
this.equipe = equipe;
}
public void attaquer() { //Il est un peu crétin il sait qu'aller en avant
setPositionX(terrain.getSurfaceDeReparation()); // On avance !
}
}
Dans un autre fichier on peut avoir :
public class Defenseur extends Joueur {
public int equipe;
public Defenseur(Terrain terrain, int numero, int equipe) {
super(terrain,numero);
this.terrain = terrain;
this.numero = numero;
this.equipe = equipe;
}
public void DefenseHaute() { // Il ne sait pas attaquer lui !
setPositionX(terrain.getLigneDuMilieu()); // On avance, parce que c'est bien !
}
}
Analysons cela de plus près : on définit à la première ligne une nouvelle classe Attaquant dérivée de Joueur (fille quoi). Et oui, un attaquant quoi qu'on en dise, c'est un joueur (je vous parle dans la vrai vie là !). Bien lorsque l'on a fait, Java (pas bête), dit à Attaquant : "Toi tu es peut-être un attaquant, mais tu es avant tout un joueur, DONC tu dois savoir faire tout ce que sait faire un joueur !". Et c'est ainsi que l'attaquant devient un joueur, et sait tout faire, moui en clair il sait qui il est, il sait qu'il a un terrain sous ses pieds, et il sait se déplacer. Pourquoi le mot "extends", car on peut voir (sous un certain angle) qu'un attaquant est une extension d'un joueur, puisqu'il sait faire plus de chose.
L'avantage, c'est que l'on a pas à redéfinir setPosition et compagnie, puisqu'un joueur sait le faire ! On remarque qu'un défenseur ne se déplace pas comme un attaquant, lorsqu'on hurle à un attaquant de faire tout en avant, il va dans la surface de réparation alors que le défenseur s'arrête au milieu du terrain.
Une chose que je n'ai toujours pas précisé, c'est le mot clef super. Il indique au programme de qui l'objet fils descends. Il descend d'un père qui a un numéro et un terrain bien précis. On crée donc les deux objets, l'un étant à l'intérieur de l'autre comme on l'a déjà dit. Attention, super peut-être omis, s'il ne contient aucun paramètre (un super() tout seul c'est triste), et surtout, il doit être placé EN PREMIERE LIGNE dans le constructeur.
Mais maintenant voyons l'entraineur (c'est pas un joueur !) :
- Code: Tout sélectionner
public class Entraineur {
public Joueur equipe[];
public int couleur;
private int nb_joueur = 0;
public final static attaquant = 1;
public final static defenseur = 2;
public final static goal = 3;
public Terrain terrain;
public Entraineur(int joueurs, int couleur) {
equipe = new Joueur[joueurs];
terrain = new Terrain(500,150);
this.couleur = couleur; // L'équipe quoi, Bleu pour les français par exemple :D
}
public void recruter(int type, int numero) {
if(nb_joueur >= equipe.length) { //S'il y a déjà trop de joueur ...
System.out.println("L'équipe est complète !");
return;
}
switch(type) {
case attaquant:
equipe[nb_joueur++] = new Attaquant(terrain, numero, couleur);
break;
case defenseur:
equipe[nb_joueur++] = new Defenseur(terrain, numero, couleur);
break;
case goal:
equipe[nb_joueur++] = new Goal(terrain, numero, couleur);
break;
}
}
}
Et oui, notre entraineur a donc des attaquants, des défenseurs et un goal dans son equipe ! Petits détails de synthaxe maintenant ... on a vu que le tableau equipe est un tableau d'objet ... ça parrait louche de façon conceptuelle mais c'est tout à fait possible, un objet est une "variable" comme une autre et son maniement est identique. Maintenant, quel est ce "final" ? Pour utiliser les cases du switch, nous devons utiliser des constantes, Java déclare un objet de façon constante grâce au mot clef "final" bien souvent suivi de "static" (pour les utilisations, autant mettre une constante statique). Pour des raisons de pratique, j'ai déclaré nb_joueur en tant que private, puisque son utilisation n'est qu'interne.
Bon l'entraineur, il est bien content, il a un tableau avec tout plein des joueurs, mais au fait ... qui est attaquant ? H4n 1l s'3sT F41T 0wN3d L1k3 4 n00b ! (traduisez par : mince alors il s'est fait roulé). Java vole encore une fois au secours des entraineurs et invente l'"instanceof". Ce mot clef est un comparateur (au même titre que == ou <=), et on a :
- Code: Tout sélectionner
equipe[1] instanceof Attaquant // Traduisez par equipe[1] est une instance de la classe Attaquant
Ce qui va renvoyer vrai ou faux (true ou false, un booléen quoi)
Ainsi, notre brâve entraineur va enfin pouvoir organiser sa stratégie d'attaque. Pour cela il doit reconnaître l'équipier comme attaquant, on imagine maintenant une sorte de Forcing. On force monsieur Java à reconnaître l'équipier comme attaquant (pour pouvoir utiliser le savoir faire de notre joueur). On a donc :
- Code: Tout sélectionner
Attaquant unattaquant;
unattaquant = (Attaquant)equipe[i];
unattaquant.attaquer();
ou plus rapidement :
((Attaquant)equipe[i]).attaquer(); // Plus difficile à lire, mais plus pratique ...
En toute généralité, (Classe)objet, renvoi l'objet reconnu comme membre de la Classe.
On remarquera que cela marche avec les types prédéfinis avec les (int) par exemple lorsque l'on manipule des doubles que l'on veut passer en entier.
Après ceci, attaquant sera un réel attaquant, qui est l'objet equipe.
- Code: Tout sélectionner
...
public void Attaque() {
for(int i = 0 ; i < equipe.length ; i++) {
if(equipe[i] instanceof Attaquant) {
((Attaquant)equipe[i]).attaquer(); // BANZAI !!!
} else if(equipe[i] instanceof Defenseur) {
((Defenseur)equipe[i]).DefenseHaute(); // On défend quand même !
}
}
}
Maintenant, suivant le contexte, lorsque vous fabriquerez une nouvelle fonction qui ne fait qu'utiliser les propriétés du JOUEUR passé en paramètre, ne faites pas la même pour chaque classe de Joueur, faites une et une seule fonction qui demande en argument un Joueur (et non un attaquant).
J'ai compliqué l'exemple, j'en suis conscient, c'était pour vous parler de toutes les principales manipulations que l'on pouvait faire avec l'héritage. En temps normal, j'aurai défini une méthode en_avant dans la classe Joueur, et j'aurai paramétré la limite que chaque joueur n'aurai pas dépassé suivant son instanciation (Defenseur, Attaquant, Goal).
Bien des exemples et exercices viendront par la suite pour concrétiser tout cela, ne vous inquiétez pas, de plus si vous avez des questions (ce qui est surrement le cas), n'hésitez pas !
Bon dites vous que si vous avez compris l'héritage, vous avez une grande partie de l'objet que vous maitrisez, il ne vous reste plus que cette partie (importante tout de même !), plus certaines techniques très pratiques.
Bien, faisons un peu le point sur ce que nous avons maintenant. Nous avons un classe mère, puis des filles. Mais voila la problématique, les filles sont séparées, elles n'ont rien en commun. L'informatique est un véritable orphelinat d'objet ... (c'est triste avouez). Nous voulons donc (et c'est légitime) que l'on puisse les rassembler grâce à certains critère. Et c'est ainsi que l'interface est née.
Nous avons maintenant plusieurs classes (d'origines différentes) que l'on veut rassembler sous une même entitée. Nous allons donc imposer des conditions pour rentrer dans le clan, c'est d'avoir une même facade extérieure en commun, que l'on appelle interface. Voila une bonne chose ! Implémentons ceci dans notre cas, je veux que chaque Attaquant et Defenseur sachent tapper dans une balle (le gardien il se débrouille ...), je veux donc qu'ils aient tous deux une méthode tapper(). Moui, jusque là on pouvait le faire avant, certes, mais maintenant on va créer une interface Tappeur, ainsi, les attaquants et les défenseurs seront des Tappeurs, et pourront être déclarés en tant que tappeur.
Rappellez vous, nous avons dû utiliser l'instanceof pour appliquer une stratégie différente à chaque personnage, là dès que l'on recense une personne qui recoit le balon, que l'on récupèrera en tant que Tappeur, nous n'aurons pas besoin de savoir si c'est un attaquant ou pas, on bourrinera et on tapera dans le ballon. Implémentons cela maintenant,
- Code: Tout sélectionner
public interface Tappeur {
public boolean a_la_balle;
public void tapper();
}
Que vous enregistrez évidemment dans un beau Tappeur.java. Remarquez une chose, nous ne commençons plus par "class", tout simplement car Tappeur N'EST PAS UNE CLASSE et n'en sera jamais une, c'est juste un modèle de ressemblance (facade). Mais le plus important, c'est que l'on ne fait que déclarer les méthodes et variables ! Il n'est pas question de savoir ce qu'elles peuvent faire. Maintenant :
- Code: Tout sélectionner
public class Attaquant extends Joueur implements Tappeur { // On implémente l'interface
public boolean a_la_balle = false;
...
public void tapper() {
terrain.ballon.move(20); // je le fais bouger de 20 unités, ce n'est qu'un exemple
}
...
}
Ca y est, l'attaquant est un tappeur pour deux raisons, tout d'abord parce qu'on le lui a demandé (hey mais c'est pas bête ça), et ensuite parce qu'on a déclaré et fabriqué la méthode et la variable requises. La deuxième condition est essentielle, sans cela, le compilateur va vous cracher à la figure et va vous demander qu'est-ce qui se passe (il vous demandera de rendre la classe abstraite, cas que l'on traitera bien plus tard).
Petite remarque : On peut implémenter plusieurs interfaces pour une seule classe, par le procédé :
- Code: Tout sélectionner
class Machin implements Facade, Mur, Pierre {
Maintenant notre intelligence artificielle veut utiliser cela, supposons que pendant notre jeu, une fonction soit appelée lorsque d'un joueur reçoit le ballon et qu'il veuille le renvoyer vers l'avant de suite.
- Code: Tout sélectionner
public void a_recu_le_ballon(Tappeur t) {
// On fait des trucs, des calculs si on veut ...
...
// Et on tappe !
t.tapper();
}
Vous avez compris le principe ? Je résume : construire un nouveau clan, y imposer des conditions, et construire des objets répondant à ces conditions, ensuite les utiliser comme des membres du clan, (pas mal comme interprétation, non ?).
Nous verrons plus tard (dans pas très longtemps d'ailleurs), en utilisant les interfaces graphiques (non ce n'est pas la même signification), que l'utilisation de ce principe se fait obligatoirement surtout lors de la manipulation d'évenement (clic sur un bouton, touche du clavier ...).
Un petit retour sur les packages. Ce qui est important c'est de savoir que un package peut s'interpréter sous forme de dossiers. Tous les packages officiels sont des java ou des javax (depuis java 2). Vous pouvez vous même créer des packages en toute première ligne du programme par le moyen suivant :
- Code: Tout sélectionner
package votre_pack;
import ...
... // etc ...
CA Y EST ! Vous êtes enfin prêt à rentrer dans le feu de l'action ! Et pour célebrer votre entrée dans les programmeurs objets, je vous propose une petite liste de classes. A chaque fois je vous conduit à aller voir de vous même sur l'API Java : http://java.sun.com/j2se/1.4.2/docs/api/index.html
Package java.lang
C'est ZE classe. La classe maitresse. Quand je vous disais que tout est objet, et bien voila, tout objet a pour ancêtre la classe Object. Ses méthodes ne sont pas utiles pour l'instant, nous en verrons une à la dernière partie. Donc lorsque vous voudrez utiliser un objet en tout généralité, vous utiliserez cette classe.
Très peu utilisée dans la programmation de base, puisqu'on en appelle d'abord à l'objet. Mais au cas où.
Classe principale pour faire fonctionner un double d'une manière objet. Très souvent utilisé en statique avec les méthodes isInfinite(double v) qui dit si le double v passé en argument est infini et parseDouble(String s) que l'on a déjà vu.
Similaire à Double
La fameuse classe de chaîne de caractère ! Nous avons charAt qui indique le caractère en position passée en argument. Le compareTo (et compareToIgnoreCase qui ignore les cases), compare la chaine à une autre (retourne 0 si c'est égal), et equals est presque identique. Voir aussi les indexOf qui recherche des sous-chaines ou des caractères dans la chaine. Bien évidemment, il y a la taille des chaines : length(). substring(int debut, int fin), retourne la sous-chaine entre debut et fin.
On a déjà vu tout ce qui est utile à priori (ou presque).
Il n'y a que des méthodes statiques.
- abs : la valeur absolue.
- acos, asin, atan : Arc cosinus, arc sinus et arc tangente.
- ceil et floor : deux méthodes de Partie entière (ceil(x) = -floor(-x))
- cos, sin, tan : cosinus, sinus, tangente (en radians)
- exp : exponentielle base e
- log : logarithme népérien
- max(a,b) : retourne la valeur max entre a et b
- min(a,b) : pareil pour le min
- pow(a,b) : fait a^b (a puissance b)
- random() : retourne un nombre aléatoire entre 0.0 et 1.0
- round : arrondi
- sqrt : racine carrée
- toDegrees, toRadians
Petite formule : sortir un nombre aléatoire entre l'entier a et l'entier b :
- Code: Tout sélectionner
int rand = (int)((b-a)*Math.random()+a);
Ceux qui veulent voir exactement pourquoi ça fait ça, il suffit de réfléchir
Package : java.util
Une classe qui définit un vecteur dans le sens informatique du terme, c'est-à-dire, un tableau à dimension variable. Après l'avoir construit vous pouvez : add(Object o), on revoit ici l'utilité de l'Object, cette fonction vous ajoute votre objet au vecteur. get(int i), récupère la position i, isEmpty() indique le vecteur est vide, remove(int index) ou remove(Object o) retire l'objet à la position i, ou la première occurence de 'o'. Comme il y a le get, il y a le set : set(int i, Object element), la syntaxe parle d'elle même. Et comme on l'a vu on a besoin de size() évidemment.
Pour note, la description des fonctions que j'ai cité, est l'implémentation de l'interface List (de java.util).
Entendez par Pile (ou file suivant votre terminologie). On aura l'occasion de les revoir, mais elles sont TRES utile en algorithmie un peu élevée.
Pour tout ce qui est de la date, heure et bien évidemment calendrier.
Très utilisé lui aussi mais très difficile à manipuler je trouve. Bon imaginez que vous avez une chaîne de caractère, que vous avez "ponctuée", vous voulez récupérer les mots seulement. Le meilleur moyen est cette classe. Vous allez créer cet objet avec pour argument votre chaine et les délimiteurs, ici : " ,;:.", et la classe vous découpera la chaine et vous livrera les mots un après l'autre.
Notez qu'ici, je n'ai que peu détaillé les classes, d'une part parce que je n'en vois pas l'utilité, et d'autre part car la documentation java est TRES TRES bien faite, bien qu'en anglais (il y a un vieux proverbe moldave qui dit que "Si tu veux faire de l'informatique, tu dois apprendre l'anglais").
Ce concept n'est pas directement lié à l'héritage donc si vous n'avez pas compris ce qu'il y avait précédemment, ce n'est pas grâve.
En Java, et dans pas mal de langage maintenant, on peut utiliser pour la même méthode, plusieurs jeu de paramètres. Je m'explique, imaginez qu'à une procédure, vous demandiez en plus des options. Prenons un exemple plus concret : on a utilisé plus haut la fonction tapper(). Cette fonction bougeait de 20 unités le ballon. Maintenant vous souhaitez garder cette fonction et en créer une autre similaire à coté, mais sur laquelle vous puissiez paramétrer la force que vous allez donner à votre ballon. Il y a évidemment :
- Code: Tout sélectionner
public void tapper() {
terrain.ballon.move(20);
}
public void tapper2(int depl) {
terrain.ballon.move(depl);
}
Inconvénients : On doit recoder notre fonction entièrement... bon ici c'est une ligne, ça va, mais imaginez en plus grand ... Ensuite vous remarquez que exécuter tapper() est identiquer à faire tapper2(20). Premièrement, Java nous laisse le soin d'appeler tapper2, tapper. Ce qui sera bien moins lourd d'écriture. Ensuite on a parler de l'équivalence des procédés, exploitons le !
- Code: Tout sélectionner
public void tapper() {
tapper(20); // On a dit que tapper() équivaut à tapper(20)
}
public void tapper(int depl) { // Procédure au complet !
terrain.ballon.move(depl); // On déplace suivant la valeur qu'on a paramétré dans depl
}
Donc, il n'y a pas 150 mille façons d'utiliser la surcharge :
- Créer une fonction générale (avec le plus grand nombre de paramètres).
- Créer des fonctions auxiliaires (avec moins de params) qui appellent directement la générale.
Remarquez que l'on est pas obligé de faire UNE seule fonction générale, on peut appeler une méthode de façon identique tout en prenant des paramètres totalement différents ! Monsieur Java, lui il s'en fout !
En PHP, la structure est affaiblie puisqu'on ne crée qu'une seule méthode et on passe directement dans les paramètres les variables par défaut. Mais ceci est une autre histoire ...
Un dernier détail, lorsque l'on utilise la surcharge de méthode, pour éviter toute confusion dans le code, nous n'oublierons pas de mettre les paramètres optionnels dans l'ordre de leur importance. Une option toute bidon sera mise en dernier, tandis que l'information capitale sera en tête dans la liste des paramètres.
Bien maintenant, vous avez vu la surcharge de méthode. Sachez qu'il existe aussi la surcharge de constructeur ! Et oui, on peut construire un objet de différente manière, ce qui est logique d'ailleurs. Jusque là, tout va pour le mieux, mais quelque chose me chiffonne, ... , et si un constructeur auxiliaire veut appeler le général. AÏE, le méchant ! Avant, nous pouvions faire appel, après tout ce ne sont que des méthodes, mais maintenant, il faut construire un objet... plus dur.
Encore une fois, Java nous sort de cette impasse (qu'elle est intelligente cette tasse de café), en effet, il nous fournit le mot clef THIS (encore lui ?). Ainsi vous aurez :
- Code: Tout sélectionner
public Objet() { // Constructeur 1
this(0); // Appel du constructeur 2 par this(param1,param2 ...);
}
public Objet(int valeur) { // Constructeur 2 indépendant des autres
this.valeur = valeur;
}
Tout se calque de façon IDENTIQUE à la surcharge de méthode une fois cela constaté. Mais alors, si notre objet fait partie d'une sous-classe. La surcharge s'applique toujours, et même plus que jamais... rappelez vous du "super" employé pour définir l'objet père, il est toujours présent et supporte très bien la surcharge, ainsi il pourra être utilisé comme le this (mais sur l'objet père bien entendu).
Ouf, j'arrive enfin à la ligne d'arrivée de ce tuto, croyez moi j'y aurai passé du temps. Bref, le chapitre III et IV doivent être les plus compliqués mais ils ne doivent surtout pas être négligés, car ils concentrent en eux, la manipulation fondamentale de Java. Ainsi, ne pas avoir compris ces chapitres c'est équivalent à ne pas avoir compris l'intérêt de la POO. Donc, faisez gaf' les zamis.
Je suis conscient qu'il y a pas mal de choses qui ne sont pas évidentes, il y a tout une pensée et tout une syntaxe nouvelle à acquérir, donc ne perdez pas espoir... ne dit-on pas que tout vient à point à qui sait attendre ?
Je prévois de continuer par quelques rudiments sur les interfaces graphiques, ça risque d'être long et je vous préviens que je rentre dans une période où je deviens peu disponible donc n'espérez pas un tuto avant le 14 mai.
Merci de votre attention ...
Bonsoir ...
Loïc [i]WydD Petit






