Délégation et extension

Arnaud Labourel

6 octobre 2021

Composition et délégation

Exemple de classe

On considère la classe Point suivante :

public class Point {
    public final double x, y;
    public Point(double x, double y){this.x = x;
                                     this.y = y;}
    public double distanceTo(Point p){
        double dx = this.x - p.x;
        double dy = this.y - p.y;
        return Math.hypot(dx, dy);
    }
}

Interaction entre les classes/instances

Une méthode peut utiliser les attributs et méthodes d’une autre instance/classe.

Composition

Afin d’implémenter ses services, une instance peut créer des instances et conserver leurs références dans ses attributs.

public class Circle {
    private Point center;
    private double radius;

    public Circle(double x, double y, double radius){
        this.center = new Point(x, y);
        this.radius = radius;
    }

    public double getX(){ return center.x; }
    public double getY(){ return center.y; }
    public double getRadius(){ return radius; }
}

Agrégation

Une instance peut simplement posséder des références vers des instances :

public class Circle {
    private Point center, point;
    public Circle(Point center, Point point){
        this.center = center;
        this.point = point;
    }
    public Point getCenter(){ return center; }
    public double getRadius(){
        double dx = center.x - point.x;
        double dy = center.y - point.y;
        return Math.hypot(dx, dy);
    }
}

Agrégation et délégation

Délégation du calcul de la distance à l’instance center de la classe Point :

public class Circle {
    private Point center, point;

    public Circle(Point center, Point point){
        this.center = center;
        this.point = point;
    }
    public Point getCenter(){ return center; }
    public double getRadius(){
        return center.distanceTo(point);
    }
}

Agrégation récursive

Il est possible de créer des structures récursives (les attributs de la classe contiennent des références vers une instance de la classe).

public class Node {
    private Node[] nodes;
    private String name;

    public Node(String name, Node[] nodes){
        this.name = name;
        this.nodes = nodes;
    }

    public Node(String name){
        this(name, new Node[0]);
    }
}

Utilisation d’une structure récursive

Node a = new Node("a");
Node b = new Node("b");
Node c = new Node("c");
Node d = new Node("d");

Utilisation d’une structure récursive

image

Utilisation d’une structure récursive

Node a = new Node("a");
Node b = new Node("b");
Node c = new Node("c");
Node d = new Node("d");
Node[] arrayAB = new Node[]{a,b};
Node ab = new Node("ab", arrayAB);
Node[] arrayCD = new Node[]{c,d};
Node cd = new Node("cd", arrayCD);

Utilisation d’une structure récursive

image

Utilisation d’une structure récursive

Node a = new Node("a");
Node b = new Node("b");
Node c = new Node("c");
Node d = new Node("d");
Node[] arrayAB = new Node[]{a,b};
Node ab = new Node("ab", arrayAB);
Node[] arrayCD = new Node[]{c,d};
Node cd = new Node(arrayCD, "cd");
Node cd = new Node("cd", arrayCD);
Node abcd = Node cd = new Node("abcd", arrayABCD);

Utilisation d’une structure récursive

image

Agrégation récursive et méthodes

Il est ensuite possible d’implémenter des méthodes de façon récursive :

public class Node {
    private Node[] nodes;
    private String name;

    // Constructeurs des transparents précédents.

    public void print(){
        System.out.print("[" + name + "]");
        for(int i = 0; i < nodes.length; i++){
            nodes[i].print();
        }
    }
}

Parcours d’une structure récursive

image

Classes abstraites et extension

Classe ListSum

Supposons que nous ayons la classe suivante :

public class ListSum {
  private int[] list = new int[10];
  private int size = 0;
  public void add(int value) {
    list[size] = value;
    size++;
  }
  public int eval() {
    int result = 0;
      for (int i = 0; i < size; i++)
        result += list[i];
      return result;
  }
}

Classe ListProduct

Supposons que nous ayons aussi la classe suivante (très similaire) :

public class ListProduct {
  private int[] list = new int[10];
  private int size = 0;
  public void add(int value) {
    list[size] = value;
    size++;
  }
  public int eval() {
    int result = 1;
    for (int i = 0; i < size; i++)
      result *= list[i];
    return result;
  }
}

Refactoring pour isoler la répétition

Les deux classes sont très similaires et il y a de la répétition de code.

Il est possible de refactorer (réécrire le code) les classes précédentes de façon à isoler les différences dans des méthodes :

public class ListSum {
  /* attributs, constructeur et méthode add. */
  public int eval() {
    int result = neutral();
    for (int i = 0; i < size; i++)
      result = compute(result, list[i]);
    return result;
  }
  private int neutral() { return 0; }
  private int compute(int a, int b) { return a+b; }
}

Refactoring pour isoler la répétition

Il est possible de refactorer les classes précédentes de façon à isoler les différences dans des méthodes :

public class ListProduct {
  /* attributs, constructeur et méthode add. */
  public int eval() {
    int result = neutral();
    for (int i = 0; i < size; i++)
      result = compute(result, list[i]);
    return result;
  }
  private int neutral() { return 1; }
  private int compute(int a, int b) { return a*b; }
}

Comment éviter la répétition ?

Après la refactorisation du code :

  • seules les méthodes neutral et compute diffèrent
  • il serait intéressant de pouvoir supprimer les duplications de code

Deux solutions :

  • La délégation en utilisant une interface
  • L’extension et les classes abstraites

Interface Operator

Nous allons externaliser les méthodes neutral et compute dans deux nouvelles classes. Elles vont implémenter l’interface Operator :

public interface Operator {
  public int neutral();
  public int compute(int a, int b);
}
public class Sum implements Operator {
  public int neutral() { return 0; }
  public int compute(int a, int b) { return a+b; }
}
public class Product implements Operator {
  public int neutral() { return 1; }
  public int compute(int a, int b) { return a*b; }
}

Délégation

Les classes ListSum et ListProduct sont fusionnées dans une unique classe List qui délègue les calculs à un objet qui implémente l’interface Operator :

public class List {
  /* attributs et méthode add */
  private Operator operator;
  public List(Operator operator){
    this.operator = operator;
  }
  public int eval() {
    int result = operator.neutral(); 
    for (int i = 0; i < size; i++)
      result = operator.compute(result, list[i]); 
    return result;
  }
}

Délégation

Utilisation des classes ListSum et ListProduct :

ListSum listSum = new ListSum();
listSum.add(2); listSum.add(3);
System.out.println(listSum.eval());
ListProduct listProduct = new ListProduct();
listProduct.add(2); listProduct.add(3);
System.out.println(listProduct.eval());

Utilisation après la refactorisation du code :

List listSum = new List(new Sum());
listSum.add(2); listSum.add(3);
System.out.println(listSum.eval());
List listProduct = new List(new Product());
listProduct.add(2); listProduct.add(3);
System.out.println(listProduct.eval());

Solution utilisant une classes abstraite

public abstract class List {
  private int[] list = new int[10];
  private int size = 0;
  public void add(int value) {
    list[size] = value; size++;
  }
  public int eval() {
    int result = neutral(); 
    // utilisation d'une méthode abstraite
    for (int i = 0; i < size; i++)
      result = compute(result, list[i]); // idem
    return result;
  }
  public abstract int neutral(); // méthode abstraite
  public abstract int compute(int a, int b); // idem
}

Mot-clé abstract

Classe abstraite

  • On peut mettre abstract devant le nom de la classe à sa définition pour signifier qu’une classe est abstraite.

  • Une classe est abstraite si des méthodes ne sont pas implémentées.

    Classe abstraite = classe avec des méthodes abstraites

  • Tout comme pour une interface, une classe abstraite n’est pas instanciable.

Méthode abstraite

  • abstract devant le nom de le type de retour de la méthode à sa définition pour signifier qu’une méthode est abstraite.
  • Méthode abstraite = méthode sans code, juste la signature (type du retour et des paramètres) est définie

Classes abstraites et extension

Tout comme pour les interfaces, il n’est pas possible d’instancier une classe abstraite :

List list = new List(); // erreur
System.out.println(list.eval()) // car que faire ici ?

Nous allons étendre la classe List afin de récupérer les attributs et les méthodes de List et définir le code des méthodes abstraites :

public class ListSum extends List {
  public int neutral() { return 0; }
  public int compute(int a, int b) { return a+b; }
}

Classes abstraites et extension

La classe ListSum n’est plus abstraite, toutes ses méthodes sont définies :

  • les méthodes add et eval sont définies dans List et ListSum hérite du code de ses méthodes.
  • les méthodes neutral et compute qui étaient abstraites dans List sont définies dans ListSum.

On peut donc instancier la classe ListSum :

ListSum listSum = new ListSum();
listSum.add(3);
listSum.add(7);
System.out.println(listSum.eval());

Classes abstraites et extension

On peut procéder de manière similaire pour créer une classe ListProduct

public class ListProduct extends List {
  public int neutral() { return 1; }
  public int compute(int a, int b) { return a*b; }
}

La classe ListProduct n’est plus abstraite, toutes ses méthodes sont définies :

ListProduct listProduct = new ListProduct();
listProduct.add(3);
listProduct.add(7);
System.out.println(listProduct.eval());

Généralisation de l’extension aux classes non-abstraites

Plus généralement, l’extension permet de créer une classe en :

  • conservant les services (attributs et méthodes) d’une autre classe;
  • ajoutant de nouveaux services (attributs et méthodes);
  • redéfinissant certains services (méthodes).

En Java :

  • On utilise le mot-clé extends pour étendre une classe ;
  • Une classe ne peut étendre directement qu’une seule classe.

Important

Il est toujours préférable de privilégier l’implémentation à l’extension.

Extension pour ajouter des nouveaux services

Supposons que nous ayons la classe Point suivante :

public class Point {
  public int x, y;
  public void setPosition(int x, int y) {
    this.x = x;
    this.y = y;
  }
}

Il est possible d’ajouter de nouveaux services en utilisant l’extension :

public class Pixel extends Point {
  public int r, g, b;
  public void setColor(int r, int g, int b) {
    this.r = r; this.g = g; this.b = b; }
}

Extension pour ajouter des nouveaux services

Les services (méthodes et attributs) de Point sont disponibles dans Pixel :

Pixel pixel = new Pixel();
pixel.setPosition(4,8);
System.out.println(pixel.x); // → 4
System.out.println(pixel.y); // → 8
pixel.setColor(200,200,120);

Évidemment, les services de Pixel ne sont pas disponibles dans Point :

Point point = new Point();
point.setPosition(4,8);
System.out.println(point.x); // → 4
System.out.println(point.y); // → 8
point.setColor(200,200,120); // impossible !

Redéfinition de méthode

Supposons que nous ayons la classe Point suivante :

public class Point { 
  public int x, y;
  public void setPosition(int x, int y) { 
    this.x = x; this.y = y; 
  }
  public void clear() { 
    x = 0; y = 0; 
  } 
}

Redéfinition de méthode

Il est possible de redéfinir la méthode clear dans Point :

public class Pixel extends Point {
  public int r, g, b;
  public void setColor(int r, int g, int b) {
    this.r = r; this.g = g; this.b = b;
    }
  public void clear(){
    x = 0; y = 0;
    r = 0; g = 0; b = 0;
  }
}

Redéfinition avec super

public class Point { 
  public int x, y;
  public void setPosition(int x, int y) { 
    this.x = x; this.y = y; 
  }
  public void clear() { 
    setPosition(0, 0); 
  }
}

Le mot-clé super permet d’utiliser la méthode clear de Point :

public class Pixel extends Point { public int r, g, b;
public void setColor(int r, int g, int b) { 
  this.r = r; this.g = g; this.b = b; }
public void clear() { super.clear(); setColor(0, 0, 0); } }

Le mot-clé super

public class Point { 
  public int x, y;
  public void setPosition(int x, int y) { 
    this.x = x; this.y = y; }
}

Si la méthode n’a pas été redéfinie, le mot clé super est inutile :

public class Pixel extends Point { 
  public int r, g, b;
  public void setColor(int r, int g, int b) { 
    this.r = r; this.g = g; this.b = b; 
  }
  public void clear() { 
    /*super.*/setPosition(0, 0); 
    setColor(0, 0, 0); 
  }
}

Les constructeurs et le mot-clé super

public class Point { 
  public int x, y;
  public Point(int x, int y) { 
    this.x = x; this.y = y; 
  }
}

La classe Point n’a pas de constructeur sans paramètre, il faut donc indiquer comment initialiser la partie de Pixel issue de la classe Point :

public class Pixel extends Point { 
  public int r, g, b;
  public Pixel(int x, int y, int r, int g, int b) {
    super(x, y); // appel du constructeur de Point
    this.r = r; this.g = g; this.b = b;
  }
}

Les constructeurs

Supposons que nous ayons la classe Point suivante :

public class Point { 
  public int x, y;
  public Point() { x = 0; y = 0; }
  public Point(int x, int y) { this.x = x; this.y = y; }
}

Par défaut, le constructeur sans paramètre est appelé :

public class Pixel extends Point { 
  public int r, g, b;
  public Pixel(int r, int g, int b) {
    // appel du constructeur sans paramètre de Point
    this.r = r; this.g = g; this.b = b;
  }
}

Les constructeurs

public class Point { 
  public int x, y;
  //public Point() { x = 0; y = 0; }
  public Point(int x, int y) { this.x = x; this.y = y; }
}

Ici, vous devez préciser les paramètres du constructeur avec super :

public class Pixel extends Point {
  public int r, g, b;
  public Pixel(int r, int g, int b) {
  // erreur de compilation
  // (aucun constructeur sans paramètre)
  this.r = r; this.g = g; this.b = b;
  }
}

Transtypages et polymorphisme

Aucune méthode ou attribut ne peut être supprimée lors d’une extension. Par exemple, Pixel possède toutes les attributs et méthodes de Point (même si certaines méthodes ont pu être redéfinies).

Par conséquent, l’upcasting est toujours autorisé :

Point point = new Pixel();
point.setPosition(2,4);
System.out.println(point.x + " " + point.y);
point.clear();

Remarques

  • Le code exécuté lors d’un appel de méthode est déterminé à l’exécution en fonction de la référence présente dans la variable.
  • Le typage des variables permet de vérifier à la compilation l’existence des attributs et des méthodes.

Upcasting

Définition d’upcasting

Considérer une instance d’une classe comme une instance d’une de ses super-classes, c’est-à-dire :

  • une classe étendue par la classe
  • une interface implémentée par la classe

Remarques

  • Après l’upcasting, les services supplémentaires de la classe (méthodes et attributs non-disponibles dans la super-classe) ne sont plus accessibles.
  • Lors de l’appel des méthodes, si la classe a redéfini (overrides) la méthode, c’est le code de la classe qui est exécuté.

La classe Object

Par défaut, les classes étendent la classe Object de Java. Par conséquent, l’upcasting vers la classe Object est toujours possible :

Pixel pixel = new Pixel();
Object object = pixel;
Object[] array = new Object[10];
for (int i = 0; i < t; i++) {
  if (i%2==0) array[i] = new Point();
  else array[i] = new Pixel();
}

Notez que object.setPosition(2,3) n’est pas autorisé dans le code ci-dessus car la classe Object ne possède pas la méthode setPosition et seul le type de la variable compte pour déterminer si l’appel d’une méthode ou l’utilisation d’une attribut est autorisé.

Méthodes de la classe Object

  • protected Object clone(): Creates and returns a copy of this object.
  • boolean equals(Object obj): Indicates whether some other object is “equal to” this one.
  • Class<?> getClass(): Returns the runtime class of this Object.
  • int hashCode(): Returns a hash code value for the object.
  • String toString(): Returns a string representation of the object.

Il y a aussi des méthodes wait et notify pour attendre sur un objet et réveiller des thread en attentes sur un objet : cours de Programmation objet concurrente en Master 1 informatique d’AMU.

La méthode toString() de la classe Object

Par transitivité de l’extension, toutes les méthodes et attributs de la classe Object sont disponibles sur toutes les instances :

Object object = new Object(); Point point = new Point(2,3);
System.out.println(object.toString());
// → java.lang.Object@19189e1
System.out.println(point.toString());
// → test.Point@7c6768

La méthode toString est utilisée par Java pour convertir une référence en chaîne de caractères :

Object object = new Object(); 
Point point = new Point(2,3); 
String string = object+";"+point; 
System.out.println(string);
// → java.lang.Object@19189e1;test.Point@7c6768

La méthode toString() et le polymorphisme (1/2)

Évidemment, il est possible de redéfinir la méthode toString :

public class Point { 
  public int x, y;
  public Point(int x, int y) { 
    this.x = x; this.y = y; 
  }

  public String toString() { 
    return "("+x+","+y+")"; 
  }
}

La méthode toString() et le polymorphisme (2/2)

Le polymorphisme fait que cette méthode est appelée si la variable contient une référence vers un Point :

Point point = new Point(2,3); 
Object object = point;
System.out.println(point); // → (2,3)
System.out.println(object); // → (2,3)

Rappel : extension d’interface

Supposons que nous ayons l’interface suivante :

public interface List<E> {
  public int size();
  public E get(int index);
}

En Java, on peut aussi étendre une interface :

public interface ModifiableList<E> extends List<E> {
  public void add(E value);
  public void remove(int index);
}

Une classe qui implémente l’interface ModifiableList doit implémenter les méthodes size, get, add et remove.

Extension d’interface

Supposons que la classe ArrayModifiableList implémente l’interface ModifiableList. Dans ce cas, nous pouvons écrire :

ModifiableList<Integer> modifiableList = new ArrayModifiableList<>();
modifiableList.add(2);
modifiableList.add(5);
modifiableList.remove(0);
List list = modifiableList;
System.out.println(list.size());

En revanche, il n’est pas possible d’écrire :

  list.remove(0);
/* → Cette méthode n'existe pas */ 
/* dans l'interface List. */

Extension de plusieurs interfaces

Supposons que nous avons l’interface suivante :

public interface Printable { public void print(); }

En Java, une interface peut étendre plusieurs interfaces :

public interface ModifiablePrintableList
  extends ModifiableList, Printable {
}

Remarques

  • Nous ne définissons pas de nouvelles méthodes dans l’interface ModifiablePrintableList.

    Cette interface ne représente que l’union des interfaces ModifiableList et Printable.

  • De nouvelles méthodes auraient pu être définies dans ModifiablePrintableList.

Extension et accessibilité

Accessibilité : modificateur public et default

Une classe ou un membre (attribut ou méthode) est accessible :

  • de n’importe où s’il est précédé du modificateur public ;
  • des classes de son paquet si rien n’est précisé (default).
public class MyClass {
  public int myPublicField;
  int myPackageField;
  public void doPublicAction() { }
  void doPackageAction() { }
}

Remarques :

  • Seules les classes publiques sont utilisable à partir d’un autre paquet
  • Un fichier ne peut contenir qu’une seule classe publique

Accessibilité : modificateur private

Un membre (attribut ou méthode) est accessible :

  • de n’importe où s’il est précédé du modificateur public
  • des classes de son paquet si rien n’est précisé (default)
  • des méthodes de la classe s’il est précédé de private
public class MyClass {
  private int privateField;
  private void doPrivateAction() { }
}

Afin de limiter les conséquences d’une modification :

  • Les méthodes ou attributs définies pour rendre lisible l’implémentation des fonctionnalités doivent être privées ;
  • Seule l’interface de la classe doit être publique.

Accessibilité : modificateur protected

Un membre (attribut ou méthode) protected n’est accessible que par les méthodes des classes du paquet et par les classes qui l’étendent.

Le modificateur protected permet de protéger un membre :

public class MyClass {
  protected int protectedField; 
  protected void doProtectedAction() { }
}
Modificateur Classe Paquet Extension Extérieur
private
default
protected
public