UP | HOME

Agrégation et héritage - Polymorphisme

Table of Contents

1 La grande question : Comment construire des interfaces ou des classes ?

  • Par agrégation
    • La relation "a un"
  • Par héritage
    • La relation "est un"
  • Avantages de l'héritage : factorisation du code et polymorphisme

2 En Java

2.1 Le cas des interfaces

2.1.1 Agrégation : la brique de base

// Un entier naturel
interface Nat { 
  // - a pour valeur un int.
  int val();
  // - peut être nul ou non
  boolean estNul();
  // - a un prédécesseur (s'il est non nul)
  Nat predecesseur();
}  

// Une fabrique générique d'entiers naturels 
interface FabriqueNaturels<T> {
  // a une méthode fabriquant zéro.
  T creer();
}

2.1.2 Héritage : la puissance de la factorisation

  • Possibilité d'un héritage multiple
  • Préservation des méthodes des interfaces parentes
  • Possibilité d'une extension par agrégation
// Un entier naturel est 
// - un élément d'un semi-anneau unitaire et
// - une fabrique d'entiers naturels. 
public interface Nat extends SemiAnneauUnitaire<Nat>, FabriqueNaturels<Nat> {
  ... // Extension par agrégation
}

// Un semi-anneau unitaire est un semi-anneau, un monoide multiplicatif 
//   et est biunifère.
interface SemiAnneauUnitaire<T> 
  extends SemiAnneau<T>, MonoideMultiplicatif<T>, 
          BiUnifere<T> // Héritage multiple
{}

2.1.3 Deux particularités de l'héritage

  • Possibilité de spécialiser une méthode

    // Cas typique d'une interface possédant des fabriques (méthodes de fabrication)
    interface Nat1 {
      Nat1 creer(); // Une fabrique
      ...
    }
    interface Nat2 extends Nat1 {
      Nat2 creer(); // Une fabrique spécialisant
                    //   (et remplaçant) Nat1 creer()
      ...
    }
    
    • Covariance possible pour le type de retour
    • Invariance pour le type des arguments (la contravariance aurait été possible mais les concepteurs du langage ont préféré permettre la surcharge)
  • Possibilité d'une surcharge : méthodes ayant le même nom mais des types différents de paramètres
// Fabrique générique avec des méthodes de même nom
interface FabriqueNat<T> {
  T creer();
  T creer(T pred);
  T creer(int val);
}

2.2 Le cas des classes

2.2.1 Agrégation : la brique de base (bis)

class NatParInt implements Nat {
  // Attributs  
  private int val;
  // Constructeurs
  public NatParInt(int val){
    if(val < 0)
      throw new IllegalArgumentException("Pas de Nat à patir d'un int négatif.");
    this.val = val;
  }
  // Méthodes (accesseurs, fabriques et services)
  @Override
  public Nat zero() {
    return this.creerNatAvecValeur(0);
  }
  ...
}

2.2.2 Héritage : la puissance de la factorisation (bis)

  • Héritage simple uniquement
  • Préservation des attributs et des méthodes des classes parentes
  • Possibilité d'une extension par agrégation
// Implémentation des entiers naturels non nuls (des successeurs)
class Succ implements Nat {
  private Nat predecesseur;
  public Succ(Nat predecesseur) {
    this.predecesseur = predecesseur;
  }
  @Override
  public int val() {
    return 1 + this.predecesseur().val();
  }
}
// Variante mémorisant la valeur de l'entier naturel
class SuccMemoire extends Succ implements Nat {
  // Extension de l'état
  private int val;
  // Constructeur
  public SuccMemoire(Nat predecesseur) {
    super(predecesseur); // Appel du constructeur parent
    this.val = this.init();
  } 
  // Extension des services
  private int init() { ... }
}

2.2.3 La possibilité, certes limitée, d'un héritage multiple

A partir de Java 8, il est possible d'implémenter les méthodes déclarées dans une interface. Elles doivent alors être précédées du qualificatif default. On parle de trait en certains langages, notamment Scala, mais nous utiliserons plutôt dans ce cours le terme d'interface concrète, par analogie avec celui de classe abstraite.

interface Nat {
  default Nat somme(Nat x){
    return ...;
  }
}

Il devient possible d'obtenir l'héritage multiple par une construction simple, en suivant la méthode de définition d'une classe d'implémentation. Toute classe X se décompose en trois types :

  • une classe E réifiant l'état (c'est-à-dire représentant l'état par un objet), qui agrège les attributs, les constructeurs et les accesseurs,
  • une interface I déclarant les accesseurs et les fabriques, non implémentés, et les services, implémentés,
  • une classe de composition C, équivalente à celle initiale X,
    • agrégeant un objet de la classe E définissant l'état,
    • implémentant les constructeurs en initialisant l'état,
    • implémentant les accesseurs par délégation à l'état,
    • implémentant les fabriques en appelant les constructeurs,
    • implémentant l'interface I et ainsi héritant du code des services.

Cette construction se généralise facilement à l'héritage multiple. Supposons des classes X_j décomposées en (E_j, I_j, C_j). La classe M correspondant à l'héritage multiple des X_j se construit comme la classe de composition précédente :

  • agrégation d'objets des classes E_j pour définir l'état,
  • implémentation des constructeurs en initialisant l'état,
  • implémentation des accesseurs par délégation à l'état,
  • implémentation des fabriques en appelant les constructeurs,
  • implémentation des interfaces I_j et ainsi héritage du code des services.

Lorsque deux méthodes identiques (même nom, même type pour les arguments) sont héritées de deux interfaces, le conflit doit être levé explicitement, en redéfinissant la méthode. Pour accéder à une méthode f définie dans une interface parente P, on doit utiliser la notation P.super.f.

Enfin, les méthodes de la classe Object ne peuvent être définies dans une interface. Les concepteurs du langages Java ont fait le choix de considérer ces méthodes comme des méthodes de bas niveau, devant accéder aux attributs : elles ne peuvent être définies que dans les classes. Si on considère qu'une de ces méthodes est plutôt de haut niveau, comme equals, et doit être implémentée dans une interface, il est nécessaire de la doubler par une seconde méthode, estEgal(Object o) par exemple.

2.2.4 Une particularité très importante : la spécialisation

  • Redéfinition d'une méthode
class Succ implements Nat {
  ...
  @Override
  public int val() {
    return 1 + this.predecesseur().val();
  }
}
class SuccMemoire extends Succ implements Nat {
  ... 
  @Override
  public int val() {
    return this.val;
  }
}
  • Effet : remplacement de la méthode spécialisée par la méthode spécialisante

    Pas d'effet lors d'une conversion

SuccMemoire b = new SuccMemoire(...);
Succ a = b;
a.val() // -> accès à val dans SuccMemoire
  • Possibilité d'appeler la méthode spécialisée de la classe parente avec super
class SuccMemoire extends Succ implements Nat {
  ...
  // Extension des services
  private int init() {
    return super.val(); // Appel de la méthode spécialisée dans Succ
  }
  //

2.2.5 Deux particularités secondaires

  • Masquage des attributs

    class A {
      public int x;
    }
    class B extends A {
      public int x;
    }
    

    Les deux attributs existent dans un objet de la classe B mais celui déclaré dans B masque celui déclaré dans A.

    Comment accéder au x déclaré dans A ?

    Par conversion.

    B b = new B();
    A a = b;
    a.x // -> accès au *x* déclaré dans *A*
    
  • Possibilité d'une surcharge

    Voir les interfaces.

3 En Typescript et en Javascript moderne - Comme en Java, et plus, mais attention au this

3.1 Comparaison Java/Typescript : similitudes et différences

Depuis la version 6 d'ECMAScript sortie en 2015, et nommée ES2015, le langage Javascript, qui implémente cette norme, possède des classes. Il n'est donc plus nécessaire d'utiliser les chaînes de prototypes pour hériter de champs (de propriétés). Les transcompilateurs réalisent d'ailleurs automatiquement la traduction du Javascript moderne avec des classes dans l'ancien sans classe, rendant superflu l'apprentissage du prototypage. Il reste un usage intéressant des prototypes, permettant de séparer complètement l'héritage du sous-typage. Il est ainsi possible de factoriser le code sans limitation. Cette possibilité repose sur la particularité des méthodes en Javascript, qui possèdent un paramètre implicite, this, référence vers l'objet courant.

Lorsqu'on déclare et définit une méthode ou une fonction anonyme en Javascript (et donc en Typescript), il existe un paramètre supplémentaire, implicite, nommé this.

  • méthode f(A) : B dans la classe C : équivalent à un champ f de type (this: C) => (A => B) (this étant implicite)
  • function(A) : B : équivalent à une fonction anonyme de type (this: C) => (A => B) (this étant implicite)

Remarque : les λ-expressions n'ont pas de this implicite.

En Java, la méthode peut donner un champ de type fonctionnel, à condition de capturer this, soit en l'initialisant soit en l'abstrayant. La fonction ainsi obtenue peut être affectée à une fonction mais pas à une méthode, comme en Typescript.

Paquet session1.reglesThis. Exemple de capture de this.

class A {
    private int x = 3;

    void afficher() {
        System.out.println("x : " + this.x);
    }

    public static void main(String[] args) {
        A a = new A();
        a.afficher(); // 'x : 3' - règle (2)
        Runnable p = a::afficher; // équivalent à (() -> a.afficher()) (liaison de this)
        p.run(); // 'x : 3' (équivalent à 'a.afficher()')
        Consumer<A> c = A::afficher; // équivalent à ((A x) -> x.afficher()) (abstraction de this) 
        c.accept(a); // 'x : 3' (équivalent à 'a.afficher()')
    }
}

En typescript, grâce à cette possibilité de considérer une méthode comme un champ, et de pouvoir l'initialiser, on peut définir des réservoirs de méthodes puis d'hériter de celles-ci dans une classe d'implémentation. Il s'agit bien d'une factorisation, non seulement statique mais aussi dynamique, puisqu'une une seule méthode est créée lors de l'exécution. Pour une approche applicable aux classes, voir la notion de mixin.

La fonction permettant l'héritage (la factorisation) d'une méthode, et utilisant le prototype (voir heritageMultiple/heritage.ts)

function heritage(nomChamp: string, classeParente: any) {
    return classeParente.prototype[nomChamp];
}

Réservoir de méthodes

class Reservoir {
  f(x : A) : B { ...}
  ...
}

Héritage

interface I {
  f(x : A) : B;
}
class C implements I {
  public readonly f: (x : A) => B;
  constructor() {
    // Héritage du code
    this.f = heritage('f', Reservoir); // Possibilité d'initialiser le champ fonctionnel f
                                       // équivalent à une méthode. Impossible en Java !
  }
  ...
}

Cette approche permet donc une factorisation et une dissociation complète entre l'héritage et le sous-typage, comme annoncé. Elle n'est pas possible en Java, puisque les deux seules manières possibles de définir une méthode sont l'agrégation et l'héritage.

Inversement, montrons comment ces techniques naturelles en Java se traduisent en Typescript, pour la construction d'interfaces et de classes. La traduction est plutôt directe, à l'exception de l'héritage multiple.

  • Interfaces
    • Agrégation : idem.
    • Héritage : idem.
    • Deux particularités
      • Spécialisation : possible pour le type de retour (covariance), autorisée pour le type des arguments (contravariance et covariance, ce qui n'est pas sûr pour la covariance mais permet d'exprimer des usages courants en Javascript).
      • Surcharge : autorisée, l'identité d'une méthode étant donnée par son nom et le nombre de ses arguments (et non le type de ses arguments comme en Java). Cette particularité s'explique par la traduction en Javascript, qui oublie les types.
  • Classes
    • Agrégation : idem.
      • Notation pratique : déclaration d'un attribut via le paramètre du constructeur

        constructor(private attribut : TypeAttribut) { ... }
        
    • Héritage : idem.
    • Possibilité d'un héritage multiple (une classe et plusieurs interfaces concrètes) : possible, en utilisant la technique de factorisation décrite au-dessus.
    • Spécialisation : idem.
    • Deux particularités secondaires
      • Masquage des attributs : masquage interdit, d'où une erreur à la compilation.
      • Possibilité d'une surcharge : voir le cas des interfaces.

3.2 this : les liaisons dangereuses

Si on a vu l'intérêt pour la factorisation d'avoir un paramètre implicite this pour les méthodes, sa liaison n'est pas sans danger. Il existe des règles pour lier une valeur à this lorsqu'une fonction est appelée. Les voici, par priorité décroissante.

  1. Si la fonction est le résultat d'un appel à function#bind, this sera l'argument donné à bind.
  2. Sinon, si la fonction a été invoquée sous la forme pointée cible.fctn(args), this sera la référence cible.
    • cible.fctn(args) équivalent à cible.fctn appliqué à (cible, args)
  3. Sinon, si le mode strict est utilisé, this ne sera pas défini (valeur undefined).
  4. Sinon, this réfère à l'objet global (Window dans un navigateur).

Le danger vient des troisième et quatrième cas, comme le montre l'exemple suivant (voir session1/this/regles en Typescript, et session1.reglesThis en Java).

class A {
  x = 3;
  afficher() {
    console.log('x : ' + this.x);
  }
}

const a = new A();
a.afficher(); // 'x : 3' - règle (2)

// Utilisation de la méthode dans un enregistrement
const z = { x: 10, p: a.afficher };
z.p(); // 'x : 10' - règle (2), puisque a.afficher ne capture pas this, contrairement à Java 

const p = z.p;
p(); // 'this' est 'undefined' en mode strict ou l'objet global sinon (règles (3) et (4)).

Le dernier appel produit une erreur, à cause des règles (3) et (4). Pour éviter de telles erreurs, il faut s'astreindre à une certaine discipline.

Recommandation

  • Ne pas utiliser la projection donnant le champ fonctionnel. Toujours utiliser la forme pointée avec des arguments. Pour obtenir l'équivalent de la projection, créer une λ-expression, comme ci-dessous.

    cible.f // à éviter
    (x , ...) => cible.f(x, ...) // à utiliser
    
  • Complément : voir la documentation de Typescript dédiée à ce sujet.

4 Le polymorphisme

4.1 Typage nominal en Java, typage structurel en Typescript

En Java, un type (objet) correspond soit à une classe, soit à une interface. On peut donc lui associer un unique nom, celui de la classe ou de l'interface. Réciproquement, à tout nom d'une classe ou d'une interface correspond un unique type. Ce nom est un nom qualifié, préfixé par le nom du paquet contenant la définition de la classe ou de l'interface. Cette forme de typage est dite nominale, du fait de cette correspondance.

En Typescript, un type est interprété par une structure d'enregistrement. Une telle structure déclare des champs (ou des propriétés), avec leur nom, leur type, leur visibilité et le droit d'accès :

{
  ctrl1 visi1 nom1 : type1,
  ctrl2 visi2 nom2 : (arg : Arg) => type2, // Champ de type fonctionnel
  ...
}

Remarque 1 : nom2 aurait pu être déclaré ainsi, de manière équivalente, en l'absence de droit d'accès.

visi2 nom2(arg : Arg): type2, // Méthode

Il existe cependant une différence entre ces deux déclarations. Lorsqu'une méthode est déclarée, son code est partagé par toutes les instances de la classe. Lorsqu'un champ de type fonctionnel est déclaré, ce n'est pas le cas.

Remarque 2 : comme en Java, la visibilité se déduit du qualificatif de visibilité utilisé dans le code. Cependant, l'interprétation de protected et de l'absence de qualificatif diffère d'un langage à l'autre. On considère un membre hérité dans une classe dérivée de la classe où apparaît la déclaration héritée du membre.

  • private : visible dans la classe de déclaration,
  • protected :
    • Java : visible dans la classe dérivée et dans toutes les sur-classes jusqu'à la classe de déclaration, et dans le paquet de la classe de déclaration,
    • Typescript : visible dans la classe dérivée et dans toutes les sur-classes jusqu'à la classe de déclaration,
  • aucun :
    • Java : visible dans le paquet,
    • Typescript : équivalent de public,
  • public : visible partout.

Remarque 3 : comme en Java, Typescript utilise des espaces de nommage pour ranger les noms. C'est l'instruction namespace qui introduit un espace de nommage. Les espaces de nommage ne sont pas liés à la visibilité, contrairement à Java. Ils ressemblent à ceux utilisés en C++.

Remarque 4 : il est possible d'interdire l'accès en écriture, en qualifiant un attribut de classe avec final en Java et readonly en Typescript. En Java, le qualificatif final devant une méthode est aussi possible. Il signifie l'impossibilité de redéfinir la méthode dans une sous-classe. Une redéfinition correspond à une modification en écriture.

Typescript utilise donc une interprétation structurelle des types. Un type en Typescript est :

  • une interface,
  • une classe,
  • une énumération,
  • une union ou une intersection de types,
  • un alias,
  • un type JSON.

Tout type est interprété comme une structure d'enregistrement. Cette forme de typage est dite structurelle, du fait de cette interprétation.

4.2 La relation de sous-typage

4.2.1 Sous-typage en Java et Typescript

En Java comme en Typescript, à tout type A, il est possible d'associer une structure d'enregistrement S. Ces structures peuvent être munies d'une relation de sous-typage, purement structurelle :

  • une structure S est sous-type d'une structure T si tout membre de T est aussi un membre de S, avec un type compatible, une visibilité compatible et un droit d'accès compatible.

La compatibilité des visibilités s'exprime ainsi :

  • soit préservation des visibilités,
  • soit passage de public à protected, ou de protected à la visibilité par défaut (package) en Java, du sous-type vers le sur-type.

Ce n'est la définition ni la plus générale, ni la plus rigoureuse, comme nous allons le voir. En théorie, la compatibilité des visibilités s'exprime par la propriété suivante : la visibilité doit diminuer du sous-type vers le sur-type.

La compatibilité des droits d'accès n'est pas vérifiée en Typescript, ce qui n'est pas correct en théorie. En Java, elle correspond à une préservation, ce qui n'est pas la définition la plus générale. En théorie, la compatibilité des droits d'accès s'exprime ainsi : les droits d'accès doivent diminuer du sous-type vers le sur-type.

La compatibilité des types peut se réduire à l'identité. En théorie, elle s'exprime ainsi : le type dans le sous-type doit être un sous-type du type dans le sur-type. Nous verrons par la suite les règles de variance qui s'appliquent aux constructeurs de types dans les deux langages. Ces règles expriment si un constructeur de types est croissant ou décroissant en ses arguments, des types, relativement à la relation de sous-typage vue comme une relation d'ordre. Les règles de variance permettent de préciser la compatibilité.

Il devient possible de définir formellement la relation de sous-typage.

Java

  • Relation de sous-typage = fermeture réflexive et transitive de la réunion des deux relations suivantes :
    • relation d'implémentation (implements)
    • relation d'héritage (extends)

Typescript

  • Relation de sous-typage entre types = image réciproque de la relation de sous-typage structurel entre structures associées

En Java comme en Typescript, si A est un sous-type de B, la structure S associée à A est un sous-type structurel de la structure T associée à B. En Java, la réciproque est fausse; en Typescript, elle est vraie.

Comme la relation de sous-typage en Typescript s'appuie sur les structures, elle est plus riche que celle en Java, qui s'appuie sur les déclarations explicites d'héritage et d'implémentation. En typescript, les déclarations d'héritage et d'implémentation impliquent seulement des vérifications de compatibilité, alors qu'en Java, elles impliquent en plus la définition de la relation de sous-typage.

4.2.2 Règle de subsomption, substituabilité

Associé au sous-typage, la règle de subsomption permet une conversion implicite, d'un sous-type vers un sur-type.

Règle de subsomption (lire la règle ainsi : si A est un sous-type de B et e a la type A, alors e a le type B.)

 A sous-type de B    e de type A
---------------------------------
         e de type B

Le système de types utilise cette règle pour convertir implicitement des expressions d'un sous-type vers un sur-type : elle est ainsi utilisée lors d'affectations, lors du passage d'arguments à une fonction.

Exemple montrant la différence entre les deux sortes de typage

  • Java

    interface Representable {
      String representation();
    }
    interface RepresentableBis {
      String representation();
    }
    Representable x = ...;
    RepresentableBis y = x; // Erreur !
    
  • Typescript

    interface Representable {
      representation() : string;
    }
    interface RepresentableBis {
      representation() : string;
    }
    let x : Representable = ...;
    let y : RepresentableBis = x; // Compatible !
    

Si la règle de subsomption s'applique dans tout contexte, alors elle implique qu'une règle de substituabilité \(^{1}\) est valide.

Règle de substituabilité (lire la règle ainsi : si B est un sous-type de A et si le code C paramétré par x est bien typé sous l'hypothèse que x a le type A, alors C est bien typé sous l'hypothèse que x a le type B.)

  B sous-type de A   (x : A) ⊢ C bien typé
-----------------------------------------
         (x : B) ⊢ C  bien typé

La règle de substituabilité est généralement respectée par Typecript et Java. Il existe cependant quelques exceptions, ce qui montre que le règle de subsomption n'est pas applicable dans tout contexte. Voir le répertoire session1/principeSubstitution dédié à Typescript.

Première exception : visibilité protected (Java et Typescript, exemple en Typescript)

class A {
  protected f(): void { }
  testA(x: A) {
    x.f(); // Pas d'erreur
  }
  testB(x: B) {
    // x.f();// Erreur
    (<A>x).f(); // Remède : une conversion explicite
  }
}
class B extends A {
  protected f(): void { }
}

La visibilité de B.f est limitée à B et ses éventuels descendants, alors que celle de A.f est restreinte à A, du fait de la redéfinition dans B. C'est une infraction : la visibilité devrait augmenter en passant de A à B, alors qu'ici elles sont incomparables (pour l'inclusion).

Seconde exception : droit d'accès readonly (Typescript)

class A {
  p: string;
}
class B extends A {
  readonly p: string;
}
function testA(x: A) : void {
  x.p = "problème !"; 
}
function testB(x: B) : void {
  // x.p = "problème !"; // Erreur : 'p' en lecture seulement
  (<A>x).p = "problème !"; // Remède : une conversion explicite
}

Le droit d'accès à p n'est pas restreint de B à A, comme ce devrait l'être.

[1] Cette règle de substituabilité est une forme statique (syntaxique) d'un principe de substitution, dynamique (sémantique), qui exprime l'interprétation sémantique de la relation de sous-typage par une inclusion. Retour

4.3 La liaison tardive

Comme il est possible de redéfinir une méthode ou d'implémenter une méthode dans un sous-type, il se pose la question lors de l'exécution de déterminer le code qui est exécuté. Etudions donc le mécanisme.

  • Contexte : un appel de méthode exp.f(args)
  • A la compilation : résolution du nom f
    • Hypothèse 1: l'expression cible exp a pour type C.
    • Hypothèse 2: les arguments args ont pour type A (une liste de types).
    • Conclusion : le nom f est résolu en une déclaration d'une méthode f(B b) (avec B sur-type de A) dans la structure d'enregistrement associée au type C, une liste de méthodes soit héritées soit déclarées dans C.
  • A l'exécution : liaison du nom f
    • Hypothèse 1 : l'expression cible exp s'évalue en c, une référence à un objet d'une classe D sous-type de C.
    • Hypothèse 2 : les arguments args s'évaluent en a (une liste de valeurs).
    • Conclusion : le nom f est lié dans la structure d'enregistrement associée à D à la méthode correspondant à la déclaration déterminée par la résolution. La méthode est appelée avec les arguments a.

      \(\Rightarrow\) Possible appel d'une méthode spécialisante

4.4 Exercice - Résolution et liaison en pratique

4.4.1 Résolution 1

class Succ ...  {
  ...
  public int val() { ... } // D1
}
class SuccMemoire extends Succ ... { 
  ...
  public int val() { ... } // D2
}
...
  public static void main(String[] args){
    SuccMemoire x = ...;
    ... x.val() ...; // 
  }
Lors de la compilation de l'appel x.val(), en quelle déclaration le nom val se résout-il ?
  • D1
  • D2

4.4.2 Liaison 1

class Succ ...  {
  ...
  public int val() { ... } // D1
}
class SuccMemoire extends Succ ... { 
  ...
  public int val() { ... } // D2
}
...
  public static void main(String[] args){
    SuccMemoire x = new SuccMemoire();
    ... x.val() ...; // 
  }
Lors de l'exécution de l'appel x.val(), à quelle méthode le nom val est-il lié ? Autrement dit, quelle est la méthode qui s'exécute ?
  • D1
  • D2

4.4.3 Résolution 2

class Succ ...  {
  ...
  public int val() { ... } // D1
}
class SuccMemoire extends Succ ... { 
  ...
  public int val() { ... } // D2
}
...
  public static void main(String[] args){
    Succ x = new SuccMemoire();
    ... x.val() ...; // 
  }
Lors de la compilation de l'appel x.val(), en quelle déclaration le nom val se résout-il ?
  • D1
  • D2

4.4.4 Liaison 2

class Succ ...  {
  ...
  public int val() { ... } // D1
}
class SuccMemoire extends Succ ... { 
  ...
  public int val() { ... } // D2
}
...
  public static void main(String[] args){
    Succ x = new SuccMemoire();
    ... x.val() ...; // 
  }
Lors de l'exécution de l'appel x.val(), à quelle méthode le nom val est-il lié ? Autrement dit, quelle est la méthode qui s'exécute ?
  • D1
  • D2

4.5 Exercice

Prévoir le résultat de l'exécution de la fonction principale de la classe session1.exercices.ResolutionLiaison.

Indication : bien distinguer la résolution de la liaison.

Author: Hervé Grall
Version history: v1: 2016-10-06; v2: 2016-10-14[+multiple inheritance]; v3: 2018-10-17[+Typescript]; v4: 2019-10-18[+this, terminology wrt fields].
Comments or questions: Send a mail.
The webpage content is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.