Classes internes et classes anonymes

Chapitres traités   

Une classe interne est une classe qui est définie à l'intérieur d'une autre classe. Cette particularité présente de nombreux avantages que nous allons étudier ensemble.

Java permet de définir ponctuellement une classe, sans lui donner de nom. Cette particularité a été introduite par la version 1.1 pour faciliter la gestion des événements. Nous la présentons succinctement ici, en dehors de ce contexte.

Choix du chapitre Les classes internes

Une classe est dite interne lorsque sa définition est située à l'intérieur de la définition d'une autre classe. Trois raisons justifient l'emploi de classes internes :

  1. Les méthodes des classes internes peuvent accéder aux attributs de la classe conteneur, à partir de l'envergure où elles sont définies, y compris les attributs qui pourrait être des données privées.
  2. Les classes internes peuvent être cachées aux autres classes du même paquetage, ce qui offre une meilleure protection sur des données sensibles.
  3. Les classes internes anonymes sont utiles pour définir des callbacks (des appels de méthode automatique) sans écrire beaucoup de code.

Accéder à l'état d'un objet à l'aide d'une classe interne

La grande particularité d'une classe interne, c'est qu'une de ses méthodes a accès à la fois à ses propres attributs et à ceux de l'objet externe. Ainsi, dans le code suivant, la méthode actionPerformed() qui est une méthode intégrée à la classe interne Minuteur, peut tout de même agir sur l'objet heure, qui est normalement un attribut de la classe externe Fenêtre :

package horloge;

import java.awt.*;
import java.awt.event.*;
import java.text.DateFormat;
import java.util.Date;
import javax.swing.*;

public class Fenêtre extends JFrame {
   private JLabel heure = new JLabel();
   
   public Fenêtre() {
      super("Horloge");
      setBounds(100, 100, 180, 80);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      heure.setFont(new Font("Arial", Font.BOLD+Font.ITALIC, 32));
      heure.setHorizontalAlignment(JLabel.CENTER);
      add(heure);      
      setResizable(false);
      setVisible(true);
      new Minuteur();
   }
   
   public static void main(String[] args) {
      new Fenêtre();
   }
   
   private class Minuteur implements ActionListener {
      private Timer minuteur = new Timer(1000, this);
      public Minuteur() {
         minuteur.start();
      }
      public void actionPerformed(ActionEvent e) {
         heure.setText(DateFormat.getTimeInstance(DateFormat.MEDIUM).format(new Date()));
      }
   }
}

La classe Minuteur est une classe interne, qui plus est, privée. Seule les classes internes peuvent être privées. Il est donc impossible à l'extérieur de la classe Fenêtre d'atteindre la classe Minuteur et d'en créer une instance. Seule la classe externe conteneur a le droit de le faire, et utilise ainsi la classe interne à bon escient. Nous sommes isi en présence d'un mécanisme hautement sécurisé.

Lien entre objet interne et objet externe

Un objet de classe interne jouit de trois propriétés particulières :

  1. Un objet d'une classe interne est toujours associé, au moment de son instanciation, à un objet de la classe externe dont on dit qu'il lui a donné naissance. Ainsi, dans le codage ci-dessus, l'objet de type Minuteur sera toujours associé à un objet de type Fenêtre.
  2. Comme nous l'avons vu, un objet d'une classe interne a toujours accès aux attributs et aux méthodes (même privés) de l'objet externe lui ayant donné naissance (attention : ici, il s'agit bien d'un accès restreint à l'objet, et non à tous les objets de cette classe).
  3. Un objet de classe externe a toujours accès aux attributs et aux méthodes (même privés) d'un objet d'une classe interne auquel il a donné naissance.

Le premier point n'apporte rien de nouveau par rapport à la situation d'objets membres, il n'en va pas de même pour les deux autres points qui permettent d'établir une communication privilégiée entre objet externe et objet interne.

Règles particulières de syntaxe pour les classes internes

C'est assez rare, mais il est possible que votre classe interne possède un attribut qui porte le même nom qu'un attribut de la classe externe. Du coup, comment atteindre cet attribut externe depuis la classe interne ? La réponse est simple. Il suffit de faire référence à this de la classe voulue par la syntaxe générale suivante :

ClasseExterne.this

En reprenant l'exemple précédent, voici comment nous pouvons également atteindre l'objet heure par cette technique :

Fenêtre.this.heure.setText(DateFormat.getTimeInstance(DateFormat.MEDIUM).format(new Date()));

Si la classe interne n'est pas privée, vous pouvez y faire référence de l'extérieur à l'aide de la syntaxe suivante :

ClasseExterne.ClasseInterne

Classes internes locales

Pour protéger encore plus votre code source, et lorsque vous avez besoin d'une seule instance de la classe interne, il est également possible de définir les classes localement à l'intérieur d'une seule méthode.

package horloge;

import java.awt.*;
import java.awt.event.*;
import java.text.DateFormat;
import java.util.Date;
import javax.swing.*;

public class Fenêtre extends JFrame {
   private JLabel heure = new JLabel();
   
   public Fenêtre() {
      super("Horloge");
      setBounds(100, 100, 180, 80);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      heure.setFont(new Font("Arial", Font.BOLD+Font.ITALIC, 32));
      heure.setHorizontalAlignment(JLabel.CENTER);
      add(heure);      
      setResizable(false);
      lancerMinuteur(true);
      setVisible(true);    
   }
   
   public static void main(String[] args) {
      new Fenêtre();      
   }
   
   private void lancerMinuteur(final boolean beep) {
      class Minuteur implements ActionListener {
         private Timer minuteur = new Timer(1000, this);
         public Minuteur() {
            minuteur.start();
         }
         public void actionPerformed(ActionEvent e) {
            heure.setText(DateFormat.getTimeInstance(DateFormat.MEDIUM).format(new Date()));
            if (beep) Toolkit.getDefaultToolkit().beep();
         }
      } 
      new Minuteur();
   }
}

Voici quelques remarques importantes sur les classes internes locales :

  1. Les classes locales ne sont jamais déclarées avec un spécificateur d'accès (c'est-à-dire public ou private). Leur portée est toujours restreinte au bloc dans lequel elles sont déclarées.
  2. Les classes locales présente l'immense avantage d'être complètement cachées au monde extérieur, et même au reste du code de la classe Fenêtre. A part lancerMinuteur(), aucune méthode ne connaît l'existence de la classe Minuteur.
  3. Les classes locales ont un autre avantage sur les autres classes internes. Elles peuvent non seulement accéder aux attributs de leurs classes externes, mais également aux variables locales ! Ces variables locales doivent toutefois être déclarées final.

Le mot clé final peut être appliqué aux variables locales, aux variables d'instance et aux variables statiques. Dans tous les cas, cela signifie la même chose : cette variable ne peut être affectée qu'une seule fois après sa création. Il n'est pas possible d'en modifier ultérieurement la valeur - elle est définitive.

Classes internes anonymes

Lors de l'utilisation de classes internes locales, vous pouvez souvent aller plus loin. Si vous ne désirez créer qu'un seul objet de cette classe, il n'est même pas nécessaire de donner un nom à cette classe. Une telle classe est appelée classe interne anonyme :

private void lancerMinuteur(final boolean beep) {
   ActionListener écouteur = new ActionListener() {
     public void actionPerformed(ActionEvent e) {
        heure.setText(DateFormat.getTimeInstance(DateFormat.MEDIUM).format(new Date()));
        if (beep) Toolkit.getDefaultToolkit().beep();
     }
   }; 
   new Timer(1000, écouteur).start();
}

Ainsi, dans la méthode lancerMinuteur(), nous créons un objet écouteur d'une classe qui implémente l'interface ActionListener, où la méthode actionPerformed() requise est celle définie entre accolades { }.

Voici les différents critères requis pour construire une classe anonyme :

  1. Tous les paramètres employés pour construire l'objet sont donnés entre les parenthèses ( ) qui suivent le nom du supertype. En général, la syntaxe est :

    new SuperType(paramètres de construction)
    {
      méthodes et attributs de la classe interne
    }
    SuperType peut être ici une interface telle que ActionListener ; la classe interne implémente alors cette interface. SuperType peut également être une classe, et dans ce cas la classe interne étend cette classe.
  2. Une classe interne anonyme ne peut avoir de constructeurs, car le nom d'un constructeur doit être identique à celui de la classe (et celle-ci n'a pas de nom). Au lieu de cela, les paramètres de construction sont donnés au constructeur de la superclasse. En particulier, chaque fois qu'une classe interne implémente une interface, elle ne peut pas avoir de paramètres de construction. Il faut néanmoins toujours fournir les parenthèses, comme dans cet exemple :

    new TypeInterface() { méthodes et attributs de la classe interne }

A titre d'exemple, je vous propose de modifier la méthode lancerMinuteur() en créant cette fois-ci le Timer dans la classe anonyme. Pour que ce dernier puisse être lancé, je prévoie alors un bloc d'initialisation à la place du constructeur puisqu'il nous est interdit d'en placer un :

private void lancerMinuteur(final boolean beep) {
  new ActionListener() {
     private Timer minuteur = new Timer(1000, this);
     { minuteur.start(); }
     public void actionPerformed(ActionEvent e) {
        heure.setText(DateFormat.getTimeInstance(DateFormat.MEDIUM).format(new Date()));
        if (beep) Toolkit.getDefaultToolkit().beep();
     }
  }; 
}

Après cette première approche, je vous porpose de passer au chapitre suivant qui est entièrement consacré aux classes anonymes.
.

 

Choix du chapitre Les classes anonymes

Je le rappelle, une classe anonyme est une classe locale (interne) sans nom.

Une classe anonyme est définie et instanciée dans une unique expression concise en utilisant l'opérateur new.


Alors qu'une définition de classe locale est une instruction dans un bloc de code Java, une définition de classe anonyme est une expression, ce qui signifie qu'elle peut être incluse dans une expression plus grande comme un appel de méthode.


Lorsqu'une classe locale n'est utilisée qu'une seule fois, envisagez d'utiliser la syntaxe des classes anonymes qui place la définition et l'utilisation de la classe au même endroit.

Comme vous pouvez le constater, la syntaxe de définition d'une classe anonyme et de création de cette classe utilise le mot clé new, suivi du nom de la classe et d'une définition d'un corps entre accolades { }. Si le nom suivant le mot clé new est le nom d'une classe, la classe anonyme devient une sous-classe de la classe nommée. Si le nom suivant new spécifie une interface, alors la classe anonyme implémente cette interface et devient une sous-classe de Object.

Exemple de classe anonyme

Supposons que l'on dispose d'une classe A.

Il est possible de créer un objet d'une classe dérivée de A, en utilisant une syntaxe de cette forme :



Tout se passe comme si nous avions procédé ainsi :



Cependant, dans ce dernier cas, il serait possible de définir des références de type A1, alors que c'est impossible dans le premier cas.

Voici un petit programme illustrant cette possibilité. La classe A y est réduite à une seule méthode affiche(). Nous créons une classe anonyme, dérivée de A, qui redéfinit la méthode affiche().



Je suis un anonyme dérivé de A.

Notez bien que si A n'avait pas comporté de méthode affiche(), l'appel a.affiche() aurait été incorrect, compte tenu du type de la référence a (revoyez éventuellement les règles relatives au polymorphisme). Cela montre qu'une classe anonyme ne peut pas introduire de nouvelles méthodes ; notez à ce propos que même un appel de cette forme serait incorrect :


Les classes anonymes d'une manière générale

Il s'agit de classes dérivées ou implémentant une interface.
.

La syntaxe de définition d'une classe anonyme ne s'applique que dans deux cas :

  1. Classe anonyme dérivée d'une autre (comme dans l'exemple précédent),
  2. Classe anonyme implémentant une interface.

Voici un exemple simple de la deuxième situation :



Je suis un anonyme implementant Affichable.

Utilisation de la référence à une classe anonyme

Dans les précédents exemples, la référence de la classe anonyme était conservée dans une variable (d'un type de base ou d'un type interface).

Nous pouvons aussi la transmettre en argument d'une méthode ou en valeur de retour :


L'utilisation de classes anonymes conduit généralement à des codes peu lisibles. Nous les réserverons à des cas très particuliers où la définition de la classe anonyme reste brève. Les classes anonymes sont particulièrement intéressantes dans la définition de classes écouteurs d'événements. En voici d'ailleurs un exemple, traité d'abord sans classe anonyme, puis réalisé sous forme de classe anonyme :