Есть ли способ имитировать концепцию C ++ 'friend' в Java?

StackOverflow https://stackoverflow.com/questions/182278

  •  05-07-2019
  •  | 
  •  

Вопрос

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

Это было полезно?

Решение 3

Понятие «друг» полезно в Java, например, для отделения API от его реализации. Для классов реализации обычно требуется доступ к внутренним компонентам API-классов, но они не должны быть доступны клиентам API. Это может быть достигнуто с помощью шаблона «Friend Accessor», как описано ниже:

Класс, предоставляемый через 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);
}

Пример доступа из класса в пакете реализации 'friend':

package impl;

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

Другие советы

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

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

Ромео хочет обниматься Джульетта и Джульетта хочет разрешить Ромео обнимите ее.

В C ++ Джульетта объявляет Romeo как (любителя) друга , но в java таких вещей нет.

Вот классы и хитрость:

Дамы прежде всего:

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

}

Таким образом, метод Juliet.cuddle является public , но вам нужен Romeo.Love для его вызова. Он использует этот Romeo.Love в качестве " безопасности подписи " чтобы убедиться, что только Romeo может вызвать этот метод и проверит, что любовь реальна, чтобы среда выполнения генерировала исключение 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 может обниматься Джульетта , и только он может, потому что только он может создать и получить доступ к Romeo.Love , который требуется Джульеттой для того, чтобы обнять ее (иначе она даст вам исключение NullPointerException ).

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

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

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

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

Во-первых, используйте шаблон Friend Accessor / шаблон друга , описанный в ( Практический API дизайн, Тулач 2008).

Второе - использовать OSGi. Здесь есть статья . как OSGi выполняет это.

Смежные вопросы: 1 , 2 и 3 .

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

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

Просто подумай

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

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

Вот ясный пример использования с повторно используемым классом 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);
    }
}

Класс 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 ++: закрытые члены не доступны вне пакета / частной области, за исключением определенного класса, который владелец делает своим другом.

Класс, которому нужен доступ друга к члену, должен создать внутреннюю публичную аннотацию " Friend Class " что класс, владеющий скрытыми свойствами, может экспортировать доступ, возвращая подкласс, который реализует методы реализации доступа. & Quot; 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());
    }
}

Затем Служба, которой нужен доступ друга, является частным членом пакета Entity:

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

Наконец, класс Entity, обеспечивающий дружественный доступ к закрытому члену пакета только для класса 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();
            }
        };
    }
}

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

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

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

Я думаю, что классы друзей в 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;
        }
    }
}

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

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

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

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

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

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

  • Пакет -частный (он же.по умолчанию) достаточно в большинстве случаев, когда у вас есть группа сильно переплетенных классов
  • Для классов отладки, которым нужен доступ к внутренним компонентам, я обычно делаю метод закрытым и получаю к нему доступ через отражение.Скорость здесь обычно не важна
  • Иногда вы реализуете метод, который является "взломом" или иным способом, который может быть изменен.Я делаю его общедоступным, но использую @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 ++, поскольку позволяет ограничивать доступ на уровне для каждого экземпляра , а не на уровне для класса . Управляя ссылкой доступа, вы контролируете доступ к объекту. Вы также можете создавать несколько средств доступа и предоставлять каждому доступ по-разному, что позволяет детально контролировать, к какому коду можно получить доступ:

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

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

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

После долгих ударов головой (не очень), это было мое окончательное решение, и мне оно очень нравится. Он гибкий, простой в использовании и позволяет очень хорошо контролировать доступ к классам. (Доступ только со ссылкой очень полезен.) Если вы используете для доступа / ссылок защищенный вместо частного, подклассы Foo могут даже возвращать расширенные ссылки из getReference , Это также не требует никакого отражения, поэтому его можно использовать в любой среде.

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top