Capsule de théorie Polymorphisme

Qu’est-ce que le polymorphisme ?

En programmation, le polymorphisme désigne la possibilité pour une opération d’être appliquée à des valeurs de différents types. Le terme dénote le fait que l’opération doit pour cela avoir plusieurs formes, plusieurs implémentations.

En programmation orientée objet, les opérations sont des méthodes et il existe au moins trois manières de mettre en œuvre le polymorphisme :

  • Le polymorphisme par sous-typage.
  • Le polymorphisme ad hoc.
  • Le polymorphisme paramétrique.

Durant les précédents modules de programmation, vous avez déjà eu l’occasion, même sans le savoir, d’utiliser ces trois types de polymorphisme. Nous allons maintenant nous pencher de manière plus détaillée sur les deux premiers. Le polymorphisme paramétrique est le type de polymorphisme mis en œuvre dans les collections de Java et qui vous permet, par exemple, de spécifier les types des éléments d’un ArrayList. En Java, on parle de « types génériques ». Si ces types sont très faciles à utiliser, leur conception, en revanche, dépasse le cadre de ce module.

Polymorphisme par sous-typage

Le polymorphisme par sous-typage est une forme de polymorphisme spécifique aux langages de programmation orientés objet puisqu’elle repose sur l’héritage et la redéfinition (overriding) de méthodes virtuelles. Selon le principe de substitution de Liskov (LSP), il est permis d’invoquer une méthode avec des paramètres dont les types sont des sous-types des types attendus. Cela vaut pour les paramètres qui sont explicitement définis, mais cela vaut également pour l’objet sur lequel la méthode est invoquée.

La figure 1 illustre la manière de réaliser une classe dont toutes les méthodes sont virtuelles pures à l’aide du mot clé « interface » en Java.

1
2
3
4
5
6
7
public interface Unit {
    public String getName();
    public String getSymbol();
    public Dimension getDimension();
    public double convertFromBaseUnit(double value);
    public double convertToBaseUnit(double value);
}
Fig. 1 – Interface Unit

La figure 2 illustre l’implémentation partielle des méthodes virtuelles de l’interface Unit par la classe UnitBase. On remarque la syntaxe est identique à celle de la surécriture (override) d’une méthode virtuelle pure. Puisqu’elle ne fournit pas d’implémentation pour les méthodes convertFromBaseUnit et convertToBaseUnit (qui restent virtuelles pures), la classe Unit ne peut pas être instanciée et ce fait est explicité par le mot clé abstract.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public abstract class UnitBase implements Unit{
    private final String name;
    private final String symbol;
    private final Dimension dimension;
    
    public UnitBase(String name, String symbol, Dimension dimension) {
        this.name = name;
        this.symbol = symbol;
        this.dimension = dimension;
    }
    
    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getSymbol() {
        return symbol;
    }

    @Override
    public Dimension getDimension() {
        return dimension;
    }
}
Fig. 2 – Classe abstraite UnitBase

Les figures 3 et 4 illustrent la surécriture (override) des méthodes virtuelles pures convertToBaseUnit et convertFromBaseUnit de la classe UnitBase par les classes IntervalUnit et RatioUnit. Ces différentes implémentations sont autant de formes de ces méthodes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class RatioUnit extends UnitBase {
    private final double ratio;

    public RatioUnit(Dimension dimension, String name, String symbol, double ratio) {
        super(name, symbol, dimension);
        this.ratio = ratio;
    }
    
    @Override
    public double convertToBaseUnit(double value) {
        return value / ratio;
    }
    
    @Override
    public double convertFromBaseUnit(double value) {
        return value * ratio;
    }
}
Fig. 3 – Class RatioUnit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class IntervalUnit extends UnitBase {
    private final double ratio;
    private final double offset;
    
    public IntervalUnit(Dimension dimension, String name, String symbol, double ratio, double offset) {
        super(name, symbol, dimension);
        this.ratio = ratio;
        this.offset = offset;
    }
    
    @Override
    public double convertToBaseUnit(double value) {
        return (value - offset) / ratio;
    }
    
    @Override
    public double convertFromBaseUnit(double value) {
        return value * ratio + offset;
    }
}
Fig. 4 – IntervalUnit

Polymorphisme ad hoc

Le polymorphisme par sous-typage impose que toutes les formes de la méthode aient la même signature ; le type et le nombre des arguments ne doivent pas varier. Le polymorphisme ad hoc permet de s’affranchir de cette contrainte. Celui-ci repose sur la possibilité qu’offrent certains langages de définir plusieurs fonctions ayant le même nom. Dans les langages orientés objet, on parle généralement de surcharge (overloading). Contrairement au polymorphisme par sous-typage où la sélection de la méthode se fait à l’exécution, ici le choix de la méthode à invoquer est fait par le compilateur en fonction du type et du nombre des arguments.

Les différentes forme des méthodes print et println de la classe PrintStream (objet System.out) sont un exemple de polymorphisme ad hoc.