Я хотел бы иметь возможность написать класс Java в одном пакете, который может получать доступ к закрытым методам класса в другом пакете, не делая его подклассом другого класса. Это возможно?
Есть ли способ смоделировать концепцию «друга» C ++ в Java?
Ответы (18)
Вот небольшой трюк, который я использую в 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
).
public Whatever getPrivateField(Romeo.Love l) { l.hashCode(); return mPrivateField; }
.
- person Salomon BRYS; 13.08.2015
Romeo
Love
для Julia
вечным, изменив поле love
на final
;-).
- person Matthias; 10.11.2015
Romeo
увлекался растафарианством Love
, вы также можете сделать love
поле static
;-).
- person Matthias; 10.11.2015
love
в Love
. Да, вы действительно можете это сделать (см. stackoverflow.com/a/14027255/1084488). В результате упоминания Love
в коде Romeo
(например, в Juliet.cuddle(Love);
) будут интерпретироваться как ссылки на его вечный, один Love
объект (!), Тогда как упоминания Romeo.Love
вне класса Romeo
будут относиться к в публичный Love
класс (!).
- person Matthias; 10.11.2015
final
;)
- person faghani; 10.10.2016
Romeo.Love
объект.
- person noamtm; 07.06.2020
Romeo.Love
?
- person David Tóth; 27.01.2021
Разработчики Java явно отвергли идею друга, поскольку она работает на C ++. Вы кладете своих «друзей» в один пакет. Частная, защищенная и пакетная безопасность обеспечивается как часть языкового дизайна.
Джеймс Гослинг хотел, чтобы Java была C ++ без ошибок. Я полагаю, он считал, что этот друг был ошибкой, потому что это нарушает принципы ООП. Пакеты обеспечивают разумный способ организации компонентов без излишней строгости в отношении ООП.
NR указал, что вы можете обмануть, используя отражение, но даже это работает, только если вы не используете SecurityManager. Если вы включите стандартную безопасность Java, вы не сможете обмануть с помощью отражения, если не напишете политику безопасности, специально разрешающую это.
friend
нарушает ООП (в частности, больше, чем доступ к пакету), то он действительно этого не понимал (вполне возможно, многие люди это неправильно понимают).
- person Konrad Rudolph; 27.05.2011
Концепция «друга» полезна в 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);
}
}
Accessor
общедоступным статическим вложенным классом Enclosed
с частным конструктором и общедоступными методами. Затем просто введите Accessor
экземпляра друзьям, как вы уже делаете. Классы в других пакетах могли бы использовать аксессор, только если он был внедрен. Я не думаю, что есть много пользы от помещения суперкласса доступа в пакет friend, потому что вы должны сделать методы защищенными, что означает, что они в любом случае видны внешнему миру, например. через JavaDocs.
- person Radiodef; 24.08.2018
Exposed
не видны другим пакетам, а методы средства доступа защищены, поэтому они доступны только для пакета друзей. На самом деле это проверки во время компиляции.
- person Radiodef; 27.08.2018
Есть два решения вашего вопроса, которые не связаны с хранением всех классов в одном пакете.
Первый - использовать шаблон Friend Accessor / Friend Package, описанный в (Практическое проектирование 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 ++: закрытые члены недоступны за пределами пакета / частной области, за исключением определенного класса, который владелец делает себе другом.
Класс, которому требуется дружественный доступ к члену, должен создать внутренний общедоступный абстрактный «дружественный класс», к которому класс, владеющий скрытыми свойствами, может экспортировать доступ, возвращая подкласс, реализующий методы реализации доступа. Метод «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;" но можно было бы сократить его, сохранив при этом проверку во время компиляции, используя аннотации.
В 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. Позже в своем приложении вы напишете частный класс-оболочку и сделаете защищенный метод общедоступным. Вот и все.
ProtectedContainer
можно разделить на подклассы вне пакета!
- person Raphael; 11.10.2017
Если вы хотите получить доступ к защищенным методам, вы можете создать подкласс класса, который вы хотите использовать, который предоставляет методы, которые вы хотите использовать как общедоступные (или внутренние по отношению к пространству имен, чтобы быть безопаснее), и иметь экземпляр этого класса в своем классе. (используйте его как прокси).
Что касается частных методов (я думаю), вам не повезло.
Я согласен с тем, что в большинстве случаев ключевое слово друга не нужно.
- Package-private (также известный как default) достаточно в большинстве случаев, когда у вас есть группа сильно связанных классов.
- Для классов отладки, которым нужен доступ к внутренним компонентам, я обычно делаю метод закрытым и получаю к нему доступ через отражение. Скорость здесь обычно не важна
- Иногда вы реализуете метод, который является «взломом» или иным образом, который может быть изменен. Я делаю его общедоступным, но использую @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, WAR / EAR и т. Д.), Соглашениями о развертывании и именовании пакетов. Например, предложенное выше решение, шаблон «Friend Accessor», хорошо подходит для обычных приложений Java. Интересно, сложно ли реализовать это в OSGi из-за разницы в стиле загрузки классов.
Однажды я видел решение на основе отражения, которое выполняло «проверку друзей» во время выполнения с использованием отражения и проверки стека вызовов, чтобы узнать, разрешено ли это делать классу, вызывающему метод. Будучи проверкой во время выполнения, она имеет очевидный недостаток.