Я хотів би мати можливість написати клас Java в одному пакеті, який може отримати доступ до непублічних методів класу в іншому пакеті, не вносити його в підклас іншого класу. Чи можливо це?
Я хотів би мати можливість написати клас Java в одному пакеті, який може отримати доступ до непублічних методів класу в іншому пакеті, не вносити його в підклас іншого класу. Чи можливо це?
Відповіді:
Ось невеликий трюк, який я використовую в 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
).
Romeo
«S Love
для Julia
вічної, змінюючи love
поле , щоб бути final
;-).
Дизайнери Java явно відкинули ідею друга, оскільки він працює в C ++. Ви поміщаєте своїх «друзів» у той самий пакет. Приватна, захищена та упакована безпека застосовується як частина мовного дизайну.
Джеймс Гослінг хотів, щоб Java була C ++ без помилок. Я вважаю, що він вважав, що друг помилився, оскільки він порушує принципи ООП. Пакети пропонують розумний спосіб впорядкування компонентів, не будучи занадто пуристими щодо OOP.
NR вказує, що ви можете обманювати, використовуючи роздуми, але навіть це працює, лише якщо ви не використовуєте SecurityManager. Якщо увімкнути стандартну безпеку Java, ви не зможете обдурити роздуми, якщо не напишете політику безпеки, щоб спеціально її дозволити.
friend
порушує OOP (зокрема, більше, ніж доступ до пакету), то він насправді цього не розумів (цілком можливо, багато людей неправильно розуміють це).
Концепція "друг" корисна в Java, наприклад, для відділення API від його реалізації. Для класів реалізації звичайно потрібен доступ до внутрішніх класів API, але вони не повинні піддаватися клієнтам API. Цього можна досягти, використовуючи схему "Другого доступу", як детально описано нижче:
Клас, відкритий через 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);
}
}
Є два варіанти вашого питання, які не передбачають збереження всіх класів в одному пакеті.
Перший полягає у використанні схеми Friend Accessor / Friend Package, описаної в (Practical API Design, Tulach 2008).
Друга - використовувати OSGi. Існує стаття тут пояснити , як OSGi вирішує цю задачу.
Наскільки я знаю, це неможливо.
Можливо, Ви могли б дати нам ще детальну інформацію про ваш дизайн. Такі питання, ймовірно, є наслідком вад дизайну.
Просто врахуйте
Відповідь ейрікма проста і відмінна. Я можу додати ще одне: замість того, щоб мати загальнодоступний метод getFriend (), щоб отримати друга, якого неможливо використати, ви можете піти ще на крок і заборонити отримання друга без маркера: getFriend (Service.FriendToken). Цей FriendToken був би внутрішнім публічним класом з приватним конструктором, так що лише Сервіс міг створити його.
Ось чіткий приклад використання з 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
клас має приватний конструктор, що робить це набагато більш безпечним способом його виконання.
Надане рішення було чи не найпростішим. Інший підхід ґрунтується на тій же ідеї, що і в 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();
}
};
}
}
Гаразд, я мушу визнати, що це трохи довше, ніж "обслуговування друзів :: Сервіс;" але можливо скоротити його, зберігаючи перевірку часу компіляції, використовуючи примітки.
У Java можливо "дружні стосунки з пакунками". Це може бути корисно для тестування одиниць. Якщо ви не вкажете приватний / громадський / захищений перед методом, він буде "товариш у пакеті". Клас в одному пакеті зможе отримати доступ до нього, але він буде приватним поза класом.
Це правило відоме не завжди, і це добре наближення ключового слова 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
їх можна підкласифікувати поза пакетом!
Якщо ви хочете отримати доступ до захищених методів, ви можете створити підклас класу, який ви хочете використовувати, який відкриває методи, які ви хочете використовувати як загальнодоступні (або внутрішні у просторі імен, щоб бути більш безпечними), і мати примірник цього класу у вашому класі (використовувати його як проксі).
Що стосується приватних методів (я думаю) вам не пощастило.
Я погоджуюсь, що в більшості випадків друге ключове слово непотрібне.
І нарешті, якщо це дійсно необхідно, є схема дружнього аксесуара, згадана в інших відповідях.
Не використовується ключове слово чи так.
Ви можете "обдурити", використовуючи роздуми тощо, але я б не рекомендував "обман".
Я знайшов спосіб вирішення цієї проблеми - створити об'єкт аксесуара, наприклад:
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
. Він також не потребує рефлексії, тому його можна використовувати в будь-якому середовищі.
Я віддаю перевагу делегуванню чи складу чи заводському класу (залежно від проблеми, що спричиняє цю проблему), щоб уникнути того, щоб зробити його публічним класом.
Якщо це проблема "класи інтерфейсу / реалізації в різних пакетах", я б використовував загальнодоступний заводський клас, який би знаходився в тому ж пакеті, що і пакет impl, і запобігав би впливу класу impl.
Якщо проблема "Я ненавиджу робити цей клас / метод загальнодоступним просто для надання цієї функціональності для якогось іншого класу в іншому пакеті", то я б використовував публічний клас делегата в тому ж пакеті і викривав би лише ту частину функціоналу потрібний клас "аутсайдер".
Деякі з цих рішень керуються архітектурою завантаження цільового сервера (пакет OSGi, WAR / EAR тощо), умовами розгортання та іменування пакетів. Наприклад, запропоноване вище рішення, модель "Friend Accessor" - розумна для звичайних програм Java. Цікаво, чи стає складним реалізувати його в OSGi через різницю у стилі завантаження.
Я не знаю, чи комусь це корисно, але я впорався з цим наступним чином:
Я створив інтерфейс (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;
}
}