Existe uma maneira de simular o conceito de 'amigo' do C ++ em Java?


196

Eu gostaria de poder escrever uma classe Java em um pacote que possa acessar métodos não públicos de uma classe em outro pacote sem precisar torná-la uma subclasse da outra classe. Isso é possível?

Respostas:


466

Aqui está um pequeno truque que eu uso no JAVA para replicar o mecanismo de amigo do C ++.

Digamos que eu tenho uma aula Romeoe outra aula Juliet. Eles estão em pacotes diferentes (família) por razões de ódio.

Romeoquer cuddle Juliete Julietquer apenas deixá- Romeo cuddlela.

Em C ++, Julietdeclararia Romeocomo um (amante), friendmas não existem tais coisas em java.

Aqui estão as aulas e o truque:

Damas primeiro :

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

}

Portanto, o método Juliet.cuddleé, publicmas você precisa Romeo.Lovechamá-lo. Ele usa isso Romeo.Lovecomo uma "segurança de assinatura" para garantir que apenas seja Romeopossível chamar esse método e verifica se o amor é real para que o tempo de execução gere um NullPointerExceptionse for null.

Agora meninos:

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

A classe Romeo.Loveé pública, mas seu construtor é private. Portanto, qualquer um pode vê-lo, mas apenas Romeopode construí-lo. Eu uso uma referência estática para Romeo.Loveque o que nunca é usado seja construído apenas uma vez e não afete a otimização.

Portanto, Romeopode cuddle Juliete só ele pode, porque só ele pode construir e acesso de um Romeo.Loveexemplo, que é exigido por Julietque cuddleela (ou então ela vai golpeá-lo com um NullPointerException).


107
+1 para "dar um tapa em você com uma NullPointerException". Muito impressionante.
Nickolas

2
@ Steazy Existe: procure as anotações NotNull, NonNull e CheckForNull. Consulte a documentação do seu IDE para descobrir como usar e aplicar essas anotações. Eu sei que o IntelliJ incorpora isso por padrão e que o eclipse precisa de um plug-in (como o FindBugs).
Salomon BRYS

27
Você pode fazer Romeo's Lovepara Juliaa eternidade, alterando o lovecampo para ser final;-).
Matthias

5
@Matthias O campo de amor é estático ... Eu vou editar a resposta para torná-lo final;)
Salomon BRYS

12
Todas as respostas devem ser assim (Y) +1 para o humor e um ótimo exemplo.
Zia Ul Rehman Mughal

54

Os designers de Java rejeitaram explicitamente a idéia de amigo, pois funciona em C ++. Você coloca seus "amigos" no mesmo pacote. A segurança privada, protegida e empacotada é imposta como parte do design do idioma.

James Gosling queria que o Java fosse C ++ sem os erros. Acredito que ele sentiu que esse amigo foi um erro porque viola os princípios de POO. Os pacotes fornecem uma maneira razoável de organizar componentes sem ser muito purista em relação ao POO.

NR apontou que você poderia trapacear usando a reflexão, mas mesmo isso só funciona se você não estiver usando o SecurityManager. Se você ativar a segurança padrão Java, não poderá enganar-se com reflexão, a menos que escreva uma política de segurança para permitir especificamente.


11
Não pretendo ser um pedante, mas os modificadores de acesso não são um mecanismo de segurança.
Greg D

6
Modificadores de acesso fazem parte do modelo de segurança java. Eu estava me referindo especificamente a java.lang.RuntimePermission para reflexão: accessDeclaredMembers e accessClassInPackage.
David G

54
Se Gosling realmente pensou que friendviolava a OOP (em particular, mais do que o acesso a pacotes), ele realmente não a entendeu (inteiramente possível, muitas pessoas entendem mal).
Konrad Rudolph

8
Às vezes, os componentes de classe precisam ser separados (por exemplo, implementação e API, objeto principal e adaptador). A proteção no nível do pacote é ao mesmo tempo muito permissiva e muito restritiva para fazer isso corretamente.
dhardy

2
@GregD Eles poderiam ser considerados um mecanismo de segurança no sentido de ajudar a impedir que os desenvolvedores usem um membro da classe incorretamente. Eu acho que eles provavelmente deveriam ser referidos como um mecanismo de segurança .
esmagar

45

O conceito de 'amigo' é útil em Java, por exemplo, para separar uma API de sua implementação. É comum que as classes de implementação precisem acessar as classes internas da API, mas elas não devem ser expostas aos clientes da API. Isso pode ser alcançado usando o padrão 'Friend Accessor', conforme detalhado abaixo:

A classe exposta através da 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();
        }
    }
}

A classe que fornece a funcionalidade 'friend':

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

Exemplo de acesso de uma classe no pacote de implementação 'friend':

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 eu não sabia o que significa "estático" na classe "Exposta": O bloco estático é um bloco de instrução dentro de uma classe Java que será executado quando uma classe for carregada pela primeira vez na JVM. Leia mais em javatutorialhub. com / ...
Guy L

Padrão interessante, mas requer que as classes Exposed e Accessor sejam públicas, enquanto as classes que implementam uma API (ou seja, um conjunto de classes Java que implementam um conjunto de interfaces públicas Java) seriam melhor "protegidas por padrão" e, portanto, inacessíveis ao cliente separar tipos de suas implementações.
Yann-Gaël Guéhéneuc

8
Estou bastante enferrujado no meu Java, então perdoe minha ignorância. Qual é a vantagem disso em relação à solução "Romeu e Julieta" divulgada pela Salomon BRYS? Essa implementação me assustaria se eu a encontrasse em uma base de código (sem a sua explicação anexada, isto é, comentários pesados). A abordagem de Romeu e Julieta é muito simples de entender.
Steazy

1
Essa abordagem tornará os problemas visíveis apenas no tempo de execução, enquanto o uso indevido de Romeu e Julieta os tornaria visíveis no tempo de compilação, durante o desenvolvimento.
ymajoros

1
@ymajoros O exemplo de Romeu e Julieta não torna visível o uso indevido em tempo de compilação. Ele conta com um argumento sendo passado corretamente e uma exceção sendo lançada. Essas são as ações em tempo de execução.
Radiodef 24/08/19

10

Existem duas soluções para sua pergunta que não envolvem manter todas as classes no mesmo pacote.

A primeira é usar o padrão Friend Accessor / Friend Package descrito em (Practical API Design, Tulach 2008).

O segundo é usar o OSGi. Há um artigo aqui explicando como o OSGi realiza isso.

Perguntas relacionadas: 1 , 2 e 3 .


7

Tanto quanto eu sei, não é possível.

Talvez você possa nos dar mais alguns detalhes sobre o seu design. Perguntas como essas provavelmente são o resultado de falhas de design.

Apenas considere

  • Por que essas classes estão em pacotes diferentes, se estão tão intimamente relacionadas?
  • A deve acessar membros privados de B ou a operação deve ser movida para a classe B e acionada por A?
  • Isso realmente está chamando ou a manipulação de eventos é melhor?

3

A resposta da eirikma é fácil e excelente. Posso acrescentar mais uma coisa: em vez de ter um método acessível ao público, getFriend () para obter um amigo que não pode ser usado, você pode dar um passo adiante e impedir que ele seja aceito sem um token: getFriend (Service.FriendToken). Esse FriendToken seria uma classe pública interna com um construtor privado, para que apenas o Serviço pudesse instanciar um.


3

Aqui está um exemplo claro de caso de uso com uma Friendclasse reutilizável . O benefício desse mecanismo é a simplicidade de uso. Talvez seja bom para dar mais acesso às classes de teste de unidade que o restante do aplicativo.

Para começar, aqui está um exemplo de como usar a Friendclasse.

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

Em outro pacote, você pode fazer isso:

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

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

A Friendclasse é a seguinte.

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

No entanto, o problema é que ele pode ser abusado da seguinte maneira:

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

Agora, pode ser verdade que a Otherclasse não tenha nenhum construtor público, tornando Abuserimpossível o código acima . No entanto, se sua classe não tem um construtor público, então provavelmente é aconselhável duplicar a classe amigo de uma classe interna. Tome esta Other2classe como um exemplo:

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

E então a Owner2classe seria assim:

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

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

Observe que a Other2.Friendclasse tem um construtor privado, tornando assim uma maneira muito mais segura de fazê-lo.


2

A solução fornecida talvez não fosse a mais simples. Outra abordagem é baseada na mesma idéia do C ++: membros privados não são acessíveis fora do pacote / escopo privado, exceto para uma classe específica em que o proprietário faz um amigo.

A classe que precisa de acesso de amigo a um membro deve criar uma "classe de amigo" abstrata pública interna à qual a classe que possui as propriedades ocultas pode exportar o acesso, retornando uma subclasse que implementa os métodos de implementação de acesso. O método "API" da classe de amigo pode ser privado, portanto, não é acessível fora da classe que precisa de acesso de amigo. Sua única declaração é uma chamada para um membro protegido abstrato que a classe exportadora implementa.

Aqui está o código:

Primeiro, o teste que verifica se isso 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());
    }
}

Em seguida, o Serviço que precisa de acesso de amigo a um membro privado do pacote da Entidade:

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: a classe Entity que fornece acesso amigável a um membro privado do pacote apenas para a 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();
            }
        };
    }
}

Ok, devo admitir que é um pouco mais longo que "friend service :: Service;" mas pode ser possível reduzi-lo, mantendo a verificação em tempo de compilação usando anotações.


Isso não funciona muito bem, pois uma classe normal no mesmo pacote poderia apenas getFriend () e, em seguida, chamar o método protegido ignorando o método privado.
user2219808

1

Em Java, é possível ter uma "amizade relacionada ao pacote". Isso pode ser útil para testes de unidade. Se você não especificar privado / público / protegido na frente de um método, ele será "amigo no pacote". Uma classe no mesmo pacote poderá acessá-la, mas será privada fora da classe.

Essa regra nem sempre é conhecida e é uma boa aproximação de uma palavra-chave "amigo" em C ++. Acho um bom substituto.


1
Isso é verdade, mas eu estava realmente perguntando sobre o código residente em diferentes pacotes ...
Matthew Murdoch

1

Eu acho que as classes de amigo em C ++ são como o conceito de classe interna em Java. Usando classes internas, você pode realmente definir uma classe anexa e uma classe fechada. A classe fechada tem acesso total aos membros públicos e privados da classe envolvente. consulte o seguinte link: http://docs.oracle.com/javase/tutorial/java/javaOO/nested.html


Er, não, eles não são. É mais como amizade na vida real: pode, mas não precisa ser, mútua (A ser amigo de B não significa que B é considerado amigo de A), e você e seus amigos podem ser de pessoas totalmente diferentes. famílias e ter seus próprios círculos de amigos, possivelmente (mas não necessariamente) sobrepostos. (Não que eu gostaria de ver as classes com muitos amigos Ele pode ser um recurso útil, mas deve ser usado com cautela..)
Christopher Creutzig

1

Eu acho que a abordagem de usar o padrão de acessador de amigos é muito complicada. Eu tive que enfrentar o mesmo problema e resolvi usando o bom e antigo construtor de cópias, conhecido em C ++, em 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;
    }
}

No seu aplicativo, você pode escrever o seguinte 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;
        }
    }
}

A vantagem desse método é que apenas seu aplicativo tem acesso aos dados protegidos. Não é exatamente uma substituição da palavra-chave friend. Mas acho que é bastante adequado quando você escreve bibliotecas personalizadas e precisa acessar dados protegidos.

Sempre que você precisar lidar com instâncias do ProtectedContainer, envolva seu ProtectedAccessor e obtenha acesso.

Também funciona com métodos protegidos. Você os define protegidos em sua API. Posteriormente em seu aplicativo, você escreve uma classe de invólucro particular e expõe o método protegido como público. É isso aí.


1
Mas ProtectedContainerpode ser subclassificado fora do pacote!
Raphael

0

Se você deseja acessar métodos protegidos, pode criar uma subclasse da classe que deseja usar que expõe os métodos que você deseja usar como público (ou interno para o namespace para ser mais seguro) e ter uma instância dessa classe em sua classe. (use-o como um proxy).

No que diz respeito aos métodos privados (eu acho), você está sem sorte.


0

Concordo que, na maioria dos casos, a palavra-chave friend é desnecessária.

  • O pacote privado (também conhecido como padrão) é suficiente na maioria dos casos em que você tem um grupo de classes fortemente interligadas
  • Para classes de depuração que desejam acesso a componentes internos, geralmente torno o método privado e o acesso via reflexão. A velocidade geralmente não é importante aqui
  • Às vezes, você implementa um método que é um "hack" ou está sujeito a alterações. Torno-o público, mas use @ Deprecated para indicar que você não deve confiar nesse método existente.

E, finalmente, se for realmente necessário, existe o padrão de acessador de amigo mencionado nas outras respostas.


0

Não está usando uma palavra-chave ou algo assim.

Você poderia "trapacear" usando reflexão etc., mas eu não recomendaria "trapacear".


3
eu consideraria essa uma idéia tão ruim que até sugerir que é abominável para mim. Obviamente, isso é uma confusão, na melhor das hipóteses, e não deve fazer parte de nenhum design.
shsteimer 25/11/08

0

Um método que encontrei para resolver esse problema é criar um objeto acessador, assim:

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

O primeiro código a chamar getAccessor()"reivindica propriedade" do acessador. Geralmente, esse é o código que cria o objeto.

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

Isso também tem uma vantagem sobre o mecanismo de amigo do C ++, porque permite limitar o acesso em um nível por instância , em oposição a um nível por classe . Controlando a referência do acessador, você controla o acesso ao objeto. Você também pode criar vários acessadores e conceder acesso diferente a cada um, o que permite um controle refinado sobre qual código pode acessar o que:

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

Por fim, se quiser que as coisas sejam um pouco mais organizadas, você pode criar um objeto de referência, que mantém tudo junto. Isso permite reivindicar todos os acessadores com uma chamada de método, além de mantê-los juntos com sua instância vinculada. Depois de ter a referência, você pode passar os acessadores para o código que precisa:

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

Depois de muita pancada na cabeça (não do tipo bom), essa foi minha solução final, e eu gostei muito. É flexível, simples de usar e permite um controle muito bom sobre o acesso à classe. (O acesso somente com referência é muito útil.) Se você usar protegido em vez de privado para os acessadores / referências, as subclasses de Foo podem até retornar referências estendidas de getReference. Também não requer reflexão, portanto pode ser usado em qualquer ambiente.


0

A partir do Java 9, os módulos podem ser usados ​​para tornar isso um problema não em muitos casos.


0

Prefiro delegação ou composição ou classe de fábrica (dependendo do problema que resulta nesse problema) para evitar torná-lo uma classe pública.

Se for um problema "interface / classes de implementação em pacotes diferentes", eu usaria uma classe de fábrica pública que estaria no mesmo pacote que o pacote impl e impediria a exposição da classe impl.

Se for um problema "Eu odeio tornar esta classe / método público apenas para fornecer essa funcionalidade a alguma outra classe em um pacote diferente", eu usaria uma classe delegada pública no mesmo pacote e exporia apenas essa parte da funcionalidade necessário pela classe "de fora".

Algumas dessas decisões são orientadas pela arquitetura de carregamento de classe do servidor de destino (pacote configurável OSGi, WAR / EAR etc.), convenções de implementação e nomeação de pacotes. Por exemplo, a solução proposta acima, o padrão 'Friend Accessor' é inteligente para aplicativos java normais. Gostaria de saber se fica complicado implementá-lo no OSGi devido à diferença no estilo de carregamento de classe.


0

Eu não sei se é de alguma utilidade para alguém, mas eu lidei com isso da seguinte maneira:

Eu criei uma interface (AdminRights).

Toda classe que deve ser capaz de chamar essas funções deve implementar AdminRights.

Então eu criei uma função HasAdminRights da seguinte maneira:

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

Certa vez, vi uma solução baseada em reflexão que fazia "verificação de amigos" em tempo de execução usando reflexão e verificação da pilha de chamadas para ver se a classe que chamava o método tinha permissão para fazê-lo. Sendo uma verificação de tempo de execução, tem a desvantagem óbvia.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.