我想在一个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
可以调用这个方法,并检查爱情是否真实,如果为null
,运行时将抛出NullPointerException
。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
打你)。Java的设计者明确拒绝了C++中“友元”的概念。你可以把你的“朋友”放在同一个包中。私有、受保护和包级别的安全性是作为语言设计的一部分强制执行的。
James Gosling希望Java成为没有错误的C++。我相信他认为“友元”是一个错误,因为它违反了面向对象编程原则。包提供了一种合理的组织组件的方式,而不必过于纯粹地遵循面向对象编程原则。
NR指出,您可以使用反射来欺骗,但即使如此,只有在您未使用SecurityManager时才有效。如果您启用Java标准安全性,除非编写特定的安全策略来允许其,否则您将无法通过反射来欺骗。
friend
违反了面向对象编程(特别是超过了包访问权限),那么他真的没有理解它(这是完全可能的,很多人都会误解它)。 - Konrad Rudolph'Friend'概念在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);
}
在“友元”实现包中的类中访问示例:
package impl;
public final class FriendlyAccessExample {
public static void main(String[] args) {
Accessor accessor = Accessor.getInstance();
Exposed exposed = accessor.createExposed();
accessor.sayHello(exposed);
}
}
你的问题有两个解决方案,不需要将所有类都放在同一个包中。
第一个解决方案是使用《实用API设计》(Tulach 2008)中描述的Friend Accessor/Friend Package模式。
第二个解决方案是使用OSGi。这里有一篇文章here解释了OSGi如何实现这一点。
eirikma的回答非常简单且出色。我可能还需要补充一点:不要使用无法使用的公共可访问方法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
类具有私有构造函数,因此这是一种更安全的做法。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());
}
}
接着,需要访问实体包私有成员的服务:
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();
}
}
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;”有点长,但通过使用注释可能可以缩短它,同时保留编译时检查。
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
可以在包外被子类化! - Raphael
Romeo
对Julia
的Love
字段更改为final
来使他们之间的爱情永恒 ;-). - Matthiaslove
字段重命名为Love
。是的,你实际上可以这样做(参见https://dev59.com/amzXa4cB1Zd3GeqPUHoL#14027255)。结果将是,在`Romeo`的代码中提到"`Love`"(例如在`Juliet.cuddle(Love);`中)将被解释为对*他*永恒的、一个`Love`对象(!)的引用,而在`Romeo`类外部提到的`Romeo.Love`将指向公共的`Love`类(!)。 - Matthias