Les classes abstraites et les interfaces

Les classes abstraites :

Dans la classe Forme de l’exercice 3, on a créé une méthode aire() qui retournait systématiquement la valeur 0 car, dans cette classe, on n’avait pas assez d’informations pour calculer l’aire correctement. En programmation objet, on rendrait cette méthode virtuelle pure (dénomination C++) ou abstraite (dénomination Java). Une méthode abstraite implique que sa classe l’est également. Cela se traduit en Java de la manière suivante :

Forme.java
public abstract class Forme {
  double x;
  double y;

  public Forme(double x, double y) {
    this.x = x;
    this.y = y;
  }

  public abstract double aire();

  public double distanceOrigine() {
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }
}

On remarque que, dans une classe abstraite, certaines méthodes ne sont pas abstraites. De plus, la classe peut contenir des attributs. En revanche, comme vous l’avez vu l’année dernière, même si elle contient un constructeur, on ne peut pas en créer une instance directement : seule une classe qui en descend et qui implémente toutes les méthodes abstraites peut le faire.

Cercle.java
public class Cercle extends Forme {
  double rayon;

  public Cercle(double x, double y, double rayon) {
    super(x, y);
    this.rayon = rayon;
  }

  @Override
  public double aire() {
    return Math.PI * this.rayon * this.rayon;
  }

  public static void main(String[] args) {
    var c = new Cercle(3,4, 2);
    System.out.println("aire = " + c.aire());
    System.out.println("distance à l'origine = " + c.distanceOrigine());
  }
}

Les interfaces :

Une interface Java est assez proche, dans l’esprit, d’une classe abstraite mais elle diffère en essence du fait qu’aucune méthode (non-statique) n’est implantée et qu’aucun attribut d’instance « non constant » n’est toléré. Pour être tout à fait précis, voici les différences:

Classe abstraite Interface
Les classes abstraites peuvent avoir des méthodes abstraites et non abstraites (implantées) Les interfaces ont seulement des méthodes abstraites et, éventuellement, des méthodes *statiques* non abstraites
Une classe abstraite ne peut hériter que d'une autre classe. En revanche, elle peut implémenter plusieurs interfaces. Une interface ne peut pas hériter d'une classe. Mais elle peut hériter (via le mot-clef `extends`) de plusieurs interfaces.
Une classe abstraite peut contenir des attributs `final`, non `final`, `static` et non `static`. Une interface peut posséder seulement des attributs `static` et `final`.
Une classe abstraite peut être déclarée "protected", "private-package", etc. Elle peut contenir des méthodes "protected", "private", etc. Une interface est *implicitement* `public` et ses méthodes sont déclarées `public` (cela simplifie le langage).

Une classe souhaitant exploiter une interface doit le signaler via le mot-clef implements:

Forme.java
interface Forme {
  static final double x_origine = 1.0;  // on déplace l'origine
  static final double y_origine = 2.0;  // des axes

  abstract double aire();

  abstract double distanceOrigine();
}
Cercle.java
public class Cercle implements Forme {
  double x,y;
  double rayon;

  public Cercle(double x, double y, double rayon) {
    this.x = x;
    this.y = y;
    this.rayon = rayon;
  }

  public double aire() {
    return Math.PI * this.rayon * this.rayon;
  }

  public double distanceOrigine() {
    double xx = this.x - Forme.x_origine;
    double yy = this.y - Forme.y_origine;
    return Math.sqrt(xx * xx + yy * yy);
  }

  public static void main(String[] args) {
    var c = new Cercle(3,4, 2);
    System.out.println("aire = " + c.aire());
    System.out.println("distance à l'origine = " + c.distanceOrigine());
  }
}

Exercice 1 : Jeu de rôle polymorphique   

Une classe Jeu contient un Joueur et un ensemble d'Orc, de Gobelin et de Elf. Tous ces personnages ont une position (x,y) dans le plan 2D.

Le joueur est proche d’un ennemi si la distance les séparant est :

Créez les différentes classes. Notamment, créez une méthode estProche() de la classe Joueur qui renvoie un booléen vérifiant les règles ci-dessus. Attention : on souhaite que le code soit maintenable et extensible facilement (on peut rajouter de nouveaux types d’ennemis sans avoir à modifier tout le code).

Rajoutez une méthode aUnEnnemiProche() qui indique si l’un des ennemis est proche.

Indice 1 

Pensez héritage et polymorphisme.

Les exceptions

La gestion des exceptions est identique en Java et en C++. Cela se fait via des try/catch :

try { .... }
catch(MaBelleException e) { ... }
catch(MaSuperException1 e) { ... }
catch(Exception e) {}

Les catch sont examinés dans l’ordre où ils sont écrits. Donc, les exceptions les plus spécifiques doivent être en premier et les plus générales en dernier. Notamment, comme toutes les exceptions héritent de la classe Exception, si vous utilisez celle-ci, elle doit être indiquée en dernier.

Pour lever une exception, il faut utiliser le mot-clef throw. Il est de bon ton de spécifier, dans le prototype de la méthode qui lève l’exception qu’elle peut effectivement lever l’exception. Cela permet, notamment pour les utilisateurs de votre méthode, de savoir quelles exceptions ils auront éventuellement à catcher.

public void maMethode() throws IllegalArgumentException, ArithmeticException {
  .....
  throw new IllegalArgumentException("message d'erreur");
  .....
  throw new ArithmeticException("message d'erreur");
}

Il est à noter que, dans certaines situations, le compilateur vous obligera à écrire des try/catch. C’est notamment le cas lorsque vous manipulez des fichiers. Le code ci-dessous lit un fichier et le new Scanner(fic) impose le try/catch (si vous ne l’indiquez pas, votre programme ne compilera pas).

Fichier.java
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class Fichier {
  public static void litFichier(String filename)  {
    File file = new File(filename);

    try { // imposé par le langage Java
      // on crée un scanner pour lire le fichier. C'est lui qui
      // va réellement ouvrir le fichier en lecture
      Scanner scanner = new Scanner(file);
      while (scanner.hasNext()) {
        System.out.println(scanner.nextDouble());
      }
      scanner.close(); // ne pas oublier de fermer le fichier
    }
    catch(FileNotFoundException e) { // imposé par le langage Java
      e.printStackTrace(); // affiche l'erreur
    }
  }

  public static void main(String[] args) {
    Fichier.litFichier("toto.txt");
  }
}

Dans le code ci-dessus, il ne faut pas oublier de fermer le fichier. Java propose une alternative dans la syntaxe try {} / catch {} qui ferme automatiquement le fichier :

try (  // Notez les parenthèses. C'est ce qui permet d'éviter le close()
  Scanner scanner = new Scanner(file);
) {
  while (scanner.hasNext()) {
    System.out.println(scanner.nextDouble());
  }
}
catch(FileNotFoundException e) {
  e.printStackTrace(); // affiche l'erreur
}

Enfin, notez qu’une exception n’est autre qu’une classe qui hérite d’une autre classe d’exception. Pour en créer une, il suffit donc :

MonException.java
package fr.polytech.aco;

public class MonException extends Exception{
  public MonException(String message){
    super(message);
  }
}

On peut alors utiliser MonException comme n’importe quelle autre exception.

La généricité

La généricité est le dernier mécanisme qui permet d’obtenir du polymorphisme. La problématique est la suivante : on souhaite écrire un code qui est exactement le même pour différents types de paramètres et on souhaite éviter de dupliquer ce code. Par exemple, en C, on pourrait avoir :

int somme(int x, int y) { return x + y; }
float somme(float x, float y) { return x + y; }
double somme(double x, double y) { return x + y; }

Ce type de code est à proscrire, d’abord parce que c’est fastidieux de recopier de nombreuses fois le même code, mais, plus important, parce que ce code n’est pas maintenable : si on s’aperçoit que la formule x + y est erronée, il faut corriger plein de fonctions/méthodes différentes, avec la possibilité d’en oublier une et d’introduire des bugs dans le programme.

La généricité permet de pallier ce problème. En C++, on parle de templates. On déclare donc un code générique, puis on l’utilise en instanciant le type qui nous intéresse :

// déclaration de la fonction somme
template <typename T>
T somme(const T& x, const T& y) { return x+y; }

// exploitation du template
std::cout << somme<int>(3,4) << std::endl;
std::cout << somme<double>(3.4, 4.5) << std::endl;

En Java, on parle plutôt de classe générique mais l’idée est la même. On n’indique pas le mot-clef template mais on met entre <> les types dont dépend la classe générique.

Tableau.java
 1import fr.polytech.aco.MonException;
 2
 3public class Tableau<T> {
 4  private T[] tableau;
 5  private int nbElts = 0;
 6
 7  public Tableau(int size) {
 8    this.tableau = (T[]) new Object[size];
 9  }
10
11  public void pushBack(T x) throws MonException {
12    if (nbElts == tableau.length) {
13      throw new MonException("tableau plein");
14    }
15    tableau[nbElts] = x;
16    nbElts++;
17  }
18
19  public void printTableau() {
20    for(int i = 0; i < nbElts; i++) {
21      System.out.println(tableau[i]);
22    }
23  }
24
25  public static void main(String[] args) {
26    Tableau<String> tableau = new Tableau<>(10);
27    try {
28      tableau.pushBack("aaa");
29      tableau.pushBack("bbb");
30      tableau.printTableau();
31    }
32    catch (MonException e) {
33      e.printStackTrace();
34    }
35  }
36}

Sur la ligne 26, le new n’est pas obligé de spécifier le type T du tableau car on l’a déjà indiqué avant l’opérateur d’affectation. Si on utilise le mot-clef var, il faut spécifier T dans le new :

var tableau = new Tableau<String>(10);

Attention: contrairement au C++ dans lequel le type T peut être n’importe quoi, en Java, T doit impérativement être un type référence.

On peut également spécifier des contraintes sur les types possibles :

Tableau.java
public class Tableau<T extends Number> {
}

Dans le code ci-dessus, seuls des tableaux dont les types des éléments héritent de Number sont admissibles.

Exercice 2 : MyArray générique   

Reprenez le code de votre classe MyArray de la séance 2 et transformez le de telle sorte qu’elle puisse contenir des objets de n’importe quel type référence.

Exercice 3 : MyArray et MyList...   

Votre classe MyArray permet de stocker des objets de n’importe quel type, d’en ajouter via les méthodes pushBack() et pushFront(). Rajoutez une méthode printAll() qui permet d’afficher les éléments contenus dans MyArray.

J’ai, de mon côté une classe MyList qui permet également de stocker des objets de n’importe quel type (sous forme d’une liste), d’en ajouter via les méthodes pushBack() et pushFront(), et d’afficher le contenu de la liste via la méthode printAll().

Dans la classe Main qui contient le main(), on souhaite créer une unique méthode statique test() qui prend soit un de vos tableaux d'Integer soit une de mes listes d'Integer. Modifiez votre classe MyArray pour que ce soit possible et implantez la méthode test().

Indice 1 

Les deux classes MyArray et MyList ont des méthodes ayant les mêmes signatures…

Indice 2 

Les interfaces indiquent les signatures des méthodes…

Indice 3 

Les types des paramètres des méthodes ne sont pas forcément des types primitifs ou des classes, ce peut être également des interfaces.

 
© C.G. 2007 - 2024