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

196

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

Мэтью Мердок
источник

Ответы:

466

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

Саломон БРИС
источник
107
+1 за "пощечину с NullPointerException". Очень впечатляюще.
Николас
2
@Steazy: ищите аннотации NotNull, NonNull и CheckForNull. Обратитесь к документации вашей IDE, чтобы узнать, как использовать и применять эти аннотации. Я знаю, что IntelliJ встраивает это по умолчанию и затмение нуждается в плагине (например, FindBugs).
Salomon BRYS
27
Вы можете сделать Romeoэто Loveдля Juliaвечного, изменив loveполе на final;-).
Матиас
5
@Matthias Поле любви статично ... Я отредактирую ответ, чтобы сделать его окончательным;)
Salomon BRYS
12
Все ответы должны быть такими (Y) +1 для юмора и отличного примера.
Зия Уль Рехман Могал
54

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

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

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

Дэвид Г
источник
11
Я не хочу быть педантом, но модификаторы доступа не являются механизмом безопасности.
Грег Д
6
Модификаторы доступа являются частью модели безопасности Java. Я специально обращался к java.lang.RuntimePermission для отражения: accessDeclaredMembers и accessClassInPackage.
Дэвид Г
54
Если Гослинг действительно думал, что friendнарушил ООП (в частности, больше, чем доступ к пакетам), то он действительно не понимал этого (вполне возможно, многие люди неправильно это понимают).
Конрад Рудольф
8
Иногда компоненты класса необходимо разделять (например, реализация и API, основной объект и адаптер). Защита на уровне пакетов в то же время слишком ограничительна и слишком ограничена, чтобы делать это должным образом.
Дхарди,
2
@GregD Их можно считать механизмом безопасности в том смысле, что они помогают предотвратить неправильное использование участниками класса разработчиками. Я думаю, что их лучше назвать механизмом безопасности .
раздавить
45

Концепция «друга» полезна в 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);
    }
}
Мэтью Мердок
источник
1
Потому что я не знал, что означает «статический» в классе «Exposed»: Статический блок - это блок операторов внутри Java-класса, который будет выполняться при первой загрузке класса в JVM Подробнее на javatutorialhub. ком /…
Гай Л
Интересный шаблон, но он требует, чтобы классы Exposed и Accessor были общедоступными, в то время как классы, реализующие API (т. Е. Набор классов Java, реализующих набор общедоступных интерфейсов Java), лучше были бы защищены по умолчанию и, таким образом, недоступны для клиента. отделить типы от их реализации.
Янн-Гаэль Геенек
8
Я довольно ржавый на моей Java, так что прости мое невежество. В чем преимущество этого решения по сравнению с решением "Ромео и Джульетта", опубликованным Salomon BRYS? Эта реализация отпугнула бы меня, если бы я наткнулся на это в кодовой базе (без вашего объяснения, то есть с тяжелыми комментариями). Подход Ромео и Джульетты очень прост для понимания.
Steazy
1
Этот подход сделает проблемы видимыми только во время выполнения, а неправильное использование Ромео и Джульетты сделает их видимыми во время компиляции во время разработки.
ymajoros
1
@ymajoros Пример «Ромео и Джульетты» не делает неправильное использование видимым во время компиляции. Он полагается, что аргумент передается правильно, и генерируется исключение. Это оба действия во время выполнения.
Radiodef
10

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

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

Второе - использовать OSGi. Существует статья здесь объяснить , как OSGi решает эту задачу.

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

Джефф Аксельрод
источник
7

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

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

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

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

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

Ариг
источник
3

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

intrepidis
источник
2

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

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

Вот код:

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

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

eirikma
источник
Это не совсем работает, так как обычный класс в том же пакете может просто получить getFriend () и затем вызвать защищенный метод, минуя закрытый.
user2219808
1

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

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

daitangio
источник
1
Это правда, но я действительно спрашивал о коде, находящемся в разных пакетах ...
Мэтью Мердок
1

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

Sephiroth
источник
Э-э, нет, это не так. Это больше похоже на дружбу в реальной жизни: это может быть, но не обязательно, взаимно (А, будучи другом Б, не означает, что Б считается другом А), и вы и ваши друзья могут быть совершенно разными семьи и есть свои, возможно (но не обязательно) пересекающиеся круги друзей. (Не то чтобы я хотел видеть уроки с большим количеством друзей. Это может быть полезной функцией, но ее следует использовать с осторожностью.)
Кристофер Кройциг,
1

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

Крис
источник
1
Но ProtectedContainerможет быть подкласс за пределами пакета!
Рафаэль
0

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

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

Омар Кохеджи
источник
0

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

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

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

Casebash
источник
0

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

Вы можете «обмануть», используя рефлексию и т. Д., Но я бы не рекомендовал «обманывать».

NR.
источник
3
я бы посчитал это настолько плохой идеей, что даже предположить, что это отвратительно для меня. Очевидно, что это в лучшем случае обломок, и он не должен быть частью какого-либо дизайна.
Штеймер
0

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

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

jpfx1342
источник
0

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

Рафаэль
источник
0

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

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

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

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

Сурадж Рао
источник
0

Я не знаю, полезно ли это кому-либо, но я справился с этим следующим образом:

Я создал интерфейс (AdminRights).

Каждый класс, который должен иметь возможность вызывать указанные функции, должен реализовывать AdminRights.

Затем я создал функцию HasAdminRights следующим образом:

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;
    }
}
mdre
источник
-1

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

Ран Бирон
источник