web-dev-qa-db-fra.com

Substitution de méthodes privées dans Java

Comme succinctement décrit ici , remplacer les méthodes privées dans Java n'est pas valide car les méthodes privées d'une classe parent sont "automatiquement finales et cachées de la classe dérivée". Ma question est largement académique.

Comment ne constitue-t-il pas une violation de l'encapsulation de ne pas permettre que la méthode privée d'un parent soit "surchargée" (c'est-à-dire implémentée indépendamment, avec la même signature, dans une classe enfant)? La méthode privée d'un parent ne peut pas être accédée ou héritée par une classe enfant, conformément aux principes d'encapsulation. C'est caché.

Alors, pourquoi la classe enfant devrait-elle être empêchée d'implémenter sa propre méthode avec le même nom/signature? Y a-t-il une bonne base théorique pour cela, ou s'agit-il simplement d'une solution pragmatique quelconque? D'autres langages (C++ ou C #) ont-ils des règles différentes à ce sujet?

57
Bill

Vous ne pouvez pas remplacer une méthode privée, mais vous pouvez en introduire une dans une classe dérivée sans problème. Cela compile bien:

class Base
{
   private void foo()
   {
   }
}

class Child extends Base
{
    private void foo()
    {
    }
}

Notez que si vous essayez d'appliquer l'annotation @Override À Child.foo() vous obtiendrez une erreur au moment de la compilation. Tant que votre compilateur/IDE est défini pour vous donner des avertissements ou des erreurs si vous êtes manquant une annotation @Override, Tout devrait bien se passer. Certes, je préfère l'approche C # de override étant un mot-clé, mais il était évidemment trop tard pour le faire en Java.

Quant à la gestion par C # de la "substitution" d'une méthode privée - une méthode privée ne peut pas être virtuelle en premier lieu, mais vous pouvez certainement introduire une nouvelle méthode privée avec le même nom qu'une méthode privée dans la classe de base.

70
Jon Skeet

Eh bien, autoriser l'écrasement de méthodes privées entraînera soit une fuite d'encapsulation, soit un risque pour la sécurité. Si nous supposons que c'était possible, nous obtiendrions la situation suivante:

  1. Disons qu'il y a une méthode privée boolean hasCredentials() alors une classe étendue pourrait simplement la remplacer comme ceci:

    boolean hasCredentials() { return true; }
    

    rompant ainsi le contrôle de sécurité.

  2. La seule façon pour la classe d'origine d'éviter cela serait de déclarer sa méthode final. Mais maintenant, ce sont des informations d'implémentation des fuites à travers l'encapsulation, car une classe dérivée maintenant ne peut plus créer une méthode hasCredentials plus - elle le ferait entrer en conflit avec celui défini dans la classe de base.

    C'est mauvais: disons que cette méthode n'existe pas au début dans Base. Maintenant, un implémenteur peut légitimement dériver une classe Derived et lui donner une méthode hasCredentials qui fonctionne comme prévu.

    Mais maintenant, une nouvelle version de la classe Base d'origine est publiée. Son interface publique ne change pas (et ses invariants non plus), nous devons donc nous attendre à ce qu'il ne casse pas le code existant. Seulement, car maintenant il y a un conflit de noms avec une méthode dans une classe dérivée.

Je pense que la question découle d'un malentendu:

Comment est-ce/non/une violation de l'encapsulation de ne pas permettre à la méthode privée d'un parent d'être "surchargée" (c'est-à-dire implémentée indépendamment, avec la même signature, dans une classe enfant)

Le texte entre parenthèses est le opposé du texte qui le précède. Java ne vous permet "d'implémenter indépendamment [une méthode privée], avec la même signature, dans une classe enfant "Ne pas autoriser cela violerait l'encapsulation, comme je l'ai expliqué ci-dessus.

Mais "ne pas permettre que la méthode privée d'un parent soit" surchargée "" est quelque chose de différent, et nécessaire pour assurer l'encapsulation.

27
Konrad Rudolph

"Les autres langages (C++ ou C #) ont-ils des règles différentes à ce sujet?"

Eh bien, C++ a des règles différentes: le processus de liaison des fonctions membres statiques ou dynamiques et l'application des privilèges d'accès sont orthogonaux.

Donner à une fonction membre le modificateur de privilèges d'accès private signifie que cette fonction ne peut être appelée que par sa classe déclarante, pas par d'autres (pas même les classes dérivées). Lorsque vous déclarez une fonction membre private en tant que virtual, même pure virtuelle (virtual void foo() = 0;), vous permettez à la classe de base de bénéficier de la spécialisation tout en appliquant les privilèges d'accès.

En ce qui concerne les fonctions membres virtual, les privilèges d'accès vous indiquent ce que vous êtes censé faire:

  • private virtual signifie que vous êtes autorisé à spécialiser le comportement mais l'invocation de la fonction membre est faite par la classe de base, sûrement de façon contrôlée
  • protected virtual signifie que vous devez/devez appeler la version de classe supérieure de la fonction membre lors de sa substitution

Ainsi, en C++, les privilèges d'accès et la virtualité sont indépendants l'un de l'autre. Déterminer si la fonction doit être liée statiquement ou dynamiquement est la dernière étape de la résolution d'un appel de fonction.

Enfin, le modèle de conception de la méthode modèle doit être préféré à public virtual fonctions membres.

Référence: Conversations: pratiquement la vôtre

L'article donne une utilisation pratique d'un private virtual fonction membre.


ISO/CEI 14882-2003 §3.4.1

La recherche de nom peut associer plusieurs déclarations à un nom si elle trouve que le nom est un nom de fonction; on dit que les déclarations forment un ensemble de fonctions surchargées (13.1). La résolution de surcharge (13.3) a lieu après que la recherche de nom a réussi. Les règles d'accès (article 11) ne sont prises en compte qu'une fois la recherche de nom et la résolution de surcharge de fonction (le cas échéant) réussies. Ce n'est qu'après la recherche du nom, la résolution de surcharge de fonction (le cas échéant) et la vérification d'accès que les attributs introduits par la déclaration du nom sont utilisés plus avant dans le traitement des expressions (article 5).

ISO/CEI 14882-2003 §5.2.2

La fonction appelée dans un appel de fonction membre est normalement sélectionnée en fonction du type statique de l'expression d'objet (article 10), mais si cette fonction est virtuelle et n'est pas spécifiée à l'aide de aqualified-id, la fonction réellement appelée sera l'outrider final (10.3) de la fonction sélectionnée dans le type dynamique de l'expression d'objet [Remarque: le type dynamique est le type de l'objet pointé ou référencé par la valeur actuelle de l'expression d'objet.

16
Gregory Pakosz

La méthode privée d'un parent ne peut pas être accédée ou héritée par une classe enfant, conformément aux principes d'encapsulation. C'est caché.

Alors, pourquoi la classe enfant devrait-elle être empêchée d'implémenter sa propre méthode avec le même nom/signature?

Il n'y a pas une telle restriction. Vous pouvez le faire sans aucun problème, ce n'est tout simplement pas appelé "prioritaire".

Les méthodes remplacées sont soumises à une répartition dynamique, c'est-à-dire que la méthode qui est réellement appelée est sélectionnée au moment de l'exécution en fonction du type réel de l'objet sur lequel elle est appelée. Avec la méthode privée, cela ne se produit pas (et ne devrait pas, selon votre première déclaration). Et c'est ce que signifie l'expression "les méthodes privées ne peuvent pas être remplacées".

7
Michael Borgwardt

Je pense que vous avez mal interprété ce que dit ce post. C'est pas dire que la classe enfant est "empêchée d'implémenter sa propre méthode avec le même nom/signature".

Voici le code, légèrement édité:

public class PrivateOverride {
  private static Test monitor = new Test();

  private void f() {
    System.out.println("private f()");
  }

  public static void main(String[] args) {
    PrivateOverride po = new Derived();
    po.f();
    });
  }
}

class Derived extends PrivateOverride {
  public void f() {
    System.out.println("public f()");
  }
}

Et la citation:

Vous pouvez raisonnablement vous attendre à ce que la sortie soit "public f ()",

La raison de cette citation est que la variable po contient en fait une instance de Derived. Cependant, comme la méthode est définie comme privée, le compilateur examine en fait le type de la variable plutôt que le type de l'objet. Et il traduit l'appel de méthode en invokespecial (je pense que c'est le bon opcode, je n'ai pas vérifié les spécifications JVM) plutôt qu'en invokeinstance.

3
kdgregory

Cela semble être une question de choix et de définition. La raison pour laquelle vous ne pouvez pas faire cela dans Java est parce que la spécification le dit, mais la question était plutôt pourquoi la spécification le dit.

Le fait que C++ le permette (même si nous utilisons un mot-clé virtuel pour forcer la répartition dynamique) montre qu'il n'y a aucune raison inhérente pour laquelle vous ne pouvez pas autoriser cela.

Cependant, il semble parfaitement légal de remplacer la méthode:

class B {
    private int foo() 
    {
        return 42;
    }

    public int bar()
    {
        return foo();
    }
}

class D extends B {
    private int foo()
    {
        return 43;
    }

    public int frob()
    {
        return foo();
    }
}

Semble compiler OK (sur mon compilateur), mais le D.foo n'est pas lié à B.foo (c'est-à-dire qu'il ne le remplace pas) - bar () renvoie toujours 42 (en appelant B.foo) et frob () toujours renvoie 43 (en appelant D.foo), qu'il soit appelé sur une instance B ou D.

Une des raisons pour lesquelles Java n'autorise pas le remplacement de la méthode est qu'ils n'aimaient pas autoriser la modification de la méthode comme dans l'exemple de Konrad Rudolph. Notez que C++ diffère ici car vous devez utiliser le mot-clé "virtuel" afin d'obtenir une répartition dynamique - par défaut, il ne vous permet donc pas de modifier le code dans la classe de base qui s'appuie sur la méthode hasCredentials. L'exemple ci-dessus protège également contre cela car D.foo ne remplace pas appels à foo de B.

2
skyking

Lorsque la méthode est privée, elle n'est pas visible pour son enfant. Il n'y a donc aucun sens à le remplacer.

0
GuruKulki

Je m'excuse d'avoir utilisé le terme remplacer de manière incorrecte et incompatible avec ma description. Ma description décrit le scénario. Le code suivant étend l'exemple de Jon Skeet pour décrire mon scénario:

class Base {
   public void callFoo() {
     foo();
   }
   private void foo() {
   }
}

class Child extends Base {
    private void foo() {
    }
}

L'utilisation est la suivante:

Child c = new Child();
c.callFoo();

Le problème que j'ai rencontré est que la méthode parent foo () était appelée même si, comme le montre le code, j'appelais callFoo () sur la variable d'instance enfant. Je pensais que je définissais une nouvelle méthode privée foo () dans Child () que la méthode héritée callFoo () appellerait, mais je pense qu'une partie de ce que kdgregory a dit peut s'appliquer à mon scénario - probablement en raison de la façon dont le constructeur de classe dérivé appelle super (), ou peut-être pas.

Il n'y avait aucun avertissement du compilateur dans Eclipse et le code s'est compilé. Le résultat était inattendu.

0
Bill

Au-delà de tout ce qui a été dit auparavant, il y a une raison très sémantique de ne pas autoriser le dépassement des méthodes privées ... ILS SONT PRIVÉS !!!

Si j'écris une classe et que j'indique qu'une méthode est "privée", elle devrait être complètement invisible pour le monde extérieur. Personne ne devrait pouvoir y accéder, le remplacer ou quoi que ce soit d'autre. Je devrais simplement pouvoir savoir que c'est MA méthode exclusivement et que personne d'autre ne va s'en moquer ou en dépendre. Il ne pourrait pas être considéré comme privé si quelqu'un pouvait s'en moquer. Je crois que c'est vraiment aussi simple que cela.

0
Steve