Les interfaces

Chapitres traités   


Notion et intérêt des interfaces

Protection des objets

Nous avons déjà longuement parlé du principe d'encapsulation qui consiste, entre autre, de protéger au maximum les attributs d'un objet. Dans ce cas de figure (qui est normalement le cas courant) la modification des attributs passe obligatoirement par des méthodes adaptées. Il peut être intéressant de protéger encore plus un objet afin qu'il ne soit accessible que par un intermédiaire (un représentant). Java permet d'utiliser cette technique au moyen de l'interface. Seules quelques méthodes sont autorisées à être utilisées. Ce sont les méthodes désignées par l'interface. Comme son nom l'indique, l'interface joue le rôle d'intermédiaire entre l'application et l'objet sollicité.

Choix du chapitre Objets distribués

Dans l'exemple ci-dessus, il existe une classe FichePersonne. On désire qu'elle ne soit pas directement accessible par les applications n°1 et n°2. La seule tolérance admise pour ces deux applications est de permettre la visualisation de la fiche de chacune des personnes. On utilise donc un représentant de la classe qui est l'interface Visualisation et qui valide la seule méthode affiche pour ce cas particulier.

Il existe une technologie récente qui est à la fois performante et séduisante qui permet de construire des objets comme nous avons l'habitude de faire, mais qui ont la particularité d'être accessibles depuis n'importe quel ordinateur du réseau. C'est la technologie des objets distribués. Elle est intéressante puisqu'elle permet de construire les objets une fois pour toute sur une machine, et ainsi, il n'est pas nécessaire de les recopier sur tous les autres ordinateurs du réseau. Attention toutefois, ces objets restent sur l'ordinateur où ils ont été créés. Ce sont des objets distants qui fonctionnent en permanence, et donc la seule possibilité de les solliciter est de passer par des requêtes sur le réseau, et donc de passer par des interfaces qui utilisent les méthodes appropriées. Dans l'exemple ci-dessus, l'application du client désire récupérer l'adresse d'une personne, elle passe donc par le représentant de la classe FichePersonne qui est l'interface RequêtePersonne qui dispose de la méthode getAdresse (généralement, il existe plusieurs méthodes pour ce genre d'interface). On retrouve ici le même principe que pour les bases de données.

Choix du chapitre Hiérarchies différentes

Il arrive assez souvent qu'on désire mettre en relation des objets qui n'ont apparemment aucun rapport entre eux. C'est notamment le cas lorsqu'on désire afficher des objets qui sont issus de hiérarchies différentes. Il suffit alors de prévoir une interface qui proposera une méthode commune à l'ensemble des objets à afficher. Dans l'exemple ci-dessus, les classes Cercle et Carré héritent de la classe Forme qui dispose de la méthode Dessine. Par ailleurs, la classe Texte qui est totalement indépendante de la première hiérarchie dispose également de la méthode Dessine. Pour être sûr de dessiner un objet quelconque sur une fenêtre, par exemple, il suffit de passer par l'interface Présentation qui dispose de la méthode Dessine et qui spécifie donc que les objets associés à cette interface vont obligatoirement implémenter cette méthode.

Choix du chapitre Gestion événementielle

Tout système d'exploitation qui supporte des interfaces graphiques doit constamment surveiller l'environnement afin de détecter des événements tels que la pression sur une touche du clavier ou sur un bouton de la souris. Java contrôle complètement la manière dont les événements sont transmis de la source d'événement (par exemple, un bouton ou un élément de menu) à l'écouteur d'événement (event listener). Vous pouvez désigner n'importe quel objet comme écouteur d'événement. Lorsqu'un événement arrive à la source, celle-ci envoie une notification à tous les objets écouteurs d'événements.

Comme il est possible d'avoir n'importe quel objet (issu d'une hiérarchie quelconque) comme écouteur d'événement, il est nécessaire de passer par le système d'interface. En fait, un objet écouteur est une instance d'une classe qui implémente l'interface spéciale appelée interface écouteur (listener interface). Dans l'exemple ci-dessus, deux écouteurs ont été mis en oeuvre, l'objet relatif à un élément du menu et l'objet boutonCercle (dans ce cas de figure, ils sont à la fois source et écouteur). Ils sont représentés par l'interface ActionListener, et lorsqu'un un événement se produit (correspondant à un clic sur un bouton de la souris ou la validation par le clavier) la méthode actionPerformed est exécutée.

Pour en savoir plus sur la gestion événementielle

Choix des chapitres Mise en oeuvre des interfaces

Mais qu'est-ce qu'une interface ? Il s'agit essentiellement d'une annonce (ou d'une promesse) indiquant qu'une classe implémentera certaines méthodes et respectera donc le contrat prévu par l'interface. Nous devons d'ailleurs utiliser le mot clé implements pour signaler qu'effectivement cette classe tiendra cette promesse.

Définition d'une interface

Les interfaces contiennent uniquement des définitions de constante et des déclarations de méthodes, sans leur définition (autrement dit, les méthodes indiquées dans les interfaces ne comportent que le prototype sans le corps). La déclaration d'une interface se présente comme celle d'une classe. On utilise simplement le mot clé interface à la place de class :

En reprenant l'exemple proposé plus haut, nous obtenons :

Par essence, les méthodes d'une interface sont :

  1. abstraites (puisque nous ne fournissons pas de définition) et,
  2. publiques (puisqu'elles devront être redéfinies plus tard).

Néanmoins, il n'est pas nécessaire de mentionner les mots clés public et abstract (nous pouvons quand même le faire).

Une interface peut être dotée des mêmes droits d'accès qu'une classe (public ou droit de paquetage). Dans le cas où nous désirons avoir une interface publique :

Cette interface particulière possède une méthode. Certaines interfaces possèdent plusieurs méthodes. Les interfaces ne disposent jamais d'attributs (à part les constantes), et les méthodes ne sont jamais implémentées directement dans l'interface. La fourniture des attributs et des implémentations de méthodes sont pris en charge par la ou les classes qui implémentent l'interface.

Choix du chapitre Implémentation d'une interface

Pour qu'une classe implémente une interface et donc respecter le contrat (ou la promesse) prévu par cette dernière, vous devez exécuter deux étapes :

  1. déclarer que votre classe a l'intention d'implémenter l'interface donnée,
  2. fournir les définitions de toutes les méthodes de l'interface.

Pour déclarer qu'une classe implémente une interface, employez le mot réservé implements :

Alors, la classe A :

  1. dispose des constantes définies dans l'interface I.
  2. est contrainte de définir toutes les méthodes prototypées dans l'interface I ; plus exactement, si la classe A ne définit pas toutes les méthodes prototypées dans I, elle doit être déclarée abstraite et ne pourra donc pas être instanciée.

Pour en savoir plus sur les classes abstraites

Choix du chapitre Utilisation de l'interface

Sachant que la classe A implémente l'interface I, nous savons (si A n'est pas abstraite) qu'elle dispose de toutes les méthodes de cette interface I ; nous possedons donc un renseignement sur ce dont nous pouvons faire avec les instances de cette classe. Concrètement, en reprenant l'exemple de la classe FichePersonne et en utilisant l'interface Visualisation, nous pouvons écrire :

On peut donc définir des variables de type interface, ici de type Visualisation ; nous pouvons alors invoquer uniquement la méthode déclarée dans Visualisation sur un objet référencé par une variable de type Visualisation ou, si cela avait été le cas, utiliser les constantes définies dans l'interface. Cela a des conséquences fondamentales sur les possibilités de programmation.

  1. Les attributs (constants) et les méthodes d'une interface sont automatiquement publics ; cela implique qu'une classe qui implémente une interface devra déclarer publiques (avec le modificateur public) les méthodes de l'interface qu'elle définit.
  2. Si une classe A implémente une interface I, une sous-classe B de A implémente aussi automatiquement I ; une instance B pourra être référencée par une variable de type I et bénéficiera, au moins par héritage, des définitions des méthodes prototypées dans I.

Une même classe peut implémenter plusieurs interfaces :

Dans ce cas d'utilisation, les interfaces permettent de compenser, en grande partie, l'absence d'héritage multiple.

Choix du chapitre Propriétés des interfaces

Les interfaces ne sont pas des classes. En particulier, vous ne devez jamais utiliser l'opérateur new pour créer un objet de type interface :

x = new Visualisation(); // erreur

Cependant, bien que vous ne puissez pas construire des objets interface, vous pouvez toujours déclarer des variables interface :

Visualisation personne ;

Une variable interface doit faire référence à un objet d'une classe qui implémente l'interface :

personne = new FichePersonne("lagafe", "gaston", ... );

La variable interface ne peut ensuite utiliser que les méthodes prévues par le contrat, c'est-à-dire, déclarées dans l'interface :

personne.Affiche();

Choix du chapitre Variables de type interface et polymorphisme

Bien que la vocation d'une interface soit d'être implémentée par une classe, on peut définir des variables de type interface :

Bien entendu, on ne pourra pas affecter à i une référence à quelque chose de type I puisqu'on ne peut pas instancier une interface (pas plus qu'on ne pouvait instancier une classe abstraite !). En revanche, on pourra affecter à i n'importe quelle référence à un objet d'une classe implémentant l'interface I :

De plus, à travers i, on pourra manipuler des objets de classes quelconques, non nécessairement liées par héritage, pour peu que ces classes implémentent l'interface I.

Voici un exemple illustrant cet aspect. Une interface Affichable comporte une méthode affiche. Deux classes Entier et Flottant implémentent cette interface (aucun lien d'héritage n'apparaît ici). On crée un tableau hétérogène de références de type Affichable qu'on remplit en instanciant des objets de type Entier et Flottant.

Résultat :

Je suis un entier de valeur 25
Je suis un flottant de valeur 1.25
Je suis un entier de valeur 42

Cet exemple est restrictif puisqu'il peut se traiter avec une classe abstraite. Voyons maintenant ce que l'interface apporte de plus.

Choix du chapitre Un des principaux intérêts

Une interface permet de mettre en connexion plusieurs hiérarchies de classes, qui à priori n'ont aucuns lien communs entre elles, par l'intermédiaire de méthodes spécifiques déterminées par l'interface, comme ici la méthode affiche :

Choix du chapitre Interfaces et constantes

L'essentiel du concept d'interface réside dans les en-têtes de méthodes qui y figurent. Mais une interface peut aussi renfermer des constantes symboliques qui seront alors accessibles à toutes les classes implémentant l'interface :

Ces constantes sont automatiquement considérées comme si elles avaient été déclarées static et final. Il doit s'agir obligatoirement d'expressions constantes. Elles sont accessibles en dehors d'une classe implémentant l'interface. Par exemple, la constante MAXI de l'interface I se notera simplement I.MAXI.

Choix du chapitre Dérivation d'une interface

Tout comme vous pouvez construire des hiérarchies de classes, vous pouvez étendre des interfaces. Cela autorise plusieurs chaînes d'interfaces allant d'un plus large degré de généralité à un plus petit degré de spécialisation :

En fait, la définition de I2 est totalement équivalente à :

La dérivation des interfaces revient simplement à concaténer des déclarations.

Choix du chapitre Exemple de synthèse

Je vous propose un petit exemple de synthèse qui permet de stocker dans un même fichier des objets de classes totalement différentes.

Principal.java
import java.io.*;
//------------------------------------------------------------------------------
interface Enregistrement extends Serializable {
  void sauvegarder(ObjectOutputStream fichier);
  void afficher();
}
//------------------------------------------------------------------------------
class Personne implements Enregistrement {
  private String nom, prénom;

  public Personne(String nom, String prénom) {
    this.nom = nom;
    this.prénom = prénom;
  }
  public void sauvegarder(ObjectOutputStream fichier) {
    try {
      fichier.writeUTF(getClass().getName());
      fichier.writeObject(this);
    } 
    catch (IOException ex) { System.err.println("Erreur de fichier"); }
  }
  public void afficher() {
    System.out.println(nom);
    System.out.println(prénom);
  }
}
//------------------------------------------------------------------------------
class Notes implements Enregistrement {
  private double[] notes = new double[10];
  private int nombre = 0;

  public Notes ajoutNote(double note) {
    notes[nombre++] = note;
    return this;
  }
  public void sauvegarder(ObjectOutputStream fichier) {
    try {
      fichier.writeUTF(getClass().getName());
      fichier.writeObject(this);
    } 
    catch (IOException ex) { System.err.println("Erreur de fichier"); }
  }
  public void afficher() {
    for (int i = 0; i < nombre; i++)
      System.out.print(notes[i] + " ");
    System.out.println();
  }
}
//---------------------------------------------------------------------------------------------
public class Principal {
  public static void main(String[] args) {
    Personne personne = new Personne("nomUn", "prénomUn");
    Notes notes = new Notes();
    notes.ajoutNote(15.0).ajoutNote(8.0).ajoutNote(12.0);
    Enregistrement[] enregistrement = {personne, notes, new Personne("nomDeux", "prénomDeux")};
    try {
      ObjectOutputStream sortie = new ObjectOutputStream(new FileOutputStream("test.dat"));
      sortie.writeInt(enregistrement.length);
      for (int i = 0; i < enregistrement.length; i++) enregistrement[i].sauvegarder(sortie);
    } 
    catch (IOException ex) { System.err.println("Erreur création de fichier"); }
    Enregistrement[] restitution = null;
    try {
      ObjectInputStream entrée = new ObjectInputStream(new FileInputStream("test.dat"));
      restitution = new Enregistrement[entrée.readInt()];
      for (int i = 0; i < restitution.length; i++) {
        String classe = entrée.readUTF();
        if (classe.equals("Personne")) restitution[i] = (Personne) entrée.readObject();
        if (classe.equals("Notes")) restitution[i] = (Notes) entrée.readObject();
      }
    } 
    catch (IOException ex) { System.err.println("Erreur création de fichier"); } 
    catch (ClassNotFoundException ex) { System.err.println("Erreur lecture de l'objet"); }
    for (int i = 0; i < restitution.length; i++) restitution[i].afficher();
  }
}
//---------------------------------------------------------------------------------------------
Principal.java

 


Cet exercice consiste à créer une classe Cercle qui sera capable entre autre d'afficher un cercle de diamètre 100 aux coordonnées fixées au moment de la création ainsi qu'une classe Carré qui sera également capable d'afficher un carré de 100 pixels de côté. Ces deux classes hériterons d'une classe Forme qui possède les attributs correspondant aux coordonnées centrale de chaque type de figure ainsi qu'une méthode d'affichage vide. Vous allez rajouter une classe que vous nommerez Texte qui aura la particularité de stocker une chaîne de caractères de votre choix à une position bien déterminée.

Dans la classe Panneau (héritage de JPanel), vous allez créer un tableau d'éléments affichables, qui comportera un cercle, un carré, et un objet de la classe Texte. En fait, la première case du tableau correspond au cercle de coordonnées (70, 70), la deuxième case au carré de coordonnées (200, 70), et enfin la troisième le texte de bienvenue qui vous est proposé aux coordonnées (100, 150).

Lorsque vous proposerez l'affichage, il faudra lancer l'affichage d'un élément sans le connaître au préalable. Etant donné que la classe Texte n'a absolument rien à voir avec toute la hiérarchie de la classe Forme, il est nécessaire de passer par une interface que vous appellerez Présentation, et la connexion se fera par la méthode dessine. Le cas échéant, faites toutes les modifications qui vous semblent nécessaires pour avoir une écriture cohérente.