Есть ли способ смоделировать концепцию «друга» C ++ в Java? - PullRequest
168 голосов
/ 08 октября 2008

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

Ответы [ 18 ]

405 голосов
/ 05 сентября 2013

Вот небольшой трюк, который я использую в 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).

50 голосов
/ 08 октября 2008

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

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

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

44 голосов
/ 25 ноября 2008

Понятие «друг» полезно в 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);
    }
}
10 голосов
/ 27 мая 2011

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

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

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

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

8 голосов
/ 08 октября 2008

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

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

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

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

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

3 голосов
/ 18 августа 2011

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

2 голосов
/ 07 октября 2009

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

1 голос
/ 26 марта 2012

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

1 голос
/ 13 декабря 2010

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

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

...