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

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

Ответ 1

Концепция "друга" полезна в 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);
    }
}

Ответ 2

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

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

Romeo хочет cuddle Juliet а Juliet хочет, чтобы Romeo только cuddle ее.

В C++ Juliet объявила бы Romeo своим (любовником) friend но в 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.Love только один раз и не влияет на оптимизацию.

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

Ответ 3

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

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

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

Ответ 4

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

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

Вторым является использование OSGi. Здесь приведена статья объясняющая, как это делает OSGi.

Вопросы, относящиеся: 1, 2 и 3.

Ответ 5

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

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

Просто рассмотрите

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

Ответ 6

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

Ответ 7

Вот ясный пример использования с повторно используемым классом 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 невозможным. Однако, если ваш класс имеет открытый конструктор, то это, вероятно, целесообразно дублировать класс друга как внутренний класс. Возьмем этот класс 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 имеет закрытый конструктор, что делает этот способ более безопасным.

Ответ 8

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

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

Ответ 9

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

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

Ответ 10

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

Ответ 11

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

Ответ 12

Не использовать ключевое слово или так.

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

Ответ 13

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

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

Ответ 14

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

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

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

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

Ответ 15

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

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

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

Ответ 16

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

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.

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

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

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

Ответ 17

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

Ответ 18

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