Interfaces et Classes Abstraites


Interfaces

Héritage et Sous-Type

On a vu l’intérêt de l’héritage et du polymorphisme pour le sous-typage.


Animal a1 = Math.random() < 0.5 ? new Dog() : new Cat();
a1.speak();
                      

Ce qui permet l’utilisation de variables de type générique, pouvant stocker des objets qu’on utilise quelque soit leur type.

Ceci est rendu possible par la redéfinition des méthodes et le polymorphisme.

Héritage et Sous-Type

C’est parfois l’utilité principale de l’héritage, plus que la “réutilisation du code de la classe de base”.

Héritage et Sous-Type

C’est tellement utile qu’on aimerais parfois qu’un objet hérite de plusieurs classe, afin de lui associé plusieurs types.

Impossible car on ne peut hériter que d’une classe.

Interfaces

Les interfaces permettent de créer des sous-types mais sans créer de classe de base.

On à la même relation “EST UN” entre un objet et une interface.

Par exemple on pourrait dire “Un objet est Inflammable s’il propose une méthode enflammer()”

Les objets inflammables n’ont pas vraiment de choses en commun que l’on pourrait centraliser dans une classe Inflammable. Il n’hérite pas de propriété avec cette relation (à part l’existence de cette fonction).

Interfaces

Une interface est une liste de noms de méthodes (uniquement les signatures des méthodes).

Une interface est un prototype de classe. Elle définit la signature des méthodes qui doivent être implémentées dans les classes construites à partir de ce prototype.

Interfaces

On dit qu’une classe implémente une interface, si elle définit les méthodes de l’interface.

En java on déclare qu’une classe implémente une interface avec le mot clé implements.

Une interface définit un type (comme une classe) et les classes qui implémentent cette interface sont donc des sous-types.

Exemple


interface Inflammable {
    void enflammer();
}

class Bois implements Inflammable {
	public void enflammer() {
		System.out.println("Je brule et fais des braises");
	}
}

class Dancefloor  implements Inflammable {
	public void enflammer() {
		System.out.println("♪ ♫ Youhouhou ♬ ♫ ");
	}
}

public class Main {
    static public void main(String[] args) {
        Inflammable[] tab = { new Bois(), new Dancefloor() };

        for(Inflammable i : tab)
        	i.enflammer();
    }
}
    

Le mot clé public est implicite dans une interface.

Run it!

Interfaces

Avantage :

  • Une classe peut implémenter plusieurs interfaces.
  • On a tous les avantages du sous-typage comme avec l’héritage classique (notamment le polymorphisme).

 

Exemple 2


public interface Comparable {
    boolean greaterThan(Object o);
}
class Maximize {
    static public Comparable max(Comparable a, Comparable b) {
        if(a.greaterThan(b)) {
            return a;
        }
        return b;
    }
}

class Person implements Comparable {
    private int size;
    Person(int size) { this.size = size; }
    @Override
    public String toString() { return "size: "+size; }
    public boolean greaterThan(Object o) {
        return this.size > ((Person)o).size;
    }
}
public class Main {
    static public void main(String[] args) {
        Person p1 = new Person(157);
        Person p2 = new Person(173);
        System.out.println(Maximize.max(p1, p2));
    }
}

run it!

Interface

Une interface peut remplacer une classe pour déclarer :

  • un attribut
  • une variable
  • un paramètre
  • une valeur de retour

 

Attention, on ne peut pas instancier une interface.


      Comparable c = new Comparable(); // NON

Exemple 3


    interface Comparable {
    boolean greaterThan(Object o);
}
class Maximize {
    static public Comparable max(Comparable a, Comparable b) {
        if(a.greaterThan(b)) {
            return a;
        }
        return b;
    }
}

class ListElements {
	Comparable[] tab;
	int nbElements = 0;

	public ListElements(int maxSize) {
		tab = new Comparable[maxSize];
	}

	public void add(Comparable e) {
		tab[nbElements] = e;
		nbElements++ ;
	}
	public boolean isIncreasing() {
		for (int i = 1; i < nbElements ; i++) {
			if (tab[i-1].greaterThan(tab[i])) return false;
		}
	    return true;
	}
}

class Person implements Comparable {
    private int size;
    Person(int size) { this.size = size; }
    @Override
    public String toString() { return "size: "+size; }
    public boolean greaterThan(Object o) {
        return this.size > ((Person)o).size;
    }
}
public class Main {
    static public void main(String[] args) {
        Person p1 = new Person(157);
        Person p2 = new Person(173);
        Person p3 = new Person(175);
        ListElements l = new ListElements(10);
        l.add(p1);
        l.add(p2);
        l.add(p3);
        System.out.println(l.isIncreasing());
        l.add(p2);
        System.out.println(l.isIncreasing());
    }
}

run it

Exemple 4 – Java 8 Interfaces


public interface InterfaceA {
    public default void foo() {
        System.out.println("A -> foo()");
    }
}
 
public interface InterfaceB {
    public default void foo1() {
        System.out.println("B -> foo()");
    }
}
 
private class Test implements InterfaceA, InterfaceB { 
.....
}

Les méthodes “foo” et “foo1” par défaut sont appelables dans la classe Test.

Exemple 4 – Java 8 Interfaces


public interface InterfaceA {
    public default void foo() {
        System.out.println("A -> foo()");
    }
}
 
public interface InterfaceB {
    public default void foo() {
        System.out.println("B -> foo()");
    }
}
 
private class Test implements InterfaceA, InterfaceB {
    // Erreur de compilation : "class Test inherits unrelated defaults for foo() from types InterfaceA and InterfaceB"
}

Exemple 4 – Java 8 Interfaces – Solution


   public class Test implements InterfaceA, InterfaceB {
     public void foo() {
        System.out.println("Test -> foo()");
    }
}

La méthode “foo” par défaut n’est plus appelable directement. L’appel de cette méthode se réalise en utilisant Interface.super.méthode.


public class Test implements InterfaceA, InterfaceB {
     public void foo() {
        InterfaceB.super.foo();
    }
}

Méthodes et Classes Abstraites

Méthodes Abstraites

Lorsqu’on crée une hiérarchie de classes (pensez aux animaux), il y a certaines méthodes difficiles à définir dans les classes trop générales.

Dans la classe Animal, par exemple, comment définir la méthode manger.


public class Animal {
     ...
     public void manger(Nourriture n) {
         // Que faire?
     }
}                        

Méthodes Abstraites

Dans ce cas, on peut définir une méthode abstraite avec le mot clé abstract.


public abstract class Animal {
     ...
     public abstract void manger(Nourriture n);
     // pas de code associé
}                        

Une méthode abstraite doit obligatoirement être redéfinie dans les classes dérivées.

c’est exactement comme pour les méthodes définies par une interface. Les méthodes d’une interface sont d’ailleurs aussi appelées “méthodes abstraites”.

Classe Abstraites

Une classe qui contient une méthode abstraite, est aussi abstraite, et doit être déclarée comme telle.


public abstract class Animal {
     ...
     public abstract void manger(Nourriture n);
     // pas de code associé
}                        

Comme pour les interfaces, on ne peut pas instancier une classe abstraite

Classe Abstraite


public class Lion extends Mammifère {
    ...
    @Override
    public void manger(Nourriture n) {
        // code spécifique aux lions
    }
}
                        

Classe Abstraite, Remarques

A la différence d’une interface, une classe abstraite peut contenir des méthodes concrètes (non-abstraites).

Une classe abstraite peut ne pas contenir de méthodes abstraites.

Instanciation et Spécialisation

Comme avec une interface, une classe abstraite constitue un type à part entière, mais qui ne peut pas être instanciée :


Animal unAnimal; // OK
unAnimal = new Animal (...); // ERREUR
                        

Une sous-classe d’une classe abstraite peut :

  • implémenter toutes les méthodes abstraites. Elle pourra alors être déclarée comme concrète et donc instanciée.
  • ne pas implémenter toutes ces méthodes abstraite. Elle reste alors nécessairement abstraite et ne pourra être instanciée.
  • ajouter d’autre(s) méthode(s) abstraite(s). Elle reste alors nécessairement abstraite et ne pourra être instanciée.

 

Classe Abstraite

Les classes abstraites sont entre les classes concrètes et les interfaces.

On les utilise :

  • lorsqu’on a besoin de l’héritage (réutilisation du code ; méthodes concrètes dans la classe de base qui fonctionnent de la même manière quelque soit les classes dérivées).
  • mais que certaines méthodes n’ont pas de sense à être définie

 

Exemple de Classe Abstraite

Exemple

Problème : faire un système permettant de représenter des formes géométriques dans un terminal. Toutes les formes géométriques doivent avoir une couleur de fond, une couleur de bordure et une largeur de bordure.

Solution : Définir une classe Shape qui permet des gérer tous ce que les figures ont en commun. Shape doit avoir une méthode abstraite contains(int x, int y) pour savoir si un pixel appartient à la figure.

La Classe Shape


public abstract class Shape {
    private char fillColor, strokeColor;
    private int strokeWidth;


    public Shape() {

    }
    public Shape(char fillColor, char strokeColor, int strokeWidth) {
        this.fillColor = fillColor;
        this.strokeColor = strokeColor;
        this.strokeWidth = strokeWidth;
    }

    public void setFillColor(char b) { fillColor = b; }
    public void setStrokeColor(char b) { strokeColor = b; }
    public void setStrokeWidth(int w) { strokeWidth = w; }

    public char getFillColor() { return fillColor; }
    public char getStrokeColor() { return strokeColor; }
    public int getStrokeWidth() { return strokeWidth; }

    abstract public boolean contains(int x, int y);

    public boolean onStroke(int x, int y) {
        if(contains(x, y)) return false;

        for(int dx = -strokeWidth; dx <= strokeWidth; ++dx) {
            for(int dy = -strokeWidth; dy <= strokeWidth; ++dy) {
                if(contains(x + dx, y + dy)) return true;
            }
        }
        return false;
    }
    @Override
    public String toString() { return "Shape"; }
}
                        

La Classe Rectangle



public class Rectangle extends Shape {
    private int x, y, w, h;
    public Rectangle(int x, int y, int w, int h) {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
    }
    @Override
    public boolean contains(int x, int y) {
        return x >= this.x && x < this.x + this.w &&
               y >= this.y && y < this.y + this.h;
    }
}

                        

La Classe Circle


public class Circle extends Shape {
    private int x, y, radius;
    public Circle(int x, int y, int radius) {
        this.x = x;
        this.y = y;
        this.radius = radius;
    }
    @Override
    public boolean contains(int x, int y) {
        return (x-this.x)*(x-this.x) + (y - this.y)*(y - this.y) < radius*radius;
    }
}
                        

Exemples de Hiérarchies

Problème de Figures avec Aires

Problème : on veut créer un programme qui calcule les aires d’un ensemble de figures. On veut pouvoir créer de nouvelles classes représentant des figures données. On veut donc que chaque figure possède une méthode getArea() qui retourne cette aire.

Solution : ici, inutile de créer une classe Shape car les figures n’auront rien en commun à part une méthode getArea(). Une interface Measurable par exemple est suffisante.

Classe et Interface

Parfois il est intéressant de proposer à la fois une classe abstraite et une interface pour modéliser un comportement.

On le retrouve souvent dans l’API standart de Java. Runnable Collection

Héritage et Composition

Parfois il est aussi préférable d’utiliser la composition. (exemple du copieur/imprimante/scanner).

Héritage et Composition

Supposons qu’on veut enrichir une classe ListProduct (qui stocke des produits), afin qu’elle garde en mémoire la somme des prix des produits stockés.

On suppose que cette classe est proposée par une bibliothèque et qu’on n’a pas accès au code source.


class Product {
	public int getPrice() { return 1; }
}
class ListProduct {
    ...
    public ListProduct() {
        ...
    }
    public void addAll(Product[] tab) {
        ...
    }
    public void add(Product e) {
    ...
    }
}
class AwesomeListProduct extends ListProduct {

    private int totalPrice;
    public void addAll(Product[] tab) {
        super.addAll(tab);

        for(Product p : tab) {
            totalPrice += p.getPrice();
        }
    }
    public void add(Product p) {
        super.add(p);
        totalPrice += p.getPrice();
    }
    public int getTotalPrice() { return totalPrice; }
}
                        

Héritage et Composition


class Main {
  public static void main(String[] args) {
  	AwesomeListProduct list = new AwesomeListProduct();

  	Product[] tab = {new Product(), new Product()};
  	list.addAll(tab);

    System.out.println(list.getTotalPrice());

  }
}
                        

run it!

Qu’affiche se programme?


2
                        

Héritage et Composition

En effet on ne sais pas comment est implémentée la méthode addAll. Dans ce cas, elle appelle la méthode add().


class ListProduct {
    private Product[] products;
    private int nbProduct;
    public ListProduct() {
        products = new Product[10];
    }
    public void addAll(Product[] tab) {
        for(Product e : tab) {
            add(e);
        }
    }
    public void add(Product e) {
        products[nbProduct] = e;
        nbProduct++;
    }
}
                        

Héritage et Composition

En voyant ça, on peut se dire qu’on n’a juste à enlever la redéfinition de la méthode addAll()

Cependant, notre classe va alors dépendre de la manière dont la classe ListProduct est implémentée.

Si la librairie modifie l’implémentation de ces méthodes, par exemple pour optimiser la méthode addAll(). Notre classe pourrait ne plus fonctionner.

Héritage et Composition

Il ne faut pas que notre programme dépendent de l’implémentation interne des libraries utilisées.

Si possible les différentes parties de notre programme doivent aussi être indépendantes. Il faut éviter les dépendances sur l’implémentation entre composants.

Héritage et Composition

Comment faire alors?

Dans ce cas on peut utiliser la composition.

Héritage et Composition


class AwesomeListProduct {
    private ListProduct listProduct;
    private int totalPrice;
    public AwesomeListProduct() {
        listProduct = new ListProduct();
    }
    public void addAll(Product[] tab) {
        listProduct.addAll(tab);

        for(Product p : tab) {
            totalPrice += p.getPrice();
        }
    }
    public void add(Product p) {
        listProduct.add(p);
        totalPrice += p.getPrice();
    }
    public int getTotalPrice() { return totalPrice; }
}
                        

Enumérations

Utilité

Prenons l’exemple du jeu du robot. Il serait intéressant d’avoir une fonction qui permet de connaitre la direction du robot (haut, bas, gauche ou droite).

Problème comment représenter cette direction?


public ??? getDirection() {
    ...
}
                        

Utilité

Problème comment représenter cette direction?

  • Se mettre d’accord pour dire que : 1 signifie haut, 2 signifie bas, etc.

 


public int getDirection() {
    if(....)
        return 1; // return UP
    ...
}
                        

if(robot.getDirection() == 1) {
    ...
}
                        

Pas forcément facile de se rappeler des valeurs ; c’est source d’erreur.

Utilité

Problème comment représenter cette direction?

  • Utiliser une chaine de caractères : “UP”, “DOWN”, “LEFT”, “RIGHT”

 


public String getDirection() {
    if(....)
        return "LEFT";
    ...
}
                        

if(robot.getDirection() == "LEFT") {
    ...
}
                        

Difficile de comprendre en regardant la signature de la méthode ce qu’elle renvoie exactement.

Utilité

Problème comment représenter cette direction?

  • Utiliser une énumération.

 


public Direction getDirection() {
    if(....)
        return Direction.LEFT;
    ...
}
                        

if(robot.getDirection() == Direction.LEFT) {
    ...
}
                        

Définir une Enumération

On crée une énumération comme une classe


public enum Direction {
    UP,
    RIGHT,
    DOWN,
    LEFT
}

Comme une classe, ça définit un type!

C’est un peu comme une classe mais qui ne contient des attributs static et public.

On ne peut pas l’instancier et il y a un nombre fini d’objets qui existent déjà et qu’on peut utiliser.

Utiliser une Enumération


public enum Direction {
    UP,
    RIGHT,
    DOWN,
    LEFT
}

Direction d = Direction.UP;
System.out.println(d); // affiche UP
System.out.println(d == Direction.UP);
switch(d) {
    case UP: d = Direction.LEFT; break;
    case LEFT: d = Direction.DOWN; break;
    case DOWN: d = Direction.RIGHT; break;
    case RIGHT: d = Direction.UP; break;
}
System.out.println(d);

Lister les valeurs possibles

la méthode values() permet de lister les valeurs possibles :


for(Direction d : Direction.values()){
    System.out.println(d);
}
                        

Conversion depuis une chaine de caractères


Scanner sc = new Scanner(System.in);

String text = sc.next(); // lit une direction au clavier, ex: UP
Direction d = ...

System.out.println(d); // affiche UP
                        

Scanner sc = new Scanner(System.in);

String text = sc.next(); // lit une direction au clavier, ex: UP
Direction d;
if(text.equals("UP")) {
    d = Direction.UP;
} else
...

System.out.println(d); // affiche UP
                        

Scanner sc = new Scanner(System.in);

String text = sc.next(); // lit une direction au clavier, ex: UP
Direction d;
for(Direction t : Direction.values()) {
    if(t.toString().equals(text)) {
        d = t;
    }
}
System.out.println(d); // affiche UP
                        

Scanner sc = new Scanner(System.in);

String text = sc.next(); // lit une direction au clavier, ex: UP
Direction d = Direction.valueOf(text);
System.out.println(d); // affiche UP
                        

Pour convertir une chaine de caractère en valeur d’énumération, il faut utiliser la méthode valueOf().

Enumération plus complexes

Comme pour les classes, on peut aussi ajouter des attributs et des méthodes.


enum Direction {
    UP("↑"),
    RIGHT("→"),
    DOWN("↓"),
    LEFT("←");

    private String arrow;
    Direction(String arrow) {
    	this.arrow = arrow;
    }
    public String toArrow() { return arrow; }
}
                        

Si on définit un constructeur nécessitant des arguments, ces-derniers doivent être fournit lors de l’énumération des valeurs possibles.

run it!

Retour sur l’exemple des figures



public enum ConsoleColor {
    BLACK('0'),
    RED('1'),
    GREEN('2'),
    WHITE('7');

      private char code = '0';

      ConsoleColor(char code){
        this.code = code;
      }

      public char code(){
        return code;
      }

      public String apply(String s) {
          return "\u001B[3" + code() + "m" + s + "\u001B[0m";
      }
}

                        

Package et Jar

Package et Jar

Vous avez vu qu’un package dans un dossier n’est pas facile à distribuer.

C’est pourquoi on peut créer, à partir d’un dossier qui contient un package, un fichier Jar (Java Archive). Plus facile à distribuer et à utiliser (notamment avec eclipse).

Package et Jar

On créee un Jar avec la commande jar.

Cette commande accepte les opérations c, t, et x ainsi que les options v (pour afficher les détails) et f (pour indiquer le nom du fichier Jar).

Package et Jar

 

  • c : Pour créer une archive. On indique le dossier contenant le package

 


jar cvf RobotGame.jar -C dossier/contenant/les/classes .
                        

Package et Jar

 

  • c : Pour créer une archive. On indique le dossier contenant le dossier contenant le package
  • t : Pour afficher le contenu d’une archive

 


jar tf RobotGame.jar
                        

Package et Jar

 

  • c : Pour créer une archive. On indique le dossier contenant le dossier contenant le package
  • t : Pour afficher le contenu d’une archive
  • x : Pour extraire une archive

 


jar xf RobotGame.jar
                        

Comment organiser un Projet?

Le mieux est de séparer les sources et les fichiers compilés.

Dans un dossier vide, on crée un dossier src qui va contenir nos fichiers sources, et un dossier bin qui va contenir nos fichiers compilés.

Organisation d’un projet

Pour mon package fr.dant.robotgame, j’ai mes sources dans un dossier src/fr/dant/robotgame

Je peux les compiler et placer les fichiers compilés dans le répertoire donné.


javac -d ./bin -cp ./bin src/fr/dant/robotgame/*.java
                        

Et finalement créer le fichier Jar :


jar cvf RobotGame.jar -C bin .
                        

MANIFEST

Le fichier MANIFEST.MF contient l’ensemble des métadonnées d’une archive jar sous forme d’un unique fichier texte stocké dans le répertoire META-INF.

Il peut contenir entre autre :
Manifest-Version : numéro de version
Created-By : nom de l’auteur
Class-Path : nom d’autre archive contenant des dépendances
Main-Class : nom de la classe contenant la main à exécuter.

MANIFEST

Exemple :

Manifest-Version: 1.0
Created-By: Quentin Bramas
Main-Class: RobotGame

Attention : Le fichier MANIFEST doit obligatoirement :
être encodé en UTF8 ;
se terminer par un retour à la ligne (dernière ligne vide) ;
n’avoir aucun espace à la fin des différentes lignes.

MANIFEST

L’option m permet de fournir un MANIFEST.MF à la création du jar :


                        jar cvmf src/MANIFEST.MF RobotGame.jar -C bin .