¿Hay alguna manera de simular el concepto 'amigo' de C ++ en Java?


196

Me gustaría poder escribir una clase Java en un paquete que pueda acceder a métodos no públicos de una clase en otro paquete sin tener que convertirla en una subclase de la otra clase. es posible?

Respuestas:


466

Aquí hay un pequeño truco que uso en JAVA para replicar el mecanismo amigo de C ++.

Digamos que tengo una clase Romeoy otra clase Juliet. Están en diferentes paquetes (familia) por razones de odio.

Romeoquiere cuddle Juliety Julietsolo quiere dejarla Romeo cuddle.

En C ++, Julietdeclararía Romeocomo un (amante) friendpero no hay tales cosas en Java.

Aquí están las clases y el truco:

Mujeres primero :

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?");
    }

}

Entonces el método Juliet.cuddlees publicpero necesitas un Romeo.Lovepara llamarlo. Utiliza esto Romeo.Lovecomo una "seguridad de firma" para garantizar que solo se Romeopueda llamar a este método y comprueba que el amor es real para que el tiempo de ejecución arroje un NullPointerExceptionsi es así null.

Ahora muchachos:

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 clase Romeo.Lovees pública, pero su constructor sí private. Por lo tanto, cualquiera puede verlo, pero solo Romeopuede construirlo. Utilizo una referencia estática para Romeo.Loveque la que nunca se usa solo se construya una vez y no afecte a la optimización.

Por lo tanto, Romeopuede cuddle Juliety sólo él puede, porque sólo él puede construir y acceder a un Romeo.Loveejemplo, que es requerido por Julieta cuddleella (o de lo que ella te da palmadas con una NullPointerException).


107
+1 para "abofetearlo con una NullPointerException". Muy impresionante.
Nickolas

2
@Steazy Hay: busque las anotaciones NotNull, NonNull y CheckForNull. Consulte la documentación de su IDE para saber cómo usar y aplicar esas anotaciones. Sé que IntelliJ incorpora esto por defecto y que eclipse necesita un complemento (como FindBugs).
Salomon BRYS

27
Usted puede hacer Romeo's Lovepara el Juliaeterno cambiando el lovecampo para ser final;-).
Matthias

55
@Matthias El campo del amor es estático ... Editaré la respuesta para que sea definitiva;)
Salomon BRYS

12
Todas las respuestas deberían ser así (Y) +1 para el humor y el gran ejemplo.
Zia Ul Rehman Mughal

54

Los diseñadores de Java rechazaron explícitamente la idea de amigo, ya que funciona en C ++. Pones a tus "amigos" en el mismo paquete. La seguridad privada, protegida y empaquetada se aplica como parte del diseño del lenguaje.

James Gosling quería que Java fuera C ++ sin los errores. Creo que sintió que ese amigo fue un error porque viola los principios de la OOP. Los paquetes proporcionan una forma razonable de organizar los componentes sin ser demasiado purista acerca de la POO.

NR señaló que puedes hacer trampa usando la reflexión, pero incluso eso solo funciona si no estás usando SecurityManager. Si activa la seguridad estándar de Java, no podrá hacer trampa con reflexión a menos que escriba una política de seguridad para permitirla específicamente.


11
No quiero ser pedante, pero los modificadores de acceso no son un mecanismo de seguridad.
Greg D

66
Los modificadores de acceso son parte del modelo de seguridad de Java. Me refería específicamente a java.lang.RuntimePermission para la reflexión: accessDeclaredMembers y accessClassInPackage.
David G

54
Si Gosling realmente pensó que friendviolaba la POO (en particular, más que el acceso al paquete), entonces realmente no lo entendió (completamente posible, muchas personas lo malinterpretan).
Konrad Rudolph

8
Los componentes de clase a veces necesitan separarse (por ejemplo, implementación y API, objeto central y adaptador). La protección a nivel de paquete es al mismo tiempo demasiado permisiva y demasiado restrictiva para hacerlo correctamente.
dhardy

2
@GregD Podrían considerarse un mecanismo de seguridad en el sentido de que ayudan a evitar que los desarrolladores utilicen incorrectamente un miembro de la clase. Creo que probablemente sea mejor referirse a ellos como un mecanismo de seguridad .
enamorado

45

El concepto 'amigo' es útil en Java, por ejemplo, para separar una API de su implementación. Es común que las clases de implementación necesiten acceso a los componentes internos de la clase API, pero estos no deben exponerse a los clientes API. Esto se puede lograr usando el patrón 'Friend Accessor' como se detalla a continuación:

La clase expuesta a través de la 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 clase que proporciona la funcionalidad 'amigo':

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);
}

Ejemplo de acceso desde una clase en el paquete de implementación 'amigo':

package impl;

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

1
Porque no sabía lo que significa "estático" en la clase "Expuesta": el bloque estático es un bloque de enunciados dentro de una clase Java que se ejecutará cuando una clase se cargue por primera vez en la JVM Leer más en javatutorialhub. com / ...
Guy L

Patrón interesante, pero requiere que las clases Exposed y Accessor sean públicas, mientras que las clases que implementan una API (es decir, un conjunto de clases Java que implementan un conjunto de interfaces Java públicas) estarían mejor "protegidas por defecto" y, por lo tanto, inaccesibles para el cliente para separar tipos de sus implementaciones.
Yann-Gaël Guéhéneuc

8
Estoy bastante oxidado en mi Java, así que perdona mi ignorancia. ¿Cuál es la ventaja de esto sobre la solución "Romeo y Julieta" que Salomon BRYS publicó? Esta implementación me asustaría si me topara con una base de código (sin su explicación adjunta, es decir, comentarios pesados). El enfoque de Romeo y Julieta es muy simple de entender.
Steazy

1
Este enfoque hará que los problemas sean visibles solo en tiempo de ejecución, mientras que el mal uso de Romeo y Julieta los haría visibles en el momento de la compilación, mientras se desarrollan.
ymajoros

1
@ymajoros El ejemplo de Romeo y Julieta no hace visible el mal uso en tiempo de compilación. Se basa en un argumento que se pasa correctamente y se lanza una excepción. Esas son ambas acciones en tiempo de ejecución.
Radiodef

10

Hay dos soluciones a su pregunta que no implican mantener todas las clases en el mismo paquete.

El primero es usar el patrón de Accesor de amigos / Paquete de amigos descrito en (Diseño práctico de API, Tulach 2008).

El segundo es usar OSGi. Hay un artículo aquí explicando cómo OSGi logra esto.

Preguntas relacionadas: 1 , 2 y 3 .


7

Que yo sepa, no es posible.

Tal vez, podría darnos más detalles sobre su diseño. Preguntas como estas son probablemente el resultado de fallas de diseño.

Solo considera

  • ¿Por qué esas clases están en paquetes diferentes, si están tan estrechamente relacionadas?
  • ¿Tiene A acceso a miembros privados de B o la operación debe ser movida a clase B y activada por A?
  • ¿Esto realmente llama o es mejor el manejo de eventos?

3

La respuesta de eirikma es fácil y excelente. Podría agregar una cosa más: en lugar de tener un método de acceso público, getFriend () para obtener un amigo que no se puede usar, podría ir un paso más allá y no permitir obtener al amigo sin un token: getFriend (Service.FriendToken). Este FriendToken sería una clase pública interna con un constructor privado, por lo que solo el Servicio podría crear una instancia.


3

Aquí hay un claro ejemplo de caso de uso con una Friendclase reutilizable . El beneficio de este mecanismo es la simplicidad de uso. Quizás sea bueno para dar a las clases de prueba unitarias más acceso que el resto de la aplicación.

Para comenzar, aquí hay un ejemplo de cómo usar la Friendclase.

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;
    }
}

Luego, en otro paquete, puede hacer esto:

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

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

los Friend clase es la siguiente.

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);
    }
}

Sin embargo, el problema es que se puede abusar así:

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

Ahora, puede ser cierto que la Otherclase no tiene constructores públicos, por lo que el Abusercódigo anterior es imposible. Sin embargo, si su clase lo hace tener un constructor público, entonces es probable que sea aconsejable duplicar la clase de amigo como una clase interna. Tome esta Other2clase como ejemplo:

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);
    }
}

Y luego la Owner2clase sería así:

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

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

Tenga en cuenta que la Other2.Friendclase tiene un constructor privado, por lo que esta es una forma mucho más segura de hacerlo.


2

La solución provista quizás no fue la más simple. Otro enfoque se basa en la misma idea que en C ++: los miembros privados no son accesibles fuera del paquete / ámbito privado, a excepción de una clase específica que el propietario hace un amigo de sí mismo.

La clase que necesita acceso amigo a un miembro debe crear una "clase amiga" abstracta pública interna a la que la clase propietaria de las propiedades ocultas puede exportar el acceso, devolviendo una subclase que implementa los métodos de implementación de acceso. El método "API" de la clase amigo puede ser privado, por lo que no es accesible fuera de la clase que necesita acceso amigo. Su única declaración es una llamada a un miembro protegido abstracto que implementa la clase exportadora.

Aquí está el código:

Primero, la prueba que verifica que esto realmente funciona:

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());
    }
}

Luego, el Servicio que necesita acceso amigo a un paquete privado miembro de la Entidad:

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();
    }
}

Finalmente: la clase Entity que proporciona acceso amigable a un miembro privado del paquete solo a la clase 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();
            }
        };
    }
}

De acuerdo, debo admitir que es un poco más largo que "servicio de amigos :: Servicio"; pero podría ser posible acortarlo mientras se conserva la verificación en tiempo de compilación mediante anotaciones.


Esto no funciona como una clase normal en el mismo paquete podría simplemente obtenerFriend () y luego llamar al método protegido sin pasar por el privado.
user2219808

1

En Java es posible tener una "amistad relacionada con el paquete". Esto puede ser útil para pruebas unitarias. Si no especifica privado / público / protegido frente a un método, será "amigo en el paquete". Una clase en el mismo paquete podrá acceder a ella, pero será privada fuera de la clase.

Esta regla no siempre se conoce, y es una buena aproximación de una palabra clave "amigo" de C ++. Me parece un buen reemplazo.


1
Esto es cierto, pero realmente estaba preguntando sobre el código que reside en diferentes paquetes ...
Matthew Murdoch

1

Creo que las clases de amigos en C ++ son como el concepto de clase interna en Java. Usando clases internas, puedes definir una clase de cierre y una cerrada. La clase adjunta tiene acceso completo a los miembros públicos y privados de su clase adjunta. ver el siguiente enlace: http://docs.oracle.com/javase/tutorial/java/javaOO/nested.html


Er, no, no lo son. Es más como la amistad en la vida real: puede, pero no tiene que ser, mutuo (A ser amigo de B no significa que B sea considerado amigo de A), y usted y sus amigos pueden ser de una situación totalmente diferente. familias y tener sus propios círculos de amigos, posiblemente (pero no necesariamente) superpuestos. (No es que me gustaría ver clases con muchos amigos. Puede ser una característica útil, pero debe usarse con precaución.)
Christopher Creutzig

1

Creo que el enfoque de usar el patrón de acceso amigo es demasiado complicado. Tuve que enfrentar el mismo problema y lo resolví usando el buen constructor de copias antiguo, conocido de 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;
    }
}

En su aplicación, podría escribir el siguiente código:

public class MyApp {

    private static class ProtectedAccessor extends ProtectedContainer {

        protected ProtectedAccessor() {
            super();
        }

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

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

La ventaja de este método es que solo su aplicación tiene acceso a los datos protegidos. No es exactamente una sustitución de la palabra clave amigo. Pero creo que es bastante adecuado cuando escribe bibliotecas personalizadas y necesita acceder a datos protegidos.

Siempre que tenga que lidiar con instancias de ProtectedContainer, puede envolver su ProtectedAccessor a su alrededor y obtener acceso.

También funciona con métodos protegidos. Los define protegidos en su API. Más adelante en su aplicación, escribe una clase de contenedor privado y expone el método protegido como público. Eso es.


1
¡Pero ProtectedContainerse puede subclasificar fuera del paquete!
Raphael

0

Si desea acceder a métodos protegidos, puede crear una subclase de la clase que desee usar que exponga los métodos que desea usar como públicos (o internos para que el espacio de nombres sea más seguro) y tener una instancia de esa clase en su clase (Úselo como un proxy).

En lo que respecta a los métodos privados (creo), no tienes suerte.


0

Estoy de acuerdo en que en la mayoría de los casos la palabra clave amigo es innecesaria.

  • Package-private (también conocido como default) es suficiente en la mayoría de los casos en los que tiene un grupo de clases muy entrelazadas
  • Para las clases de depuración que desean acceso a elementos internos, generalmente hago que el método sea privado y accedo a él a través de la reflexión. La velocidad generalmente no es importante aquí
  • A veces, implementa un método que es un "hack" o que está sujeto a cambios. Lo hago público, pero uso @Deprecated para indicar que no debes confiar en este método existente.

Y finalmente, si realmente es necesario, existe el patrón de acceso amigo mencionado en las otras respuestas.


0

No usar una palabra clave más o menos.

Podrías "hacer trampa" usando la reflexión, etc., pero no recomendaría "hacer trampa".


3
Consideraría esto una idea tan mala que incluso sugerir que es aborrecible para mí. Obviamente, esto es un cludge en el mejor de los casos, y no debe ser parte de ningún diseño.
shsteimer

0

Un método que he encontrado para resolver este problema es crear un objeto de acceso, así:

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();
    }
}

El primer código para llamar getAccessor()"reclamaciones de propiedad" del descriptor de acceso. Por lo general, este es el código que crea el objeto.

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

Esto también tiene una ventaja sobre el mecanismo amigo de C ++, ya que le permite limitar el acceso en un nivel por instancia , en lugar de un nivel por clase . Al controlar la referencia del descriptor de acceso, usted controla el acceso al objeto. También puede crear múltiples accesores y dar acceso diferente a cada uno, lo que permite un control detallado sobre qué código puede acceder a qué:

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();
    }
}

Finalmente, si desea que las cosas estén un poco más organizadas, puede crear un objeto de referencia, que mantenga todo junto. Esto le permite reclamar todos los accesos con una llamada de método, así como mantenerlos junto con su instancia vinculada. Una vez que tenga la referencia, puede pasar los accesos al código que lo necesita:

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();
    }
}

Después de muchos golpes en la cabeza (no del tipo bueno), esta fue mi solución final, y me gusta mucho. Es flexible, fácil de usar y permite un muy buen control sobre el acceso a clases. (El acceso solo con referencia es muy útil). Si usa protegido en lugar de privado para los accesores / referencias, las subclases de Foo pueden incluso devolver referencias extendidas getReference. Tampoco requiere ningún reflejo, por lo que se puede usar en cualquier entorno.


0

A partir de Java 9, los módulos se pueden utilizar para que esto no sea un problema en muchos casos.


0

Prefiero delegación o composición o clase de fábrica (dependiendo del tema que resulte en este problema) para evitar que sea una clase pública.

Si se trata de un problema de "clases de interfaz / implementación en diferentes paquetes", utilizaría una clase de fábrica pública que estaría en el mismo paquete que el paquete impl y evitaría la exposición de la clase impl.

Si es un problema de "Odio hacer pública esta clase / método solo para proporcionar esta funcionalidad para otra clase en un paquete diferente", entonces usaría una clase de delegado público en el mismo paquete y expondría solo esa parte de la funcionalidad necesitado por la clase "outsider".

Algunas de estas decisiones son impulsadas por la arquitectura de carga de clases del servidor de destino (paquete OSGi, WAR / EAR, etc.), las convenciones de implementación y nomenclatura de paquetes. Por ejemplo, la solución propuesta anteriormente, el patrón 'Friend Accessor' es inteligente para las aplicaciones normales de Java. Me pregunto si se vuelve difícil implementarlo en OSGi debido a la diferencia en el estilo de carga de clases.


0

No sé si es de alguna utilidad para alguien, pero lo manejé de la siguiente manera:

Creé una interfaz (AdminRights).

Cada clase que debería poder llamar a dichas funciones debería implementar AdminRights.

Luego creé una función HasAdminRights de la siguiente manera:

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;
    }
}

-1

Una vez vi una solución basada en la reflexión que hacía "comprobación de amigos" en tiempo de ejecución utilizando la reflexión y comprobando la pila de llamadas para ver si la clase que llamaba al método tenía permitido hacerlo. Al ser un control de tiempo de ejecución, tiene el inconveniente obvio.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.