Есть ли способ имитировать концепцию C ++ 'friend' в Java?
Вопрос
Я хотел бы иметь возможность написать класс 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 выполняет это. Р>
Насколько я знаю, это невозможно.
Может быть, Вы могли бы рассказать нам более подробно о Вашем дизайне.Подобные вопросы, скорее всего, являются результатом недостатков дизайна.
Просто подумай
- Почему эти классы находятся в разных пакетах, если они так тесно связаны?
- Имеет ли 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 из-за разницы в стиле загрузки классов.
Однажды я увидел решение, основанное на отражениях, которое "проверяло друга" во время выполнения используя отражение и проверяя стек вызовов, чтобы увидеть, разрешено ли классу, вызывающему метод, сделать это. Будучи проверкой во время выполнения, она имеет очевидный недостаток.