Exemple:
Une Pile est une structure de données avec (essentiellement) les deux opérations empiler et dépiler, a priori on peut manipuler une Pile sans pour autant savoir comment elle est réalisée.
Quelques principes:
En supposant qu'un programme n'utilise que ce qui est définit par le type abstrait de données, on peut développer le programme indépendamment des implémentations du type de abstrait.
Définition du type Pile de X (ici X est un type quelconque)
· Nom du type : Pile[X]
· types des opérateurs (signature des méthodes)
o création
§ Pile :-> Pile[X]
o test à vide
§ estVide: Pile[X] -> Boolean
§ empiler: XxPile[X] -> Pile[X]
§ dépiler: Pile[X] -> XxPile[X]
· propriétés des opérateurs:
o préconditions:
§ dépiler(X): non etsVide(X)
o axiomes:
§ Pour tout x de X pour tout s de Pile[X]
· estVide(Pile()) (une pile créée est initialement vide)
· non estVide(empiler(x,s))
·
dépiler(empiler(x,s))=(x,s)
Toutes les Piles au sens usuel vérifient ces propriétés et ces propriétés caractérisent les Piles.
Ces définitions et propriétés sont indépendantes de toute représentations (on ne sait même pas s'il peut y avoir une représentation) et avec un peu de formalisme supplémentaire on pourrait montrer d'autres propriétés sur les Piles uniquement à partir des axiomes.
On a vu dans le chapitre précédent le type Object qui peut contenir n'importe quel objet d'u type référence. On va donc construire un type Pile de Object. On pourra ainsi construire des piles sur n'importe quel objet. (On notera cependant que dans ce cas, on ne peut assurer que tous les objets de la pile sont du même type. Il existe en java un moyen –la généricité- permettant d'avoir des Piles construites avec des objets d'un type particulier).
Une interface en Java est composée de déclarations de méthodes mais sans définition de méthodes ni présence de variables d'instance:
// Interface Pile simple
interface Pile {
public Object
depiler();
public void empiler(Object o);
public boolean
estVide();
}
On peut ensuite avoir diverses implémentations de la cette interface.
// listes d Object
class Noeud {
Object item;
Noeud suivant;
public Noeud(Object o) {
item=o;
suivant=null;
}
public Noeud(Object o, Noeud n){
item=o;
suivant=n;
}
}
// implémenter les Pile avec des listes
class PileListe implements Pile{
private Noeud sommet;
public PileListe(){sommet=null;}
public
boolean estVide(){ return (sommet==null);}
public void empiler(Object o){
sommet=new Noeud(o,sommet);
}
public Object depiler(){
Object tmp=sommet.item;
sommet=sommet.suivant;
return tmp;
}
}
// implémenter les piles avec une table
class PileTable implements Pile{
private Object[] v;
private int
taillemax=1000;
private int sommet;
public PileTable(){
v=new Object[taillemax];
sommet=0;
}
public PileTable(int
n){
taillemax=n;
v=new
Object[taillemax];
sommet=0;
}
public boolean estVide(){return sommet==0;}
public Object depiler(){
return v[--sommet];
}
public void empiler(Object o){
v[sommet++]=o;
}
}
On notera que:
La déclaration d'une variable de type PileTable ou PileListe, comme la création d'un objet correspondant ne change pas de l'habitude:
PileListe q=new PileListe();
PileTable r=new PileTable(100);
Bien sûr il ne peut y avoir d'objet Pile général: aucune implémentation n'est présente, mais un objet PileTable ou PileListe peut toujours être considéré comme une Pile.
Pile q=new
PileListe();
PileTable r
q=r;
r= (PileTable)q;
/* ce qui suit est incorrect
Pile t=new Pile();
PileListe u=new PileTable();
*/
On notera:
Notons que:
§ au moment de la compilation le compilateur vérifie la compatibilité des types d'après les déclarations des variables.
§ au moment de l'exécution la machine virtuelle java vérifie que les affectations ont bien un sens de façon à assurer par exemple que tout objet PileTable est bien une PileTable (de façon similaire pour les tableaux le machine virtuelle Java vérifie qu'on ne sort pas des indices du tableau).
On peut ensuite travailler avec une pile de façon générale sans se préoccuper de savoir s'il s'agit d'une PileTable ou d'une PileListe (ou de n'importe quelle autre implémentation de Pile!):
public
static Noeud Pile2Liste(Pile p){
Noeud tmp;
Noeud tete=tmp=new Noeud(null);
while(!p.estVide())
tmp=tmp.suivant=new
Noeud(p.depiler());
return
tete;
}
Cette méthode transforme une pile en liste chaînée: elle peut être utilisée avec n'importe quelle implémentation de Pile. Elle est utilisée dans le morceau de code suivant:
Pile q=new
PileListe();
for (int i = 0;
i < 100; i++)
q.empiler(i);
Noeud
n=Pile2Liste(q);
Il est important de noter qu'au moment de la compilation le compilateur
ne sait pas à quelle méthode concrète sera associée empiler ou depiler,
il ne connaît que les méthodes abstraites depiler
et empiler qui ont été déclarées
dans l'interface Pile. Ce n'est
qu'au moment de l'exécution que, suivant l'objet qui sera passé en paramètre (PileTable, PileListe ou autre Pile)
dans le premier cas ou qui a été affecté
dans le deuxième cas, à quelle méthode concrète (empiler
ou depiler de PileTable, PileListe ou autre Pile)
sera associée à p.depiler ou q.depiler. Il faut noter qu'au moment
de la compilation du programme, il est tout à fait possible que la méthode
concrète ne soit pas encore définie. Il s'agit d'un mécanisme nouveau:
jusqu'ici l'association entre un nom (p.depiler)
et le code se faisait à
Pour résumer:
Ces mécanismes sont à la base de la programmation orientée objets. Ils se généralisent en Java à d'autres constructions que les interfaces. Pour l'essentiel on a les deux notions liées:
[1] D'une façon générale, ce qui concerne ce qui se passe à la compilation est dit statique et ce qui concerne l'exécution est dit dynamique.