Héritage, redéfinition et surcharge

Notion de type

Le langage Java est statiquement typé. Cela signifie qu'en Java toute variable doit obligatoirement faire l'objet d'une déclaration dans laquelle on précise le type de la variable. C'est ce type déclaré qu'on appelle le type statique de la variable et qui déterminera les opérations autorisées sur cette dernière. Exemple :

Animal a;
a = new Chien();

Dans cet exemple, la variable a est de type statique Animal.

En Java, il y a deux familles de types :

  • La famille des types primitifs parmi lesquels le type booléen et les types numériques (byte, short, int, long, char, float et double). Dans une variable de type primitif, la zone mémoire associée à cette variable contient une donnée qui n'est pas un objet.
  • La famille des types référence parmi lesquels on a la sous-famille des types classe, la sous-famille des types interface et celle des types tableau. Une variable de type référence permet d'accéder à un objet (sauf quand sa valeur est égale à null). Mais dans la zone mémoire associée à une telle variable, ce n'est jamais l'objet lui-même qui est stocké mais seulement une référence (une sorte de super pointeur) vers cet objet. L'objet, lui, se trouve dans une zone mémoire à part qu'on appelle le tas. Au passage null est une valeur qui n'a pas vraiment de type, c'est simplement la valeur d'une variable de type référence quand celle-ci n'est associée à aucun objet.

On voit donc qu'une classe est un type mais tous les types ne sont pas des classes. Par exemple int est un type mais pas une classe.

Notion de sous-type

Soient T1 et T2 deux types. On dit que T2 est un sous-type de T1 si et seulement si :

  • Ou bien T1 et T2 sont des types primitifs et dans ce cas :
    • ou bien T2 = T1
    • ou bien T1 et T2 se trouvent dans la chaîne ci-contre avec T2 situé plus à gauche que T1 : byte -> short -> int -> long -> float -> double.
    • ou bien T1 et T2 se trouvent dans la chaîne ci-contre avec T2 situé plus à gauche que T1 : char -> int.
  • Ou bien T1 et T2 sont des types référence et alors là... (évitons de rentrer dans un trop grand formalisme) c'est la relation d'héritage des classes qui permettra de savoir si T2 est un sous-type de T1. Par exemple, si la classe Chien hérite de la classe Animal, alors le type Chien est un sous-type du type Animal et le type Chien[ ] (tableau de Chien) est un sous-type du type Animal[ ] (tableau de Animal) etc.

La relation « est un sous-type de » est une relation d'ordre car elle est réflexive (T est un sous-type de T), transitive (si T3 est un sous-type de T2 et si T2 est un sous-type de T1, alors T3 est un sous-type de T1) et antisymétrique (si T1 est un sous-type de T2 et si T2 est un sous-type de T1, alors T1 = T2).

Signature d'une méthode

En Java, la signature d'une méthode est la suite formée, dans l'ordre, par le nom de la méthode, la suite des types des paramètres formels. Par exemple, pour cette méthode :

public int faireTruc(int a, String b) 
{ 
    // Implémentation de la méthode...
}

la signature est :

// Attention :
// 1. Le nom des paramètres formels (a et b) n'a aucune importance.
// 2. L'ordre des types des paramètres formels compte.
faireTruc(int a, String b)

// On peut très bien écrire ceci, c'est la même signature :
faireTruc(int nombre, String description)

// Le compilateur Java utilise souvent cette notation là :
faireTruc(int, String)

Quelques précisions importantes :

  • En Java, le type de retour d'une méthode ne fait pas partie de sa signature.
  • Les noms des paramètres formels de la méthode (s'il y en a) n'ont pas d'importance. Ce qui compte, c'est la suite ordonnée des types des paramètres formels.

Par exemple, voici deux méthodes qui ont des signatures différentes :


public int faireTruc(int a, String b) 
{ 
    // Implémentation de la méthode...
}

public int faireTruc(String a, int b) 
{ 
    // Implémentation de la méthode...
}

Les deux méthodes ci-dessus ont certes le même nom, mais l'ordre des types des paramètres formels est différent. On verra qu'aux yeux du compilateur Java deux méthodes avec des signatures différentes sont purement et simplement deux méthodes totalement distinctes, même si elles ont le même nom.

Tableau des méthodes d'une classe... sans l'héritage

Pour l'instant, nous allons expliquer certaines notions sans tenir compte de ce qu'on appelle l'héritage que l'on verra dans un deuxième temps.

RÈGLE 1 SUR L'UNICITÉ DES SIGNATURES DANS UNE CLASSE

Dans le corps d'une classe donnée, on ne peut pas définir deux méthodes possédant la même signature, c'est interdit par le compilateur.

REMARQUE : en revanche, rien n'interdit de définir dans le corps de la classe deux méthodes ayant le même nom mais des signatures différentes. On dit alors que le nom de la méthode est surchargé.

La signature d'une méthode porte donc bien son nom. Elle permet, dans une classe donnée, d'identifier clairement et sans ambiguïté une méthode (avec son code d'implémentation). D'ailleurs, le compilateur Java utilise souvent ce type de notation :

A.getTruc(int, String)

// Cela signifie : 
// « Dans le corps de la classe A, l'unique méthode ayant 
//   pour signature getTruc(int a, String b) »

Avec cette notation, le compilateur ne mentionne même pas le nom des paramètres formels de la méthode dans la signature car, comme on a vu, la signature de la méthode ne dépend pas du choix des noms de ses paramètres formels (si elle en a bien sûr).

Dans le cas d'une surcharge, nous avons dans le corps de la classe deux méthodes (au moins) ayant le même nom, mais, en vertu de la règle 1, les signatures de chacune des deux méthodes doivent être différentes. Voici un exemple :

class A
{
    String faireTruc(int a, String b)
    {
        // implémentation...
    }
    
    void faireTruc(String a, int b)
    {
        // implémentation...
    }
}

Dans cet exemple, la règle 1 n'est pas bafouée car les signatures sont différentes (l'ordre des types des paramètres a été changé). On dit que le nom de méthode faireTruc a été surchargé. Dans une surcharge, les types de retour peuvent être parfaitement différents d'une méthode à l'autre, comme c'est le cas dans l'exemple ci-dessus. En fait, aux yeux du compilateur Java, les deux méthodes faireTruc(int a, String b) et faireTruc(String a, int b) implémentées dans la classe A sont purement et simplement deux méthodes totalement différentes qui peuvent parfaitement n'avoir aucun rapport entre elles d'un point de vue sémantique, le compilateur s'en moque (pour le programmeur c'est une autre histoire).

Pour une classe donnée A, on peut alors effectuer son tableau des méthodes. Un tableau de méthodes, ça ressemble à ça (exemple totalement fictif) :

Signature Code d'implémentation Type de retour
mf(A a) // le code associé... A
mg(int a, A b) // le code associé... int
mg(int a) // le code associé... int
mg(double a) // le code associé... int
mh(A a) // le code associé... void

Au passage, on peut constater dans cet exemple fictif qu'il y a une surcharge du nom de méthode mg. Chacune des trois méthodes ayant pour nom mg ont d'ailleurs bien des signatures deux à deux distinctes (d'après la règle 1).

Voici maintenant un énoncé équivalent de la règle 1. C'est celui-là qu'il faudra retenir :

RÈGLE 1 SUR L'UNICITÉ DES SIGNATURES DANS UNE CLASSE

Dans le tableau des méthodes d'une classe donnée, toutes les signatures inscrites dans la première colonne sont deux à deux distinctes. Si ce n'est pas le cas, la compilation provoquera une erreur.

Dans le tableau des méthodes d'une classe donnée, si l'on connaît la signature d'une méthode, on connaît alors sans ambiguïté le code d'implémentation ainsi que le type de retour associés. Bref, dans une classe donnée, une signature définit sans ambiguïté une méthode, alors que le nom d'une méthode à lui seul peut s'avérer être une information ambiguë si ce nom est surchargé.

Notations concernant les signatures

On voit bien maintenant qu'une phrase comme « soit la méthode m de la classe A » est ambiguë. De quelle méthode parle-t-on exactement si le nom m est surchargé ? C'est pourquoi dans la suite de l'article on utilisera deux notations :

  • La première est en fait utilisée par le compilateur Java lui-même.
  • La deuxième, quant à elle, est tout à fait personnelle (donc un peu moins légitime que la première) et sera valable seulement pour cet article.

Notation 1 : A.m(int, String) signifiera « l'unique méthode définie dans le corps de la classe A et ayant la signature m(int a, String b). » On pourra écrire directement m(int, String) s'il n'y a pas d'ambiguïté dans le contexte au niveau de la classe considérée.

Notation 2 : pour pouvoir écrire des énoncés généraux, sans rien supposer a priori sur la signature des méthodes (et notamment sur le nombre et les types des paramètres formels), on utilisera la notation A.Sign[S] qui signifiera « l'unique méthode définie dans le corps de la classe A et ayant la signature S où S représente n'importe quelle chaîne de la forme m(int a, String b) ou encore setTruc() etc. »

Armé de tout ce petit formalisme et du concept de tableau de méthodes, on va pouvoir appréhender très sereinement la notion d'héritage.

Quand l'héritage vient mettre son grain de sel

Nous allons parler de l'héritage au niveau des méthodes uniquement et laisser de côté tout ce qui concerne les variables d'instance et les constructeurs. Partons d'un exemple de code dont l'intérêt est purement pédagogique :

class A
{
    A mf(A a)             {return a;}
    int mg(int a, A b)    {return 0;}
    int mh(A a)           {return 0;}
}

class B extends A
{
    B mf(A a)             {B b = new B(); return b;}
    String mg(int a, B b) {return "";}
    String mi(String a)   {return "";}
}

La classe B hérite de la classe A à cause de la présence du mot clé extends. On dit que A est LA super-classe de B ou que B est UNE sous-classe de A. Écrivons le tableau des méthodes de la classe A :

Signature Code d'implémentation Type de retour
mf(A a) return a; A
mg(int a, A b) return 0; int
mh(A a) return 0; int

Pour construire la table des méthodes de la sous-classe B, nous allons appliquer la règle ci-dessous.

RÈGLE 2 SUR L'HÉRITAGE DES MÉTHODES

Pour construire la table des méthodes d'une sous-classe B héritant de A, on part d'abord du tableau des méthodes de la super-classe A. Appelons ce tableau TAB. Une fois que TAB est construit, nous allons le modifier au fur et à mesure. On considère une à une toutes les méthodes B.Sign[S] définies dans le corps de la sous-classe B :

  • Si la signature S de B.Sign[S] ne se trouve pas dans TAB, alors on ajoute directement B.Sign[S] dans TAB. (Si la méthode possède malgré tout un nom se trouvant déjà dans TAB, on aura tout simplement une surcharge) ;
  • Si la signature S de B.Sign[S] se trouve déjà dans TAB, alors cela signifie qu'il y a un conflit avec la méthode A.Sign[S]. Dans ce cas, B.Sign[S] va masquer A.Sign[S] dans TAB. Autrement dit, on va supprimer A.Sign[S] dans TAB pour y mettre à la place B.Sign[S]. Dans ce cas, on dit alors que la méthode A.Sign[S] a été redéfinie par la sous-classe B.

Le deuxième point de la règle 2 garantit le respect de la règle 1, à savoir l'unicité des signatures dans le tableau des méthodes d'une classe.

Appliquons la règle 2 dans notre exemple de code précédent afin d'obtenir le tableau des méthodes de la classe B. Nous partons donc du tableau des méthodes de A qui a été donnée plus haut. Nous allons l'appeler TAB. Ensuite, nous considérons une à une les méthodes se trouvant dans le corps de la classe B. Dans le corps de la classe B, il y a :

  • La méthode B.mf(A). Sa signature se trouve déjà dans TAB, il va donc avoir redéfinition de la méthode A.mf(A). Nous supprimons donc A.mf(A) dans TAB pour y mettre B.mf(A) à la place.
  • La méthode B.mg(int, B). Sa signature ne se trouve pas dans TAB : certes il y a A.mg(int, A) mais ce n'est pas exactement la même signature car le type du deuxième paramètre formel n'est pas le même. On va donc pouvoir ajouter directement la méthode B.mg(int, B) dans TAB sans qu'il ait le moindre conflit.
  • La méthode B.mi(String). Sa signature ne se trouve pas dans TAB, on va donc pouvoir ajouter directement cette méthode dans TAB sans qu'il ait le moindre conflit.

Si vous avez bien suivi la procédure, vous devriez obtenir le tableau des méthodes de la classe B ci-dessous :

Signature Code d'implémentation Type de retour
mf(A a) B b = new B(); return b; B
mg(int a, A b) return 0; int
mg(int a, B b) return ""; String
mh(A a) return 0; int
mi(String a) return ""; String
  • Une ligne totalement en jaune (comme dans le tableau des méthodes de A) correspond à une méthode héritée telle quelle de la super-classe A, sans qu'il y ait eu redéfinition.
  • Une ligne totalement en vert correspond à une méthode qui a été ajoutée par la sous-classe B et qui n'existait tout simplement pas dans la super-classe A.
  • Une ligne en jaune et en vert correspond à une redéfinition de méthode. En effet, dans la ligne 1 du tableau, la signature mf(A) est en jaune car elle se trouvait déjà dans le tableau des méthodes de la super-classe A. En revanche, l'implémentation et le code de retour sont en vert car ils proviennent de la sous-classe B, où une méthode avec la même signature mf(A) a été redéfinie.

Le cas de la méthode B.mg(int, B) (en vert dans le tableau ci-dessus) est intéressant et doit être bien compris. Nous ne sommes pas dans un cas de redéfinition car nous avons pu ajouter cette méthode dans le tableau sans qu'il y ait le moindre conflit de signature (qui entraîne, lui, une redéfinition de méthode). C'est un simple ajout de méthode par rapport à la super-classe. Cependant, cet ajout coexiste avec la méthode A.mg(int, A) héritée de la classe A et portant le même nom que B.mg(int, B). Par conséquent, B.mg(int, B) provoque bien une surcharge du nom mg. La seule subtilité est que cette surcharge du nom mg dans la classe B n'est pas répartie sur la seule classe B, mais sur les deux classes A (dont B hérite) et B.

Avec l'héritage, le tableau des méthodes d'une sous-classe ne peut que grossir (ou au pire rester stable) par rapport à la table de la super-classe et, lors de ce « grossissement », certaines méthodes de la super-classe peuvent être redéfinies par la sous-classe. Mais concernant la redéfinition, des règles doivent être respectées, sous peine d'avoir une erreur à la compilation. Ces règles sont :

RÈGLE 3 SUR LES CONDITIONS REQUISES POUR UNE REDÉFINITON

Soit une méthode A.Sign[S] d'une super-classe A redéfinie en la méthode B.Sign[S] dans une sous-classe B (ce qui implique bien que la signature S est la même dans les deux méthodes). Le compilateur acceptera la redéfinition si et seulement si :

  1. Le type de retour de B.Sign[S] est un sous-type du type de retour de A.Sign[S].
  2. Les droits d'accès de B.Sign[S] ne doivent pas être moins élevés que ceux de A.Sign[S].
  3. La méthode A.Sign[S] n'est pas déclarée final.
  4. Et une condition sur les exceptions et la clause throws, mais ce point là n'est pas traité dans cet article.

Pour comprendre les critères 1 et 2, essentiels, de cette règle, il faut d'abord comprendre ce qu'il se passe à la compilation et à l'exécution lors d'un appel de méthode, ce que l'on verra plus loin dans l'article.

Type statique versus type effectif

Imaginons que, dans le corps d'une méthode, on trouve ce morceau de code :

A a;
a = new B();
// On peut écrire de manière totalement équivalent (et plus concise) :
// A a = new B();
  • Dans la ligne 1, on déclare une variable. C'est obligatoire pour toute variable en Java. Dans la déclaration, on précise le type de la variable et c'est ce type qu'on appelle « type statique de la variable ». Dans la ligne 1, on déclare donc une variable de type statique A.
  • Dans la ligne 2, on stocke dans cette variable une référence à un objet de type B. On dit alors qu'à ce moment de l'exécution du code la variable est de type effectif B. Le type effectif d'une variable à un instant donné est donc le type de l'objet référencé par la variable à cet instant et il peut être différent du type statique de la variable.

Le type statique d'une variable est parfaitement déterminé par le compilateur étant donné qu'il est signalé dans le code source au moment de la déclaration de la variable. À l'exécution, le type statique d'une variable est immuable dans le sens où le celui-ci reste identique durant toute la durée de vie de la variable. En revanche, il n'en est pas de même du type effectif : non seulement celui-ci peut changer durant la durée de vie de la variable mais il est parfois impossible de le connaître autrement qu'au moment de l'exécution du programme. Voici un exemple fictif :

String choice;
Animal a;

choice = user.getChoice();
if (choice.equals("chien")) {
    a = new Chien();
}
else {
    a = new Chat();
}
// Plein de lignes de code...
// Plein de lignes de code...
a = new Ours();

On imagine bien en voyant ce morceau de code que la variable a de type statique Animal verra son type effectif changer au cours de l'exécution du programme : soit le type effectif sera égal à Chien puis à Ours, soit il sera égal à Chat puis Ours. Tout dépend de la réponse de l'utilisateur, qui ne sera connue qu'au moment de l'exécution du programme.

En résumé le type statique d'une variable est définie à la compilation alors que le type effectif d'une variable n'a de sens qu'au moment de l'exécution du programme. Le type effectif d'une variable veut varier au cours de l'exécution du programme. Il peut même être impossible à prédire exactement avant l'exécution du programme. Et avant tout, le type effectif d'une variable peut parfaitement être différent de son type statique (il peut aussi être identique). Le type effectif ne peut pas non plus être égal à n'importe quoi, le compilateur impose des restrictions.

RÈGLE 4 SUR LES RESTRICTIONS DE TYPE LORS D'UNE AFFECTATION

Dans une affectation de la forme A a = new B();, B doit être un sous-type de A sans quoi la compilation indiquera une erreur. Dans une affectation de la forme a = b;, le type statique de la variable b doit être un sous-type du type statique de la variable a.

Remarque : une compilation sans erreur garantit donc que, à chaque instant de l'exécution du programme, une variable sera, soit égale à null (c'est-à-dire qu'elle ne fera référence à aucun objet), soit de type effectif un sous-type du type statique. C'est ça l'idée de base du polymorphisme : manipuler des variables de type statique A qui ne font pas forcément référence à des objets de type A mais qui font aussi référence à des objets de type « sous type de A ».

Finesse de signatures

Avant de passer à la suite, il est nécessaire de comprendre les définitions suivantes un peu formelles mais très simples. Attention, les termes définis ici sont propres à cet article et je ne les ai jamais vus ailleurs. On va utiliser la notation déjà vue sur les signatures de méthodes. Petit rappel :

m(T1, T2, T3)

est la signature d'une méthode de nom m et dont les types des paramètres formels sont successivement T1, T2 et T3.

Signature plus fine ou plus englobante qu'une autre : soient deux signatures Su et Sv. On dit, de manière totalement équivalente, que :

  • la signature Su est plus fine que la signature Sv
  • la signature Sv est plus englobante que la signature Su

si et seulement si Su = m(U1, U2, U3) et Sv = m(V1, V2, V3) avec simultanément :

  • U1 est un sous-type de V1 ;
  • U2 est un sous-type de V2 ;
  • U3 est un sous-type de V3 ;

On remarque que si une signature Su est plus fine ou plus englobante qu'une signature Sv, alors les deux signatures ont le même nom et ont le même nombre de paramètres formels.

Signature LA plus fine parmi n : on considère n signatures. On dit qu'une des signatures est la plus fine de toutes si et seulement si elle est plus fine que chacune des n-1 autres signatures. Au passage, la signature la plus fine parmi n signatures (quand elle existe) est forcément unique (c'est une conséquence du fait que « est un sous-type de » est une relation d'ordre et donc est antisymétrique).

On peut deviner assez facilement l'extension des définitions ci-dessus dans le cas de signatures avec n paramètres. Dans le cas des signatures sans paramètre formel (n = 0) comme dans :

getTruc()

les restrictions sur les types des paramètres formels n'ont plus lieu d'être. Du coup, une signature est plus fine ou plus englobante qu'une autre si et seulement si les deux signatures ne possèdent pas de paramètres formels et si elles ont le même nom, à savoir getTruc ici. Donc la seule signature plus fine ou plus englobante que getTruc() est... getTruc() elle-même.

Appel de méthode à la compilation et à l'exécution

Partons encore une fois d'un exemple simple :

T t; // La variable t est de type statique T
// Plein de lignes de code...
// Plein de lignes de code...
A a = new B();
a.methode("test", t, 50);

Dans la ligne 4, nous déclarons une variable de type statique A et, quand la ligne 4 sera exécutée par le programme, le type effectif de la variable sera B. En vertu de la règle 4, pour que la ligne 4 ne provoque pas d'erreur à la compilation, il faut que B soit un sous-type de A. C'est ce que l'on supposera dorénavant dans toute la suite de cette partie.

Regardons maintenant l'appel de méthode à la ligne 5. Que vérifie exactement le compilateur pour savoir si cette appel de méthode est licite ou non ?

RÈGLE 5 SUR LA COMPILATION PUIS L'EXÉCUTION D'UN APPEL DE MÉTHODE

Lors d'un appel de méthode, le compilateur effectue des vérifications en se basant uniquement sur les types statiques des variables et expressions en présence et inscrit dans le bytecode Java uniquement la signature de la méthode qui sera appelée. À l'exécution, c'est le type effectif de la variable qui décidera de l'implémentation de la méthode appelée.

Plus précisément, lors de l'appel a.m(e1,..., en) avec a une variable de type statique A et e1,..., en des expressions de types statiques respectivement T1,..., Tn :

  1. Si A est un type primitif, alors, dans ce cas, les appels de méthode sont purement et simplement impossibles et le compilateur provoque alors une erreur.
  2. Sinon, A est de type référence. Dans ce cas, le compilateur regarde dans le tableau des méthodes de A la liste des signatures englobant la signature m(T1,..., Tn). Si cette liste est vide, le compilateur provoque alors une erreur.
  3. Sinon, la liste est non vide. Parmi les n signatures de cette liste, le compilateur recherche la signature la plus fine de toutes. Si une telle signature n'existe pas, le compilateur provoque alors une erreur.
  4. Sinon, appelons cette signature S. À ce stade, le compilateur ne provoque pas d'erreur concernant l'appel de méthode et il écrit alors dans le bytecode Java une instruction demandant l'appel sur l'objet référencé par la variable a d'une méthode de signature S. Mais le code d'implémentation à exécuter n'est pas écrit dans le bytecode, seule la signature de la méthode à exécuter est spécifiée.
  5. Une fois la compilation réussie, lors de l'exécution du bytecode Java, l'objet référencé par la variable a effectuera un appel de la méthode de signature S en cherchant l'implémentation correspondant à S dans la table des méthodes du type effectif de a à cet instant, c'est-à-dire du type de l'objet référencé par a à cet instant.

Deux exemples pour finir

Le but est de déterminer le résultat des deux programmes ci-dessous. Si on applique bien les règles expliquées dans cet article, ça ne devrait pas poser de difficulté (les solutions sont données à la fin de l'article) :

Exemple 1 :

class A{

  void m(A a){
    System.out.println("m de A");
  }
  
  void n(A a){
    System.out.println("n de A");
  }
  
}

class B extends A{

  void m(A a){
    System.out.println("m de B");
  }
  
  void n(B b){
    System.out.println("n de B");
  }
  
  public static void main(String[] argv){
    A a = new B();
    B b = new B();
    a.m(b);
    a.n(b);
  }
  
}

Exemple 2 :

class A{

  void m(A a){
	System.out.println("m de A");
  }
  
  void applyM(A a){
    a.m(a);
  }
  
}

class B extends A{

  void m(B b){
    System.out.println("m de B");
  }
  
  public static void main(String[] argv){
    B b = new B();
    b.m(b);
    (new A()).applyM(b);
  }
  
}

Pour l'exemple 2, deux remarques sont nécessaires. Tout d'abord, dans ce type de code :

(new A()).methode(u, v);

l'objet créé s'appelle un objet anonyme car aucune variable ne le référence. Dans ce cas, on peut considérer que tout se passe comme si l'objet était référencé par une variable de type statique égal à celui de l'objet. Autrement dit, le code précédent équivaut à :

A a = new A();  // en supposant que a n'est déclarée nulle part.
a.methode(u, v);

Voici la seconde remarque. Lors de appel d'une méthode, les valeurs des arguments sont affectés aux paramètres formels de la méthode, si bien que, dans le code d'implémentation de la méthode, chaque paramètre formel est toujours de type statique celui donné dans la signature de la méthode. Un exemple sera sans doute plus clair. On considère cette méthode :

public void m(A a, B b)
{
    // code d'implémentation de m
}
Lors de l'appel de méthode suivant :
x.m(e1, e2);

dans la pile d'exécution du programme, c'est ce code là qui sera exécuté :

// Affectations des valeurs des arguments aux paramètres
// formels de la méthode, mais les types statiques des
// paramètres formels sont ceux donnés par la signature de m.
A a = e1;
B b = e2;
// Puis le code d'implémentation de m
// dans lequel a est de type statique A
// et B est de type statique B.

Maintenant, voici la solution des deux exemples :

# Pour l'exemple 1
m de B
n de A
# Pour l'exemple 2
m de B
m de A
Cette entrée a été publiée dans Java. Vous pouvez la mettre en favoris avec ce permalien.

Les commentaires sont fermés.