Implémentons une file de nombre entiers. Nous avons un exemple dans le fichier suivant :
package encapsulation; public class FilePublic { /* * Elements de la file */ int[] entiers; /* * Indice de la tête de file et du premier emplacement libre dans le * tableau. */ int first, firstFree; /* * Initialise les attributs de la file. */ public void init(int taille) { entiers = new int[taille + 1]; first = firstFree = 0; } /* * Décale i d'une position vers la droite dans le tableau, revient au debut * si i déborde. */ public int incrementeIndice(int i) { i++; if (i == entiers.length) i = 0; return i; } /* * Retourne vrai si et seulement si la file est pleine. */ public boolean estPlein() { return first == incrementeIndice(firstFree); } /* * Retourne vrai si et seulement si la file est vide. */ public boolean estVide() { return first == firstFree; } /* * Ajoute l'élément n dans la file. */ public void enfile(int n) { if (!estPlein()) { entiers[firstFree] = n; firstFree = incrementeIndice(firstFree); } } /* * Supprime la tête de file. */ public void defile() { if (!estVide()) first = incrementeIndice(first); } /* * Retourne la tête de file. */ public int premier() { if (!estVide()) return entiers[first]; return 0; } }
Un exemple typique d’utilisation de cette file est donné ci-dessous :
package encapsulation; public class TestFilePublic { public static void main(String[] args) { FilePublic f = new FilePublic(); f.init(20); for (int i = 0; i < 30; i += 2) { f.enfile(i); f.enfile(i + 1); System.out.println(f.premier()); f.defile(); } while (!f.estVide()) { System.out.println(f.premier()); f.defile(); } } }
Si vous travaillez en équipe, et que vous êtes l’auteur d’une classe
FilePublic
, vous n’aimeriez pas que vos collègues
programmeurs s’en servent n’importe comment ! Vous n’aimeriez pas par
exemple qu’ils manipulent le tableau
entiers
ou l’attribut
first
sans passer par les méthodes, par exemple :
FilePublic f = new FilePublic(); f.init(20); f.entiers[4] = 3; f.first += 5; /* ... arretons la les horreurs ... */
Il serait appréciable que nous puissions interdire de telles instructions. C’est-à-dire forcer l’utilisateur de la classe à passer par les méthodes pour manier la file.
La visibilité d’un identificateur (attribut, méthode ou
classe) est l’ensemble des endroits dans le code ou il est possible de
l’utiliser. Si un identificateur et précédé du mot clé
public
, cela signifie qu’il est visible partout. Si un
identificateur et précédé du mot clé
private
, cela signifie
qu’il n’est visible qu’à l’intérieur de la classe. Seule l’instance
à qui cet identificateur appartient pourra l’utiliser. Par exemple :
package encapsulation; public class FilePrivate { private int[] entiers; private int first, firstFree; public void init(int taille) { entiers = new int[taille + 1]; first = firstFree = 0; } private int incrementeIndice(int i) { i++; if (i == entiers.length) i = 0; return i; } public boolean estPlein() { return first == firstFree + 1; } public boolean estVide() { return first == incrementeIndice(firstFree); } public void enfile(int n) { if (!estPlein()) { entiers[firstFree] = n; firstFree = incrementeIndice(firstFree); } } public void defile() { if (!estVide()) first = incrementeIndice(first); } public int premier() { if (!estVide()) return entiers[first]; return 0; } }
On teste cette classe de la même façon :
package encapsulation; public class TestFilePrivate { public static void main(String[] args) { FilePrivate f = new FilePrivate(); f.init(20); for (int i = 0; i < 30; i += 2) { f.enfile(i); f.enfile(i + 1); System.out.println(f.premier()); f.defile(); } while (!f.estVide()) { System.out.println(f.premier()); f.defile(); } } }
S’il vous vient l’idée saugrenue d’exécuter les instructions :
FilePrivate f = new FilePrivate(); f.init(20); f.entiers[4] = 3; f.first += 5; /* ... arretons la les horreurs ... */
vous ne passerez pas la compilation. Comme les champs
entiers
et
first
sont
private
, il est impossible de les
utiliser avec la notation pointée. Cela signifie qu’ils ne sont
accessibles que depuis l’instance de la classe
FilePrivate
qui sert de contexte à leur exécution. De cette façon
vous êtes certain que votre classe fonctionnera correctement. En
déclarant des champs privés, vous avez caché les divers détails
de l’implémentation, cela s’appelle l’encapsulation. Celle-ci
a pour but de faciliter le travail de tout programmeur qui utilisera
cette classe en masquant la complexité de votre code. Les
informations à communiquer à l’utilisateur de la classe sont la liste
des méthodes publiques. A savoir
public class FilePrivate { public void init(int taille){/*...*/} public boolean estPlein(){/*...*/} public boolean estVide(){/*...*/} public void enfile(int n){/*...*/} public void defile(){/*...*/} public int premier(){/*...*/} }
On remarque non seulement qu’il plus aisé de comprendre comment utiliser la file en regardant ces quelques méthodes mais surtout que la façon dont a été implémenté la file est totalement masquée.
Supposons que notre utilisateur oublie d’invoquer la méthode
init(int taille)
, que va-t-il se passer ? Votre classe va
planter. Comment faire pour être certain que toutes les variables
seront initialisées ? Un constructeur est un sous-programme
appelé automatiquement à la création de tout objet. Il porte le même
nom que la classe et n’a pas de valeur de retour. De plus, il est
possible de lui passer des paramètres au moment de
l’instanciation. Remplaçons
init
par un constructeur :
package encapsulation; public class FileConstructeur { private int[] entiers; private int first, firstFree; public FileConstructeur(int taille) { entiers = new int[taille + 1]; first = firstFree = 0; } private int incrementeIndice(int i) { i++; if (i == entiers.length) i = 0; return i; } public boolean estPlein() { return first == firstFree + 1; } public boolean estVide() { return first == incrementeIndice(firstFree); } public void enfile(int n) { if (!estPlein()) { entiers[firstFree] = n; firstFree = incrementeIndice(firstFree); } } public void defile() { if (!estVide()) first = incrementeIndice(first); } public int premier() { if (!estVide()) return entiers[first]; return 0; } }
On peut alors l’utiliser sans la méthode
init
,
package encapsulation; public class TestFileConstructeur { public static void main(String[] args) { FileConstructeur f = new FileConstructeur(20); for (int i = 0; i < 30; i += 2) { f.enfile(i); f.enfile(i + 1); System.out.println(f.premier()); f.defile(); } while (!f.estVide()) { System.out.println(f.premier()); f.defile(); } } }
Il est de bonne programmation de déclarer tous les attributs en privé, et de permettre leur accès en forçant l’utilisateur à passer par des méthodes. Si un attribut X est privé, on crée deux méthodes getX et setX permettant de manier X. Par exemple,
package encapsulation; public class ExempleAccesseurs { private int foo; public int getFoo() { return foo; } public void setFoo(int value) { foo = value; } }
On ainsi la possibilité de manier des attributs privés indirectement.
Il est possible de definir dans une même classe plusieurs fonctions portant le même nom. Par exemple,
package encapsulation; public class FileSurcharge { private int[] entiers; private int first, firstFree; public FileSurcharge(int taille) { entiers = new int[taille + 1]; first = firstFree = 0; } public FileSurcharge(FileSurcharge other) { this(other.entiers.length - 1); for (int i = 0; i < other.entiers.length; i++) entiers[i] = other.entiers[i]; first = other.first; firstFree = other.firstFree; } private int incrementeIndice(int i) { i++; if (i == entiers.length) i = 0; return i; } public boolean estPlein() { return first == firstFree + 1; } public boolean estVide() { return first == incrementeIndice(firstFree); } public void enfile(int n) { if (!estPlein()) { entiers[firstFree] = n; firstFree = incrementeIndice(firstFree); } } public void defile() { if (!estVide()) first = incrementeIndice(first); } public int premier() { if (!estVide()) return entiers[first]; return 0; } }
On remarque qu’il y a deux constructeurs. L’un prend la taille du tableau en paramètre et l’autre est un constructeur de copie. Selon le type de paramètre passé au moment de l’instanciation, le constructeur correspondant est exécuté. Dans l’exemple ci-dessous, on crée une file g en applicant le constructeur de copie à la file f.
package encapsulation; public class TestFileSurcharge { public static void main(String[] args) { FileSurcharge f = new FileSurcharge(20); for (int i = 0; i < 30; i += 2) { f.enfile(i); f.enfile(i + 1); System.out.println(f.premier()); f.defile(); } FileSurcharge g = new FileSurcharge(f); while (!f.estVide()) { System.out.println(f.premier()); f.defile(); } while (!g.estVide()) { System.out.println(g.premier()); g.defile(); } } }
En l’absence de mots-clés
public
(tout le monde),
private
(seulement la classe) et
protected
(classe +
classes dérivées), une visibilité par défaut est appliquée. Par défaut,
la visibilité des éléments est limitée au package.
Considérons l’exemple suivant :
package encapsulation; public class NativeArray { public static void main(String[] args) { int taille = (47 - 2) / 3 + 1; int[] a = new int[taille]; for (int value = 2, index = 0; value < 50; value += 3, index++) a[index] = value; for (int index = 0; index < taille; index++) { int valeur = a[index]; System.out.println(valeur); } } }
Dans ce programme, les valeurs entières 2, 5, 8, …, 47 sont placées dans un tableau puis affichées. Vous remarquerez que la principale difficulté est liée à l’utilisation des indices.
Il existe des classes en Java permettant de s’affranchir de ce type d’inconvénient en prenant en charge les problèmes d’indices. Par exemple :
package encapsulation; import java.util.ArrayList; public class CollectionArray { public static void main(String[] args) { ArrayList a = new ArrayList(); for (int value = 2; value < 50; value += 3) a.add("a " + value); for (int index = 0; index < a.size(); index++) { int valeur = (int) a.get(index); System.out.println(valeur); } } }
La classe
ArrayList
est un tableau dont la taille s’adapte au nombre
de données contenues. Les méthodes suivantes ont été utilisées dans
l’exemple ci-avant :
public boolean add(... o)
, ajoute l’objet
o
à la fin du tableau.
public ... get(int index)
permet de récupérer l’objet
stocké dans le tableau à l’indice
index
.
public int size()
retourne la taille actuelle du tableau,
c’est à dire le nombre d’éléments qu’il contient.
L’utilisation des collections se paye avec des instructions peu élégantes :
package encapsulation; import java.util.ArrayList; public class Cast { public static void main(String[] args) { ArrayList a = new ArrayList(); a.add("toto"); String s = (String) a.get(0); System.out.println(s); a.add(5); int i = (int) a.get(1); System.out.println(i); } }
Il est nécessaire d’effectuer un cast particulièrement laid pour passer la compilation. Les types paramétrés fournissent un moyen élégant de résoudre ce problème.
Lorsque l’on déclare une référence vers un type paramétré, on doit préciser
au moment de la déclaration le type que l’on souhaite utiliser.
Si par exemple, on souhaite créer un
ArrayList
ne pouvant
contenir que des objets de type
String
, la référence devra
être du type
ArrayList<String>
.
package encapsulation; import java.util.ArrayList; public class ArrayListParametre { public static void main(String[] args) { ArrayList<Integer> a = new ArrayList<Integer>(); for (int value = 2; value < 50; value += 3) a.add(value); for (int index = 0; index < a.size(); index++) System.out.println(a.get(index)); } }
Il est possible lors d’une instanciation, d’omettre les le type, java prendra automatiquement le type correct.
Il existe, de façon analogue à que l’on observe en C++, une syntaxe permettant simplement de parcourir une collection qui est :
for (T x : c) { /* ... */ }
Dans la spécification ci dessus,
c
est la collection,
T
le type (ou un sur-type) des objets de la collection et
x
une variable (muette) dans laquelle sera placé tour à tour
chaque objet de la collection.
package encapsulation; import java.util.ArrayList; public class ArrayListForEach { public static void main(String[] args) { ArrayList<Integer> a = new ArrayList<>(); for (int value = 2; value < 50; value += 3) a.add(value); for (int value : a) System.out.println(value); } }