| - Bases de la P.O.O. - |
La programmation orientée objet a été inventée dans le but de structurer la conception de logiciels, de manière à les rapprocher de la réalité. En effet, on peut observer que dans la vie courante, chaque objet peut être décomposé en un ensemble d'objets plus petits. En Java, il suffira donc de partir de la description globale d'un type d'objet pour ensuite définir chacun de ses sous-ensembles dérivés. Les définitions les plus générales et les plus abstraites effectuées au sein d'un programme, pouvant donner naissance à tout un ensemble d'objets dérivés, répondront au nom de classes.
Pour prendre un exemple, considérons la classe Plante: celle-ci décrira en détail chacun des éléments constituant une plante (racines, feuilles, capacité à pousser, recycler du CO2 en oxygène etc.). Pour le moment, cette classe n'est qu'une représentation abstraite visant à définir le concept de plante. Pour créer une plante de manière effective, il faudra, au sein du programme, appeller des instances de cette classe. Toutes ces instances auront pour point commun de posséder les caractéristiques propres à chaque plante, bien que les objets puissent différer les uns des autres (type de feuilles, hauteur etc.).
Dès à présent, nous pouvons distinguer deux propriétés caractérisant une classe: celle-ci est en effet en mesure de posséder à la fois des attributs (le type de feuilles par exemple) et des comportements (générer de l'oxygène etc.). Afin de mettre en avant ce concept, nous allons créer de manière progressive la classe Plante que nous avons évoqué plus haut.
Les attributs sont l'ensemble des éléments qui permettent de différencier, à l'intérieur d'une même classe, un objet d'un autre objet dérivé, tout en définissant son apparence, ses états etc.. Notre classe Plante pourrait regrouper les attributs suivants:
Pour définir ces attributs au sein de la classe, nous devrons recourir à des variables, qui pourront être considérées comme globales (accessibles depuis n'importe où) pour chaque objet de cette classe. Tout objet issu de notre classe Plante pourra donc être défini par ces variables, que nous nommerons variables d'instance. Pour modifier un attribut au sein d'un programme, il suffit d'utiliser l'opération d'affectation, comme dans l'exemple suivant:
plante1.hauteur = 30;
Dans cet exemple, il faut considérer que nous avons préalablement créé une instance de la classe Plante, que nous avons nommée plante1. Son attribut hauteur est à présent affecté à la valeur 30. Si besoin est, il est également possible de déclarer la valeur d'une variable d'instance lors de la création d'un objet.
En dehors des variables d'instance, il existe un autre type d'attribut qui s'attache à décrire toute une classe d'objets, et non uniquement des objets isolés provenant de cette classe: il s'agit des variables de classe. Pour mieux visualiser la chose, imaginons une variable compteur qui devrait recenser le nombre de plantes présentes dans une région: si nous utilisions une variable d'instance pour chaque objet de la classe Plante créé, il y aurait confusion au niveau du comptage, car chaque objet serait en mesure de posséder un compteur différent. La création d'une variable de classe permet de contourner ce problème, en ne stockant qu'une valeur et en permettant à chacun des objets de la classe d'accéder à cette variable.
Les comportements servent à définir de quelle manière les objets d'une classe peuvent interagir avec eux-mêmes ou encore avec d'autres objets. Cela permet de déterminer les actions nécessaires pour qu'un objet de cette classe puisse changer ses attributs, mais également ce qu'ils doivent effectuer lorsque d'autres objets lui demandent d'exécuter quelque chose. Pour notre classe Plante, nous pourrions exposer les comportements suivants:
En Java, le comportement d'une classe d'objets est défini par ce que l'on nomme des méthodes, blocs d'instructions que l'on peut comparer aux fonctions dans les autres langages.
A présent, nous allons concevoir notre première classe Java, afin d'illustrer les concepts mis en avant plus haut. Dans ce langage, la définition d'une classe est introduite par le mot clef class, suivi du nom de la classe proprement dite. Ensuite, il convient de placer le code nécessaire entre deux accolades. Pour notre classe Plante, il faudra écrire:
class Plante
{
// Code à venir
}
Pour indication, la ligne introduite par le double slash // a pour seul intérêt d'effectuer un commentaire, qui sera exclu lors de la compilation. Pour le moment, notre classe n'est censée rien effectuer, mais présente l'atout d'être dors et déjà fonctionnelle. Pour rendre celle-ci plus intéressante, déclarons-lui les attributs que nous avons défini en haut:
int hauteur; String couleur; boolean assoiffe;
Ici, le mot-clef int
déclare une variable de type entier, et boolean
une variable de type booléenne (pouvant être soit
vraie: true, soit fausse: false),
nom issu du mathématicien George Boole. Il est important
de noter que Java n'accepte pas, à l'opposé de C,
les littéraux 0 et 1
comme valeurs booléennes.
A première vue, vous pouvez vous demander pourquoi je n'ai
point mis en couleur le mot-clef String (censé introduire
une chaîne de caractères). Cela est dû au fait
qu'en invoquant la seconde ligne, en lieu et place de déclarer
une variable, on créé un objet couleur, instance
de la classe String. Cette particularité confère
aux chaînes de caractères un certain nombre de propriétés
que nous verrons plus loin. Pour l'instant, vous pouvez vous contenter
d'assimiler String en tant que mot-clef propre à
Java.
Maintenant que nous avons mis en place les attributs, il est temps de créer les méthodes spécifiques aux plantes. Pour cela, nous devons créer des fonctions qui, pour notre exemple, ne devront pas retourner d'argument. Une méthode en Java est introduite par le type de l'argument de retour, dans notre cas inexistant: nous utiliseront donc le mot-clef void (vide). On fait suivre ce type par le nom de la fonction, puis on passe les paramètres entre parenthèses; si aucun paramètre n'est à préciser, il suffira d'écrire des parenthèses vide (). Enfin, si le contenu de la fonction fait plusieurs instructions, il faudra entourer celui-ci d'accolades.
Dans un premier temps, créons la méthode arrosage(), qui devra répondre aux besoins en eau de notre objet Plante. Au niveau de la syntaxe, tout ce que vous devez connaître se situe au niveau de l'instruction System.out.println, qui a pour effet d'afficher un texte à l'écran, et de la clause if (expr) instr1 else instr2 (si expr=vrai alors instr1 sinon instr2).
void arrosage()
{
if (assoiffe)
{
System.out.println("Arrosage de la plante.");
assoiffe = false;
}
else
System.out.println("Inutile d'arroser la plante.");
}
Cette méthode teste si la variable booléenne assoiffe de l'objet Plante est affectée à la valeur true (vrai). Si tel est le cas, on arrose la plante, et la valeur de la variable assoiffe devient false (faux). Sinon, un message nous indique que la plante n'a pas besoin d'eau pour le moment.
Tant que nous y sommes, créons également la méthode montreCaracteristiques(), qui devra nous fournir une fiche de renseignements sur notre objet Plante, et nous indiquer s'il est rassasié en eau:
void montreCaracteristiques()
{
System.out.println("La plante fait " + hauteur + " metres.");
System.out.println("Ses feuilles sont de la couleur " + couleur + ".");
if (assoiffe)
System.out.println("La plante a besoin d'etre arrosee.");
else
System.out.println("La plante est rassasiee.");
}
Finallement, il nous reste à définir la méthode main(), point de départ du programme lorsqu'on génère une application autonome; pour cela, deux solutions s'offrent à nous afin de positionner cette méthode. Soit nous la plaçons dans une autre application ou applet Java qui exploite cette classe, soit dans le corps de la classe Plante, afin que celle-ci soit directement exécutable: c'est cette dernière solution que nous retiendrons. La signature de la méthode main() est toujours composée de quatre éléments:
Au final, la signature de la méthode main() sera toujours sous la forme:
public static void main(String arguments[])
{
// Instructions composant la méthode
}
Dans notre cas précis, nous devrons créer une instance de la classe Plante, objet que nous nommerons p. Ensuite, il suffira de lui changer ses attributs et effectuer des appels de méthodes:
public static void main(String arguments[])
{
// Création de l'objet p, instance de la classe Plante
Plante p = new Plante();
// Déclaration des paramètres initiaux
p.hauteur = 30;
p.couleur = "vert";
p.assoiffe = true;
// Appels des méthodes
System.out.println("Appel de la methode montreCaracteristiques:");
p.montreCaracteristiques();
System.out.println("------");
System.out.println("Appel de la methode arrosage:");
p.arrosage();
System.out.println("------");
System.out.println("Nouvel appel de la methode montreCaracteristiques:");
p.montreCaracteristiques();
System.out.println("------");
System.out.println("On arrose encore la plante:");
p.arrosage();
}
Maintenant, nous possédons tous les éléments pour réaliser notre classe Plante finale:
class Plante
{
int hauteur;
String couleur;
boolean assoiffe;
void arrosage()
{
if (assoiffe)
{
System.out.println("Arrosage de la plante.");
assoiffe = false;
}
else
System.out.println("Inutile d'arroser la plante.");
}
void montreCaracteristiques()
{
System.out.println("La plante fait " + hauteur + " metres.");
System.out.println("Ses feuilles sont de couleur " + couleur + ".");
if (assoiffe)
System.out.println("La plante a besoin d'etre arrosee.");
else
System.out.println("La plante est rassasiee.");
}
public static void main(String arguments[])
{
// Création de l'objet p, instance de la classe Plante
Plante p = new Plante();
// Déclaration des paramètres initiaux
p.hauteur = 30;
p.couleur = "vert";
p.assoiffe = true;
// Appels des méthodes
System.out.println("Appel de la methode montreCaracteristiques:");
p.montreCaracteristiques();
System.out.println("------");
System.out.println("Appel de la methode arrosage:");
p.arrosage();
System.out.println("------");
System.out.println("Nouvel appel de la methode montreCaracteristiques:");
p.montreCaracteristiques();
System.out.println("------");
System.out.println("On arrose encore la plante:");
p.arrosage();
}
}
Après compilation et exécution de la classe Plante, nous obtenons le résultat suivant à l'écran:
Appel de la methode montreCaracteristiques: La plante fait 30 metres. Ses feuilles sont de couleur vert. La plante a besoin d'etre arrosee. ------ Appel de la methode arrosage: Arrosage de la plante. ------ Nouvel appel de la methode montreCaracteristiques: La plante fait 30 metres. Ses feuilles sont de couleur vert. La plante est rassasie. ------ On arrose encore la plante: Inutile d'arroser la plante.
Il existe encore un grand nombre de propriétés relatives aux classes Java qui permettent de simplifier et mieux structurer la conception d'un programme. Parmi celles-ci pourrait figurer en premier le concept d'héritage, qui remet totalement en question la manière dont nous devrons concevoir nos futures applications. L'héritage permet en effet à une classe, que nous nommerons plus précisément sous-classe, d'hériter de la totalité des attributs et des comportements d'une autre classe (que nous appellerons alors super-classe). De fait, pour créer une nouvelle classe, il suffira de préciser ce en quoi elle diffère d'une classe déjà existante. A partir de là, il devient nécessaire d'organiser les classes suivant une structure hiérarchique, que l'on représente traditionnellement sous forme d'un arbre renversé.
Une des règles spécifique au Java est qu'une classe ne peut posséder qu'une seule super-classe. En revanche, celle-ci est en mesure de posséder un nombre illimité de sous-classes: si une super-classe possède les attributs et les comportements nécessaires aux autres sous-classes,il sera inutile de recopier le code nécessaire aux redéfinitions de ceux-ci.
La classe la plus générale de la hiérarchie Java est la super-classe Object: toutes les autres classes héritent par définition de ses attributs et de ses comportements. Par conséquent, si l'on était amené à créer une classe n'ayant aucun point commun avec les classes déjà existantes, il faudrait la faire hériter directement de la classe Object; elle serait ainsi pleinement intégrée à la hiérarchie Java. Pour automatiser ce processus il a été convenu qu'en cas d'omission, lors de la définition de la classe, du nom de la super-classe à laquelle elle est censée appartenir, celle-ci devrait alors être rattachée à la classe Object.
Pour revenir à l'exemple qui nous a fait tenir tout ce chapitre, imaginons une hiérarchie que la classe Plante serait susceptible d'engendrer:

Pour les besoins de ce chapitre, nous allons juste créer la classe planteClassique, qui présentera comme seule originalité vis-à-vis de sa super-classe Plante la possibilité de recycler du dioxyde de carbone en oxygène respirable. Pour cela, nous allons lui faire hériter de tous les attributs et de toutes les méthodes de la classe Plante (qui elle-même hérite de la classe Object), grâce au mot-clef extends:
class planteClassique extends Plante
{
// Code à venir
}
Pour assurer le fonctionnement de notre nouvelle classe, nous devrons créer une nouvelle variable booléenne co2, qui aura pour charge de renseigner quant à la présence ou non de dioxyde de carbone dans le milieu:
boolean co2;
Maintenant, créons une méthode productionOxygene(), qui nous indiquera si la plante est ou non en train de recycler le dioxyde de carbone en oxygene, en utilisant la variable co2 décrite plus haut:
void productionOxygene()
{
if (co2)
System.out.println("La plante est en train de recycler du CO2 en oxygene...");
else
System.out.println("La plante n'a pas suffisamment de CO2 pour produire de l'oxygene...");
}
A présent, nous devons nous heurter au problème que représente la méthode main() définie dans la classe Plante: en effet, celle-ci ne convient plus à l'application de notre nouvelle classe planteClassique, qui devrait mettre en oeuvre la méthode productionOxygene(). Pour empêcher une méthode provenant d'une super-classe d'être utilisée, il faut créer au sein de la sous-classe une redéfinition de cette méthode (qui possèdera donc le même nom, les mêmes arguments et retournera le même type de valeur que la méthode originelle). Pour la classe planteClassique, il faudra implanter la redéfinition suivante:
public static void main(String arguments[])
{
// Création de l'objet p, instance de la classe Plante
planteClassique p = new planteClassique();
// Déclaration des paramètres initiaux
p.hauteur = 30;
p.couleur = "vert";
p.assoiffe = true;
// Appels des méthodes
System.out.println("Appel de la methode montreCaracteristiques:");
p.montreCaracteristiques();
System.out.println("------");
System.out.println("Appel de la methode arrosage:");
p.arrosage();
System.out.println("------");
System.out.println("Nouvel appel de la methode montreCaracteristiques:");
p.montreCaracteristiques();
System.out.println("------");
System.out.println("On arrose encore la plante:");
p.arrosage();
System.out.println("------");
System.out.println("On alimente le milieu en CO2 puis appel de la methode productionOxygene:");
p.co2=true;
p.productionOxygene();
System.out.println("------");
System.out.println("A present, on place la plante dans un milieu prive de CO2:");
p.co2=false;
p.productionOxygene();
}
Dans cet exemple, on voit clairement que les attributs et les méthodes de la super-classe Plante on été conservés au sein de notre nouvelle classe. Si, à tout hasard, Java ne trouvait la variable ou la méthode définie dans la classe courante, il irait chercher un équivalent plus haut dans la hiérarchie. De même, le langage est en mesure de faire le tri entre plusieurs méthodes de nom identique, mais différant au niveau des paramètres et/ou de l'argument de retour.
Pour finir, voici le code complet de la classe planteClassique():
class planteClassique extends Plante
{
boolean co2;
void productionOxygene()
{
if (co2)
System.out.println("La plante est en train de recycler du CO2 en oxygene...");
else
System.out.println("La plante n'a pas suffisamment de CO2 pour produire de l'oxygene...");
}
public static void main(String arguments[])
{
// Création de l'objet p, instance de la classe Plante
planteClassique p = new planteClassique();
// Déclaration des paramètres initiaux
p.hauteur = 30;
p.couleur = "vert";
p.assoiffe = true;
// Appels des méthodes
System.out.println("Appel de la methode montreCaracteristiques:");
p.montreCaracteristiques();
System.out.println("------");
System.out.println("Appel de la methode arrosage:");
p.arrosage();
System.out.println("------");
System.out.println("Nouvel appel de la methode montreCaracteristiques:");
p.montreCaracteristiques();
System.out.println("------");
System.out.println("On arrose encore la plante:");
p.arrosage();
System.out.println("------");
System.out.println("On alimente le milieu en CO2 puis appel de la methode productionOxygene:");
p.co2=true;
p.productionOxygene();
System.out.println("------");
System.out.println("A present, on place la plante dans un milieu prive de CO2:");
p.co2=false;
p.productionOxygene();
}
}
Après compilation puis exécution de cette classe, nous obtenons la sortie suivante à l'écran:
Appel de la methode montreCaracteristiques: La plante fait 30 metres. Ses feuilles sont de couleur vert. La plante a besoin d'etre arrosee. ------ Appel de la methode arrosage: Arrosage de la plante. ------ Nouvel appel de la methode montreCaracteristiques: La plante fait 30 metres. Ses feuilles sont de couleur vert. La plante est rassasiee. ------ On arrose encore la plante: Inutile d'arroser la plante. ------ On alimente le milieu en CO2 puis appel de la methode productionOxygene: La plante est en train de recycler du CO2 en oxygene... ------ A present, on place la plante dans un milieu prive de CO2: La plante n'a pas suffisamment de CO2 pour produire de l'oxygene...
Si le système d'héritage simple (une seule super-classe pour chaque classe Java) peut paraître restrictif, il faut noter que ce choix simplifie notablement la création du code nécessaire à la définition des classes. Cependant, il existe un moyen de pallier à cette limitation en employant des interfaces (série de méthodes indiquant qu'une classe est pourvue d'un comportement supplémentaire vis-à-vis du comportement dont elle hérite déjà de sa super-classe).
En Java, les classes et les interfaces standard sont regroupées dans des packages, ce qui permet de ne les rendre disponibles qu'en cas de nécessité. De plus, ce système permet d'éviter les conflits intervenant entre des noms de classes identiques mais n'appartenant pas aux mêmes groupes. Toutes les bibliothèques de classes Java sont contenues dans le package java.lang, mis à part certains ensembles spécifiques à certaines distributions de Java (sun, netscape, javax etc.). Si aucun nom de package n'est précisé au sein du programme, ce dernier n'aura accès par défaut qu'aux classes du package java.lang, contenant les fonctions de base du langage. Pour importer un package, il faut employer le mot-clé import à l'en-tête du programme. Pour notre exemple, nous aurions pu charger explicitement le package java.lang grâce à la commande:
import java.lang;