web-dev-qa-db-fra.com

Existe-t-il un moyen de simuler le concept 'ami' C ++ en Java?

Je voudrais pouvoir écrire une classe Java dans un package qui peut accéder aux méthodes non publiques d'une classe dans un autre package sans avoir à en faire une sous-classe de l'autre classe. possible?

172
Matthew Murdoch

Le concept "ami" est utile en Java, par exemple, pour séparer une API de son implémentation. Il est courant que les classes d'implémentation aient besoin d'accéder aux composants internes des classes de l'API, mais ces derniers ne doivent pas être exposés aux clients de l'API. Ceci peut être réalisé en utilisant le modèle 'Friend Accessor' décrit ci-dessous:

La classe exposée via l'API:

package api;

public final class Exposed {
    static {
        // Declare classes in the implementation package as 'friends'
        Accessor.setInstance(new AccessorImpl());
    }

    // Only accessible by 'friend' classes.
    Exposed() {

    }

    // Only accessible by 'friend' classes.
    void sayHello() {
        System.out.println("Hello");
    }

    static final class AccessorImpl extends Accessor {
        protected Exposed createExposed() {
            return new Exposed();
        }

        protected void sayHello(Exposed exposed) {
            exposed.sayHello();
        }
    }
}

La classe fournissant la fonctionnalité 'ami':

package impl;

public abstract class Accessor {

    private static Accessor instance;

    static Accessor getInstance() {
        Accessor a = instance;
        if (a != null) {
            return a;
        }

        return createInstance();
    }

    private static Accessor createInstance() {
        try {
            Class.forName(Exposed.class.getName(), true, 
                Exposed.class.getClassLoader());
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException(e);
        }

        return instance;
    }

    public static void setInstance(Accessor accessor) {
        if (instance != null) {
            throw new IllegalStateException(
                "Accessor instance already set");
        }

        instance = accessor;
    }

    protected abstract Exposed createExposed();

    protected abstract void sayHello(Exposed exposed);
}

Exemple d'accès à partir d'une classe du package d'implémentation 'friend':

package impl;

public final class FriendlyAccessExample {
    public static void main(String[] args) {
        Accessor accessor = Accessor.getInstance();
        Exposed exposed = accessor.createExposed();
        accessor.sayHello(exposed);
    }
}
44
Matthew Murdoch

Voici un petit truc que j'utilise dans Java pour répliquer le mécanisme de l'ami C++.

Disons que j'ai une classe Romeo et une autre classe Juliet. Ils sont dans différents forfaits (famille) pour des raisons de haine.

Romeo veut cuddleJuliet et Juliet veut uniquement laisser Romeocuddle her.

En C++, Juliet déclarerait Romeo comme un (amoureux) friend, mais il n’existe pas de telles choses en Java.

Voici les cours et le truc:

Les dames d'abord :

package capulet;

import montague.Romeo;

public class Juliet {

    public static void cuddle(Romeo.Love love) {
        Objects.requireNonNull(love);
        System.out.println("O Romeo, Romeo, wherefore art thou Romeo?");
    }

}

Donc, la méthode Juliet.cuddle est public mais vous avez besoin d’un Romeo.Love l'appeler. Il utilise ce Romeo.Love en tant que "sécurité de signature" pour garantir que seul Romeo puisse appeler cette méthode et vérifie que l'amour est réel, de sorte que le moteur d'exécution jette un NullPointerException s'il s'agit de null.

Maintenant les garçons:

package montague;

import capulet.Juliet;

public class Romeo {
    public static final class Love { private Love() {} }
    private static final Love love = new Love();

    public static void cuddleJuliet() {
        Juliet.cuddle(love);
    }
}

La classe Romeo.Love est public, mais son constructeur est private. Par conséquent, tout le monde peut le voir, mais seul Romeo peut le construire. J'utilise une référence statique donc le Romeo.Love qui n’est jamais utilisé n’est construit qu’une fois et n’a pas d’incidence sur l’optimisation.

Par conséquent, Romeo peut cuddleJuliet et lui seul peut le faire car lui seul peut construire et accéder à un Romeo.Love instance, requise par Juliet pour cuddle her (sinon elle vous gifle avec un NullPointerException).

419
Salomon BRYS

Les concepteurs de Java ont explicitement rejeté l'idée de friend car elle fonctionne en C++. Vous placez vos "amis" dans le même package. La sécurité privée, protégée et packagée est appliquée dans le cadre du langage. conception.

James Gosling souhaitait Java être C++ sans les erreurs. Je pense qu'il pensait que cet ami était une erreur car il enfreignait les principes OOP. Les packages fournissent un moyen raisonnable de organiser les composants sans être trop puriste sur la POO.

NR a fait remarquer que vous pouviez tricher en utilisant la réflexion, mais que cela ne fonctionne que si vous n'utilisez pas SecurityManager. Si vous activez la sécurité standard Java, vous ne pourrez pas tricher en réfléchissant sauf si vous écrivez une politique de sécurité pour l’autoriser spécifiquement.

52
David G

Il existe deux solutions à votre question qui ne consistent pas à conserver toutes les classes dans le même package.

La première consiste à utiliser le modèle Friend Accessor/ Friend Package décrit dans (Practical API Design, Tulach 2008).

La seconde consiste à utiliser OSGi. Il existe un article ici expliquant comment OSGi accomplit cette tâche.

Questions connexes: 1 , 2 , et .

10
Jeff Axelrod

Pour autant que je sache, ce n'est pas possible.

Peut-être pourriez-vous nous donner plus de détails sur votre conception. Des questions comme celles-ci sont probablement le résultat de défauts de conception.

Il suffit de considérer

  • Pourquoi ces classes sont-elles dans des packages différents, si elles sont si étroitement liées?
  • Est-ce que A doit accéder aux membres privés de B ou l'opération doit-elle être déplacée en classe B et déclenchée par A?
  • Est-ce vraiment un appel ou la gestion d'événements est-elle meilleure?
8
Black

la réponse de eirikma est simple et excellente. Je pourrais ajouter une dernière chose: au lieu d’avoir une méthode accessible au public, getFriend () pour obtenir un ami qui ne peut pas être utilisé, vous pouvez aller encore plus loin et interdire d’obtenir l’ami sans un jeton: getFriend (Service.FriendToken). FriendToken serait une classe publique interne avec un constructeur privé, de sorte que seul Service puisse en instancier un.

3
AriG

Voici un exemple de cas d'utilisation clair avec une classe Friend réutilisable. L'avantage de ce mécanisme est la simplicité d'utilisation. Peut-être bon pour donner plus d'accès aux classes de tests unitaires que le reste de l'application.

Pour commencer, voici un exemple d'utilisation de la classe Friend.

public class Owner {
    private final String member = "value";

    public String getMember(final Friend friend) {
        // Make sure only a friend is accepted.
        friend.is(Other.class);
        return member;
    }
}

Ensuite, dans un autre paquet, vous pouvez faire ceci:

public class Other {
    private final Friend friend = new Friend(this);

    public void test() {
        String s = new Owner().getMember(friend);
        System.out.println(s);
    }
}

La classe Friend est la suivante.

public final class Friend {
    private final Class as;

    public Friend(final Object is) {
        as = is.getClass();
    }

    public void is(final Class c) {
        if (c == as)
            return;
        throw new ClassCastException(String.format("%s is not an expected friend.", as.getName()));
    }

    public void is(final Class... classes) {
        for (final Class c : classes)
            if (c == as)
                return;
        is((Class)null);
    }
}

Cependant, le problème est que l'on peut en abuser de la manière suivante:

public class Abuser {
    public void doBadThings() {
        Friend badFriend = new Friend(new Other());
        String s = new Owner().getMember(badFriend);
        System.out.println(s);
    }
}

Maintenant, il se peut que la classe Other ne possède aucun constructeur public, rendant ainsi le code Abuser ci-dessus impossible. Cependant, si votre classe est-ce que Si vous avez un constructeur public, il est probablement conseillé de dupliquer la classe Friend en tant que classe interne. Prends ça Other2 classe comme exemple:

public class Other2 {
    private final Friend friend = new Friend();

    public final class Friend {
        private Friend() {}
        public void check() {}
    }

    public void test() {
        String s = new Owner2().getMember(friend);
        System.out.println(s);
    }
}

Et puis le Owner2 La classe serait comme ceci:

public class Owner2 {
    private final String member = "value";

    public String getMember(final Other2.Friend friend) {
        friend.check();
        return member;
    }
}

Notez que le Other2.Friend _ class a un constructeur privé, ce qui en fait un moyen beaucoup plus sûr de le faire.

3
intrepidis

La solution fournie n'était peut-être pas la plus simple. Une autre approche est basée sur la même idée que dans C++: les membres privés ne sont pas accessibles en dehors de la portée package/private, à l'exception d'une classe spécifique dont le propriétaire se fait un ami.

La classe qui a besoin d'un accès ami à un membre doit créer une "classe d'amis" abstraite publique interne à laquelle la classe possédant les propriétés masquées peut exporter l'accès, en renvoyant une sous-classe qui implémente les méthodes d'implémentation d'accès. La méthode "API" de la classe friend peut être privée afin qu'elle ne soit pas accessible en dehors de la classe nécessitant un accès ami. Sa seule instruction est un appel à un membre protégé abstrait que la classe exportatrice implémente.

Voici le code:

D'abord le test qui vérifie que cela fonctionne réellement:

package application;

import application.entity.Entity;
import application.service.Service;
import junit.framework.TestCase;

public class EntityFriendTest extends TestCase {
    public void testFriendsAreOkay() {
        Entity entity = new Entity();
        Service service = new Service();
        assertNull("entity should not be processed yet", entity.getPublicData());
        service.processEntity(entity);
        assertNotNull("entity should be processed now", entity.getPublicData());
    }
}

Ensuite, le service qui a besoin d'un accès ami à un paquet membre privé de l'entité:

package application.service;

import application.entity.Entity;

public class Service {

    public void processEntity(Entity entity) {
        String value = entity.getFriend().getEntityPackagePrivateData();
        entity.setPublicData(value);
    }

    /**
     * Class that Entity explicitly can expose private aspects to subclasses of.
     * Public, so the class itself is visible in Entity's package.
     */
    public static abstract class EntityFriend {
        /**
         * Access method: private not visible (a.k.a 'friendly') outside enclosing class.
         */
        private String getEntityPackagePrivateData() {
            return getEntityPackagePrivateDataImpl();
        }

        /** contribute access to private member by implementing this */
        protected abstract String getEntityPackagePrivateDataImpl();
    }
}

Enfin: la classe Entity qui fournit un accès convivial à un membre privé du package uniquement à la classe application.service.Service.

package application.entity;

import application.service.Service;

public class Entity {

    private String publicData;
    private String packagePrivateData = "secret";   

    public String getPublicData() {
        return publicData;
    }

    public void setPublicData(String publicData) {
        this.publicData = publicData;
    }

    String getPackagePrivateData() {
        return packagePrivateData;
    }

    /** provide access to proteced method for Service'e helper class */
    public Service.EntityFriend getFriend() {
        return new Service.EntityFriend() {
            protected String getEntityPackagePrivateDataImpl() {
                return getPackagePrivateData();
            }
        };
    }
}

D'accord, je dois admettre que c'est un peu plus long que "friend service :: service"; mais il pourrait être possible de le raccourcir tout en conservant la vérification au moment de la compilation en utilisant des annotations.

2
eirikma

Je pense que les classes d'amis en C++ ressemblent au concept de classe interne en Java. En utilisant des classes internes, vous pouvez définir une classe englobante et une classe fermée. La classe incluse a un accès complet aux membres publics et privés de sa classe englobante. voir le lien suivant: http://docs.Oracle.com/javase/tutorial/Java/javaOO/nested.html

1
Sephiroth

Dans Java, il est possible d'avoir une "convivialité liée au paquet". Cela peut être utile pour les tests unitaires. Si vous ne spécifiez pas privé/public/protégé devant une méthode, être "ami dans le paquet": une classe du même paquet pourra y accéder, mais elle sera privée en dehors de la classe.

Cette règle n'est pas toujours connue et constitue une bonne approximation du mot clé "ami" C++. Je trouve que c'est un bon remplaçant.

1
daitangio

Je pense que l’approche consistant à utiliser le modèle d’accesseur ami est trop compliquée. J'ai dû faire face au même problème et j'ai résolu en utilisant le bon et vieux constructeur de copie, connu du C++, en Java:

public class ProtectedContainer {
    protected String iwantAccess;

    protected ProtectedContainer() {
        super();
        iwantAccess = "Default string";
    }

    protected ProtectedContainer(ProtectedContainer other) {
        super();
        this.iwantAccess = other.iwantAccess;
    }

    public int calcSquare(int x) {
        iwantAccess = "calculated square";
        return x * x;
    }
}

Dans votre application, vous pouvez écrire le code suivant:

public class MyApp {

    private static class ProtectedAccessor extends ProtectedContainer {

        protected ProtectedAccessor() {
            super();
        }

        protected PrivateAccessor(ProtectedContainer prot) {
            super(prot);
        }

        public String exposeProtected() {
            return iwantAccess;
        }
    }
}

L'avantage de cette méthode est que seule votre application a accès aux données protégées. Ce n'est pas exactement une substitution du mot clé ami. Mais je pense que c'est tout à fait approprié lorsque vous écrivez des bibliothèques personnalisées et que vous devez accéder à des données protégées.

Chaque fois que vous devez traiter des instances de ProtectedContainer, vous pouvez envelopper votre ProtectedAccessor autour de celui-ci et vous y accéder.

Cela fonctionne aussi avec des méthodes protégées. Vous les définissez protégés dans votre API. Plus tard dans votre application, vous écrivez une classe wrapper privée et exposez la méthode protégée en tant que publique. C'est ça.

1
Chris

Ne pas utiliser un mot-clé ou si.

Vous pouvez "tricher" en utilisant la réflexion, etc., mais je ne recommanderais pas de "tricherie".

0
NR.

Si vous souhaitez accéder à des méthodes protégées, vous pouvez créer une sous-classe de la classe que vous souhaitez utiliser, exposant les méthodes que vous souhaitez utiliser en tant que public (ou internes à l'espace de noms pour être plus sûres), ainsi qu'une instance de cette classe dans votre classe. (utilisez-le comme proxy).

En ce qui concerne les méthodes privées (je pense), vous n'avez pas de chance.

0
Omar Kooheji

Je ne sais pas si cela peut être utile à quiconque mais je l'ai traité de la manière suivante:

J'ai créé une interface (AdminRights).

Chaque classe qui devrait pouvoir appeler lesdites fonctions doit implémenter AdminRights.

Ensuite, j'ai créé une fonction HasAdminRights comme suit:

private static final boolean HasAdminRights()
{
    // Gets the current hierarchy of callers
    StackTraceElement[] Callers = new Throwable().getStackTrace();

    // Should never occur with me but if there are less then three StackTraceElements we can't check
    if (Callers.length < 3)
    {
        EE.InvalidCode("Couldn't check for administrator rights");
        return false;

    } else try
    {

        // Now we check the third element as this function is the first and the function wanting to check for the rights the second. We try to use it as a subclass of AdminRights.
        Class.forName(Callers[2].getClassName()).asSubclass(AdminRights.class);

        // If everything worked up to now, it has admin rights!
        return true;

    } catch (Java.lang.ClassCastException | ClassNotFoundException e)
    {
        // In the catch, something went wrong and we can deduce that the caller has no admin rights

        EE.InvalidCode(Callers[1].getClassName() + " doesn't have administrator rights");
        return false;
    }
}
0
mdre

Je conviens que dans la plupart des cas, le mot clé friend n'est pas nécessaire.

  • Package-private (aka. Default) est suffisant dans la plupart des cas où vous avez un groupe de classes fortement entrelacées
  • Pour les classes de débogage qui veulent accéder aux internes, je rend généralement la méthode privée et j'y accède par réflexion. La vitesse n'est généralement pas importante ici
  • Parfois, vous implémentez une méthode qui est un "hack" ou qui est susceptible de changer. Je le rends public, mais utilisez @Deprecated pour indiquer que vous ne devez pas vous fier à cette méthode existante.

Et enfin, si cela est vraiment nécessaire, il y a le modèle d'accesseur ami mentionné dans les autres réponses.

0
Casebash

Une méthode que j'ai trouvée pour résoudre ce problème consiste à créer un objet d'accès, comme suit:

class Foo {
    private String locked;

    /* Anyone can get locked. */
    public String getLocked() { return locked; }

    /* This is the accessor. Anyone with a reference to this has special access. */
    public class FooAccessor {
        private FooAccessor (){};
        public void setLocked(String locked) { Foo.this.locked = locked; }
    }
    private FooAccessor accessor;

    /** You get an accessor by calling this method. This method can only
     * be called once, so calling is like claiming ownership of the accessor. */
    public FooAccessor getAccessor() {
        if (accessor != null)
            throw new IllegalStateException("Cannot return accessor more than once!");
        return accessor = new FooAccessor();
    }
}

Le premier code à appeler getAccessor() "revendique la propriété" de l'accesseur. Généralement, c'est le code qui crée l'objet.

Foo bar = new Foo(); //This object is safe to share.
FooAccessor barAccessor = bar.getAccessor(); //This one is not.

Cela présente également un avantage par rapport au mécanisme ami de C++, car il vous permet de limiter l'accès à un niveau par instance, par opposition à un niveau par classe. En contrôlant la référence de l'accesseur, vous contrôlez l'accès à l'objet. Vous pouvez également créer plusieurs accesseurs et leur donner un accès différent, ce qui permet un contrôle plus fin du code pouvant accéder à ce qui:

class Foo {
    private String secret;
    private String locked;

    /* Anyone can get locked. */
    public String getLocked() { return locked; }

    /* Normal accessor. Can write to locked, but not read secret. */
    public class FooAccessor {
        private FooAccessor (){};
        public void setLocked(String locked) { Foo.this.locked = locked; }
    }
    private FooAccessor accessor;

    public FooAccessor getAccessor() {
        if (accessor != null)
            throw new IllegalStateException("Cannot return accessor more than once!");
        return accessor = new FooAccessor();
    }

    /* Super accessor. Allows access to secret. */
    public class FooSuperAccessor {
        private FooSuperAccessor (){};
        public String getSecret() { return Foo.this.secret; }
    }
    private FooSuperAccessor superAccessor;

    public FooSuperAccessor getAccessor() {
        if (superAccessor != null)
            throw new IllegalStateException("Cannot return accessor more than once!");
        return superAccessor = new FooSuperAccessor();
    }
}

Enfin, si vous souhaitez que les choses soient un peu plus organisées, vous pouvez créer un objet de référence, qui contient tout. Cela vous permet de réclamer tous les accesseurs avec un seul appel de méthode, ainsi que de les conserver avec leur instance liée. Une fois que vous avez la référence, vous pouvez transmettre les accesseurs au code qui en a besoin:

class Foo {
    private String secret;
    private String locked;

    public String getLocked() { return locked; }

    public class FooAccessor {
        private FooAccessor (){};
        public void setLocked(String locked) { Foo.this.locked = locked; }
    }
    public class FooSuperAccessor {
        private FooSuperAccessor (){};
        public String getSecret() { return Foo.this.secret; }
    }
    public class FooReference {
        public final Foo foo;
        public final FooAccessor accessor;
        public final FooSuperAccessor superAccessor;

        private FooReference() {
            this.foo = Foo.this;
            this.accessor = new FooAccessor();
            this.superAccessor = new FooSuperAccessor();
        }
    }

    private FooReference reference;

    /* Beware, anyone with this object has *all* the accessors! */
    public FooReference getReference() {
        if (reference != null)
            throw new IllegalStateException("Cannot return reference more than once!");
        return reference = new FooReference();
    }
}

Après avoir beaucoup mordu la tête (pas le bon genre), c’était ma solution finale, et j’aime beaucoup. Il est flexible, simple à utiliser et permet un très bon contrôle de l'accès aux classes. (L'accès avec référence seulement est très utile.) Si vous utilisez protected au lieu de private pour les accesseurs/références, les sous-classes de Foo peuvent même renvoyer des références étendues à partir de getReference. De plus, il ne nécessite aucune réflexion et peut donc être utilisé dans n'importe quel environnement.

0
jpfx1342

Je préfère la délégation ou la classe de composition ou d'usine (selon le problème à l'origine de ce problème) pour éviter d'en faire une classe publique.

S'il s'agit d'un problème de "classes d'interface/implémentation dans différents packages", j'utiliserais une classe de fabrique publique qui figurerait dans le même package que le package impl et empêcherait l'exposition de la classe impl.

S'il s'agit d'un problème "je n'aime pas rendre cette classe/méthode publique uniquement pour fournir cette fonctionnalité à une autre classe d'un autre package", j'utiliserais une classe déléguée publique dans le même package et n'exposerais que cette partie de la fonctionnalité. nécessaire par la classe "outsider".

Certaines de ces décisions sont dictées par l'architecture de chargement de classes du serveur cible (ensemble OSGi, WAR/EAR, etc.), le déploiement et les conventions de dénomination des packages. Par exemple, la solution proposée ci-dessus, le modèle 'Friend Accessor' est astucieux pour les applications Java normales). Je me demande si sa mise en œuvre dans OSGi est délicate à cause des différences de style de chargement de classes.

0
Shashi Velur

À partir de Java 9, les modules peuvent être utilisés pour que cela ne pose pas de problème dans de nombreux cas.

0
Raphael