Есть ли способ имитировать концепцию "друга" C++ в Java?


Я хотел бы иметь возможность написать класс Java в одном пакете, который может получить доступ к непубличным методам класса в другом пакете без необходимости делать его подклассом другого класса. Это возможно?

18 161

18 ответов:

вот небольшой трюк, который я использую в JAVA для репликации механизма друга C++.

допустим у меня есть класс Romeo и еще один класс Juliet. Они находятся в разных пакетах (семья) по причинам ненависти.

Romeo хочет cuddleJuliet и Juliet хочет только пусть Romeocuddle ее.

В C++, Juliet объявить Romeo как (любовника) friend но в java таких вещей нет.

здесь есть классы и трюк:

дамы вперед :

package capulet;

import montague.Romeo;

public class Juliet {

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

}

Итак метод Juliet.cuddle и public но вам нужно Romeo.Love для его вызова. Он использует это Romeo.Love как "безопасность подписи", чтобы гарантировать, что только Romeo может вызвать этот метод и просто вызывает hashCode на него так время выполнения будет бросать NullPointerException если это null.

теперь мальчики :

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

класс Romeo.Love является публичным, но его конструктор private. Следовательно любой может это увидеть, но только Romeo можете построить его. Я использую статическую ссылку так Romeo.Love то, что никогда не используется, строится только один раз и не влияет на оптимизацию.

таким образом, Romeo можете cuddleJuliet и только он может, потому что только он может построить и получить доступ к Romeo.Love экземпляр, который требуется Juliet до cuddle ее (иначе она даст тебе пощечину NullPointerException).

дизайнеры Java явно отвергли идею друга, как это работает в C++. Вы кладете своих "друзей" в тот же пакет. Частная, защищенная и упакованная безопасность обеспечивается как часть языкового дизайна.

Джеймс Гослинг хотел, чтобы Java была C++ без ошибок. Я считаю, что он чувствовал, что друг был ошибкой, потому что это нарушает принципы ООП. Пакеты обеспечивают разумный способ организации компонентов, не будучи слишком пуристским в отношении ООП.

NR указал, что вы можете обмануть с помощью отражения, но даже это работает только в том случае, если вы не используете SecurityManager. Если вы включите стандартную безопасность Java, вы не сможете обмануть отражение, если вы не напишете политику безопасности, чтобы специально разрешить ее.

концепция "друга" полезна в Java, например, чтобы отделить API от его реализации. Обычно для классов реализации требуется доступ к внутренним элементам класса API, но они не должны предоставляться клиентам API. Это может быть достигнуто с помощью шаблона "друг доступа", как описано ниже:

класс, открытый через 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();
        }
    }
}

класс, обеспечивающий функциональность "друга":

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

пример доступа из класса в "друг" пакет реализации:

package impl;

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

есть два решения вашего вопроса, которые не связаны с сохранением всех классов в одном пакете.

во-первых, чтобы использовать друг аксессор/Друг шаблон, описанный в (Practical API Design, Tulach 2008).

второй-использовать OSGi. Есть статья здесь объясняя, как OSGi выполняет это.

Вопросы: 1,2 и 3.

насколько я знаю, это невозможно.

возможно, вы могли бы дать нам более подробную информацию о вашем дизайне. Такие вопросы, скорее всего, являются результатом недостатков дизайна.

просто считать

  • почему эти классы в разных пакетах, если они так тесно связаны?
  • имеет ли A доступ к закрытым членам B или операция должна быть перемещена в класс B и инициирована A?
  • это действительно вызов или обработка событий лучше?

ответ эйрикмы прост и превосходен. Я мог бы добавить еще одну вещь: вместо того, чтобы иметь общедоступный метод getFriend (), чтобы получить друга, который не может быть использован, вы можете сделать еще один шаг и запретить получение друга без токена: getFriend(Service.FriendToken). Этот FriendToken будет внутренним открытым классом с частным конструктором, так что только служба может создать экземпляр.

вот четкий пример использования с многоразовым Friend класса. Преимуществом данного механизма является простота использования. Возможно, это хорошо для предоставления классам модульных тестов большего доступа, чем остальная часть приложения.

для начала, вот пример того, как использовать 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;
    }
}

затем в другом пакете вы можете сделать это:

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

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

The Friend класса выглядит следующим образом.

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);
    }
}
проблема в том, что это может быть ругали вот так:
public class Abuser {
    public void doBadThings() {
        Friend badFriend = new Friend(new Other());
        String s = new Owner().getMember(badFriend);
        System.out.println(s);
    }
}

теперь, это может быть правдой, что Other класс не имеет никаких публичных конструкторов, поэтому делает выше Abuser код невозможно. Однако, если ваш класс тут есть открытый конструктор, то, вероятно, рекомендуется дублировать класс Friend как внутренний класс. Возьми это Other2 класс в качестве примера:

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

а то Owner2 класс будет выглядеть так:

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

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

обратите внимание, что Other2.Friend класс имеет частный конструктор, что делает это гораздо более безопасным способом сделать это.

предоставленное решение было, пожалуй, не самым простым. Другой подход основан на той же идее, что и в C++: закрытые члены недоступны за пределами области пакета/private, за исключением определенного класса, который владелец делает своим другом.

класс, которому нужен дружественный доступ к члену, должен создать внутренний открытый абстрактный "класс друга", к которому класс, владеющий скрытыми свойствами, может экспортировать доступ, возвращая подкласс, реализующий методы реализации доступа. Метод " API " класса friend может быть приватным, поэтому он недоступен за пределами класса, которому требуется доступ к другу. Его единственным оператором является вызов абстрактного защищенного члена, реализуемого экспортирующим классом.

вот код:

сначала тест, который проверяет, что это действительно работает:

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

затем служба, которая нуждается в доступе друга к частному члену пакета сущности:

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

наконец: класс сущности, предоставляющий удобный доступ к закрытому члену пакета только для приложения класса.услуга.Услуга.

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

хорошо, я должен признать, что это немного дольше, чем "friend service::Service;" но можно было бы сократить его, сохраняя проверку времени компиляции с помощью аннотаций.

в Java можно иметь "дружественность, связанную с пакетом". Это может быть полезно для модульного тестирования. Если вы не укажете private/public/protected перед методом, он будет "другом в пакете". Класс в том же пакете будет иметь доступ к нему, но он будет частным вне класса.

Это правило не всегда известно, и это хорошее приближение ключевого слова c++ "friend". Я считаю, что это хорошая замена.

Я думаю, что классы друзей в C++ похожи на концепцию внутреннего класса в Java. Использование внутренних классов вы можете фактически определить заключительный класс и заключенный. Закрытый класс имеет полный доступ к открытым и закрытым членам его заключительного класса. см. следующую ссылку: http://docs.oracle.com/javase/tutorial/java/javaOO/nested.html

Я думаю, что подход к использованию шаблона доступа друга слишком сложен. Мне пришлось столкнуться с той же проблемой и я решил использовать старый добрый конструктор копирования, известным из C++ в 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;
    }
}

в вашем приложении вы можете написать следующий код:

public class MyApp {

    private static class ProtectedAccessor extends ProtectedContainer {

        protected ProtectedAccessor() {
            super();
        }

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

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

преимущество этого метода заключается в том, что только ваше приложение имеет доступ к защищенным данным. Это не совсем замена ключевого слова friend. Но я думаю, что это вполне подходит, когда вы напишите пользовательские библиотеки, и вам нужно получить доступ к защищенным данным.

всякий раз, когда вам приходится иметь дело с экземплярами ProtectedContainer, вы можете обернуть свой ProtectedAccessor вокруг него, и вы получите доступ.

Он также работает с защищенными методами. Вы определяете их защищенными в своем API. Позже в вашем приложении вы пишете частный класс-оболочку и предоставляете защищенный метод как общедоступный. Вот и все.

Если вы хотите получить доступ к защищенным методам, вы можете создать подкласс класса, который вы хотите использовать, который предоставляет методы, которые вы хотите использовать в качестве общедоступных (или внутренних для пространства имен, чтобы быть более безопасным), и иметь экземпляр этого класса в своем классе (использовать его в качестве прокси).

Что касается частных методов (я думаю) вам не повезло.

Я согласен, что в большинстве случаев ключевое слово friend не является необходимым.

    (ака. по умолчанию) достаточно в большинстве случаев, когда у вас есть группа сильно переплетенных классов
  • для классов отладки, которые хотят получить доступ к внутренним устройствам, я обычно делаю метод частным и обращаюсь к нему через отражение. Скорость, как правило, не важны здесь
  • иногда вы реализуете метод, который является "взломом" или иным образом, который может быть изменен. Я делаю это public, но используйте @Deprecated, чтобы указать, что вы не должны полагаться на этот существующий метод.

и, наконец, если это действительно необходимо, есть шаблон доступа друга, упомянутый в других ответах.

не используя ключевое слово или так.

вы можете "обмануть", используя отражение и т. д., но я бы не рекомендовал "обманывать".

метод, который я нашел для решения этой проблемы, заключается в создании объекта доступа, например:

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

первый код для вызова getAccessor() "утверждает право собственности" на аксессуар. Обычно это код, который создает объект.

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

это также имеет преимущество перед механизмом друга C++, потому что он позволяет ограничить доступ на каждого экземпляра

начиная с Java 9, модули могут быть использованы, чтобы сделать это не проблема во многих случаях.

Я предпочитаю делегирование или состав или фабричный класс (В зависимости от проблемы, которая приводит к этой проблеме), чтобы избежать его публичного класса.

Если это проблема "классов интерфейса / реализации в разных пакетах", то я бы использовал общедоступный фабричный класс, который был бы в том же пакете, что и пакет impl, и предотвратил бы воздействие класса impl.

Если это "я ненавижу делать этот класс/метод общедоступным, чтобы обеспечить эту функциональность для некоторых другой класс в другом пакете " проблема, тогда я бы использовал открытый класс делегата в том же пакете и предоставлял только ту часть функциональности, которая необходима классу "аутсайдер".

некоторые из этих решений обусловлены архитектурой загрузки классов целевого сервера (OSGi bundle, WAR/EAR и т. д.), соглашения о развертывании и именовании пакетов. Например, предложенное выше решение, шаблон "Friend Accessor" является умным для обычных приложений java. Интересно, если это становится сложно реализуйте его в OSGi из-за разницы в стиле загрузки классов.

однажды я видел решение на основе отражения, которое выполняло "проверку друзей" во время выполнения с использованием отражения и проверки стека вызовов, чтобы увидеть, разрешен ли класс, вызывающий метод. Будучи проверкой времени выполнения, он имеет очевидный недостаток.