L'encapsulation est une manière de définir une classe de telle sorte que ses attributs ne puisse pas être directement manipulés de l'extérieur de la classe, mais seulement indirectement par l'intermédiaire des méthodes.
Un des avantages de cette approche est la possiblité de redéfinir la représentation interne des attributs, sans que cela affecte la manipulation externe d'un objet de cette classe.
L'encapsulation facilite donc la mise à jour des applications. On peut en voir l'intéret dans un projet de développement réalisé par plusieurs développeurs (ce qui est généralement le cas en entreprise). Chaque développeur est responsable d'une partie du projet. Si les classes dont il est le responsable sont proprement encapsulées, il pourra modifier par la suite leurs représentations internes, sans que cela perturbe le travail des autres développeurs du projet susceptibles d'utiliser ces classes.
Un membre d'une classe désigne un attribut ou une méthode définie dans cette classe.
La visibilité des membres d'une classe définit les endroits d'où ils peuvent être utilisés. Dans cette partie du cours, nous allons voir deux sortes de visibilité: la visibilité publique et la visibilité privée.
Les membres privés d'une classe ne sont accessibles que par les méthodes de la classe:
Les membres publics d'une classe sont accessibles de l'intérieur et de l'extérieur de la classe, par conséquent:
Une classe est encapsulée, si tous ses attributs sont privés.
Les principes que nous venons d'énoncé sont valables dans tous les langages orientés objet. Voyons à présen,t à travers un exemple, comment définir une classe encapsulée en Java.
Les projets Java que nous avons considérés jusqu'ici étaient limités à une seule classe: la classe fenêtre de l'application. Dans ce contexte, les notions d'encapsulation et de visibilité des membres n'ont évidemment pas d'intérêt.
Dans le premier exemple, nous avons présenté un projet utilisant deux classes: la classe fenêtre et la classe Voiture. Dans ce projet, la classe Voiture était déclarée à l'intérieur de la classe fenêtre. Or lorsqu'une classe est déclarée à l'intérieur d'une autre, la classe interne peut forcément accéder à tous les membres de l'autre et inversement.
Pour que la notion de visibilité ait un intéret, il nous faut donc considérer un projet contenant au minimum deux classes, sans que l'une de ces deux classes ne soient déclarée à l'intérieur de l'autre.
Reprenons le projet Voiture2 en le séparant en deux fichiers:
Ce projet se trouve dans Exemple-Java-ProgObjet1/Voiture3.
L'interface graphique du projet contient un bouton supplémentaire qui permet de modifier le compteur de la voiture:
Voici la déclaration de la classe fenêtre (nous avons ommis intentionnellement les corps des méthodes):
public class Voiture3 extends javax.swing.JFrame { Voiture LaVoiture ; public Voiture3() {... private void BT_RoulerActionPerformed(...) { private void BT_CreerActionPerformed(...) { private void BT_ModifCompteurActionPerformed(...) { public static void main(String args[]) { private javax.swing.JButton BT_Creer; private javax.swing.JButton BT_ModifCompteur; private javax.swing.JButton BT_Rouler; private javax.swing.JTextField CT_Compteur; private javax.swing.JTextField CT_Distance; private javax.swing.JTextField CT_Immat; private javax.swing.JLabel jLabel1; private javax.swing.JLabel jLabel2; private javax.swing.JLabel jLabel3; }
En Java, un membre est publique si sa déclaration est précédée du mot clé public. Il est privé, s'il elle précédée du mot clé private. Par défaut, si rien n'est précisé, la visibilité est publique.
Dans notre exemple:
La classe Voiture est à présent définie dans un fichier séparé et non pas à l'intérieur de la classe fenêtre, comme dans l'exemple précédent. Voici sa déclaration (en ommettant les corps des méthodes):
public class Voiture { private int Compteur; private String Immat ; public int getCompteur () { ...} public void setCompteur (int c) {...} public Voiture (String im) {...} public boolean Rouler (int Km) {...} private boolean ChangerPneu () {... } }
Tous les attributs de cette classe sont privés. Nous avons donc ici un exemple de classe encapsulée.
L'encapsulation d'une classe interdit d'accéder à ses attributs depuis l'extérieur, mais elle autorise d'y accéder indirectement via des méthodes. Les méthodes jouant ce rôle sont appelées des accesseurs. On distingue les accesseurs en lecture des accesseurs en écriture:
Vous constaterez que la méthode AfficherAttributs a disparue. Nous avons été obligé de l'enlever car les composants de la classe fenêtre sont privés. Il est donc à présent impossible d'afficher quoi que ce soit dans la fenêtre de l'application depuis la classe Voiture !
Pour la même raison, nous avons modifié le code de la méthode Rouler:
public boolean Rouler (int Km) { if (! ChangerPneu() ) { Compteur = Compteur + Km ; return true; } else return false; }
les instructions d'affichage qui y figuraient on disparues. Il s'agit à présent d'une méthode de type fonction booléenne qui retourne la valeur true si la voiture peut rouler (c'est à dire qu'il n'est pas nécessaire de changer les pneus).
Remarquer que la méthode ChangerPneu n'est pas utilisable à l'extérieur de la classe, puisqu'elle est privée. Mais ceci n'est pas un problème, car nous n'en avons besoin que pour tester si la voiture peut rouler. Donc à l'intérieur de la méthode Rouler.
Voyons à présent comment la classe Voiture peut être utilisée depuis la classe fenêtre.
Voici la procédure évènementielle du bouton Nouvelle voiture:
private void BT_CreerActionPerformed(..) { LaVoiture = new Voiture (es.Lire (CT_Immat)); es.Afficher(LaVoiture.getCompteur(), CT_Compteur); }
comme nous ne pouvons pas accéder directement à l'attribut Compteur, on y accède par un appel de l'accesseur getCompteur, ce qui nous permet de l'afficher.
De même la procédure évènementielle du bouton Rouler a été modifiée pour prendre en compte l'encapsulation de la classe Voiture et l'absence d'instruction d'affichage dans cette classe:
private void BT_RoulerActionPerformed(...) { int d ; d = es.LireEntier (CT_Distance); if (LaVoiture.Rouler(d)) es.Afficher(LaVoiture.getCompteur(), CT_Compteur); else es.AfficherMessage("Vos pneus sont usés !"); }
la méthode Rouler est publique et donc utilisable depuis la classe fenêtre. Encore une fois, la valeur de l'attribut Compteur est obtenue via son accesseur en lecture.
Enfin, voici le code de la procédure évènementielle du nouveau bouton Modifier le compteur:
private void BT_ModifCompteurActionPerformed(...) { int c; c = es.LireEntier(CT_Compteur); LaVoiture.setCompteur(c); }
ici, c'est l'accesseur en écriture de l'attribut Compteur qui est utilisé. Notez que si nous avions voulu rendre le compteur de nos voitures infalsifiable, il aurait suffit de ne pas mettre d'accesseur en écriture sur cet attribut.
Arrivé à ce point, le lecteur pourra se demander ce que l'on a gagné avec l'encapsulation. Au premier abord, cela parait plus compliqué puisque l'on ne peut plus accéder aux attributs librement. On a l'impression de s'imposer inutilement des contraintes.
L'intéret de l'encapsulation est essentiellement dans la facilité de mise à jour d'un logiciel. Dans notre exemple, la classe Voiture est à présent facilement réutilisable dans d'autres contextes. En particulier, elle ne dépend plus de l'interface graphique du projet puisqu'elle n'accéde plus de manière directe à la fenêtre de l'application.
D'autre part, nous pouvons à présent modifier la représentation des données de la classe Voiture sans que cela affecte son utilisation. On pourrait par exemple ajouter un nouvel attribut privé: un booléen indiquant si les pneus sont usé. Tant que l'utilisation des méthodes (publiques forcément) de la classe Voiture est inchangée, cette modification sera totalement transparente à l'utilisateur de la classe.
On peut également mettre en doute l'intéret des accesseurs, surtout lorqu'il s'agit simplement de lire ou de modifier la valeur d'un attribut. N'est-il pas plus simple dans ce cas d'accéder directement à cet attribut ? Oui, mais l'accès aux attributs par des accesseurs vous donne certains avantages:
En Java, la notion de visibilité a également un sens pour une classe. Elle est directement liée à la notion de package (ou paquetage en francais) sur laquelle nous ne dirons que très peu de choses (pour plus de détails voir le livre de C. Delannoy).
La notion de package est spécifique au langage Java: c'est un regroupement de plusieurs classes. Un package porte un nom. Il existe des packages prédéfinis et vous pouvez également définir vos propres packages.
Parmis les package prédéfinis, il y a en particulier le package swing. Ce package contient les classes permettant de représenter l'interface graphique d'une application (fenêtre, bouton, étiquette, champs de texte, etc...).
Par défaut une classe ne peut être utilisée que par les classes du même package. Si on veut la rendre utilisable par tous les packages, elle doit être déclarée comme classe publique. Pour cela, il suffit de faire précéder la déclaration de la classe par la mot clé public.
Dans ce cours, nous utiliserons toujours le package par défaut pour nos classes. C'est dans ce package que le compilateur Java ira chercher une classe si rien n'est précisé.
Revenons à la déclaration de la classe fenêtre générée par NetBeans. Dans le projet Addition, NetBeans génère le code suivant:
La classe fenêtre est donc une classe publique. De plus cette classe utilise des classes du package swing: JFrame (classe représentant une fenêtre) ainsi que les classes JButton (boutons), JLabel (étiquettes) et JTextField (champs de texte). Comme ces classes n'appartiennent pas au package par défaut, Netbeans fait précéder leur nom par javax.swing (nom du package swing) afin que le compilateur sache où les trouver. Par exemple, la déclaration de l'attribut jButton1 est:
private javax.swing.JButton jbutton1;
On comprend mieux à présent la signification de ce code: le bouton jbutton1 est déclaré comme un objet de la classe Jbutton appartenant au package swing.
Vous retrouverez également le nom d'un autre package dans l'entête des procédures évènementielles. Par exemple, dans l'entête du bouton Addition du projet Additionner (toujours dans le même projet Addition):
private void BoutonAdditionnerActionPerformed ( java.awt.event.ActionEvent evt ) { ...
On y trouve un mystérieux paramètre nommé evt. Ce paramètre apparait dans la déclaration de toutes les procédures évènementielles associées aux boutons d'une fenêtre. Il représente en fait un évènement. C'est un objet de la classe ActionEvent du package java.awt.event. Il est destiné à contenir toutes les informations utiles sur l'évènement ayant déclenché l'exécution de la procédure évènementielle.