【发布时间】:2010-09-15 23:40:26
【问题描述】:
我希望能够在一个包中编写一个 Java 类,该类可以访问另一个包中的类的非公共方法,而不必使其成为另一个类的子类。这可能吗?
【问题讨论】:
我希望能够在一个包中编写一个 Java 类,该类可以访问另一个包中的类的非公共方法,而不必使其成为另一个类的子类。这可能吗?
【问题讨论】:
这是我在 JAVA 中用来复制 C++ 友元机制的一个小技巧。
假设我有一个班级Romeo 和另一个班级Juliet。出于仇恨的原因,他们在不同的包(家庭)中。
Romeo想cuddleJuliet和Juliet只想让Romeocuddle她。
在 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)。
【讨论】:
love 字段更改为final 来使Romeo 的Love 为Julia 永恒;-)。
love 字段重命名为Love。是的,您实际上可以这样做(请参阅stackoverflow.com/a/14027255/1084488)。结果将是在Romeo 的代码(例如在Juliet.cuddle(Love);)中提及“Love”将被解释为对他的 永恒的引用,一个Love 对象(!) ,而在Romeo 类之外提及Romeo.Love 将引用公共Love 类(!)。
这是一个带有可重用 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 类有一个私有构造函数,因此这是一种更安全的方法。
【讨论】:
我更喜欢委托或组合或工厂类(取决于导致此问题的问题)以避免使其成为公共类。
如果是“不同包中的接口/实现类”问题,那么我会使用与impl包在同一个包中的公共工厂类,并防止impl类暴露。
如果是“我讨厌公开这个类/方法只是为了为不同包中的其他类提供此功能”问题,那么我将在同一个包中使用公共委托类并仅公开该部分“局外人”类所需的功能。
其中一些决策是由目标服务器类加载架构(OSGi 包、WAR/EAR 等)、部署和包命名约定驱动的。例如,上面提出的解决方案“Friend Accessor”模式对于普通的 Java 应用程序来说是很聪明的。我想知道由于类加载风格的不同,在 OSGi 中实现它是否会变得棘手。
【讨论】:
从 Java 9 开始,在许多情况下,可以使用模块来解决这个问题。
【讨论】:
我认为,使用朋友访问器模式的方法太复杂了。我不得不面对同样的问题,我使用 Java 中的 C++ 中已知的良好的、旧的复制构造函数解决了:
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;
}
}
}
这种方法的优点是只有您的应用程序才能访问受保护的数据。它不完全是friend关键字的替代品。但我认为当你编写自定义库并且需要访问受保护的数据时,它非常适合。
当您必须处理 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++ 的友元机制更有优势,因为它允许您在 per-instance 级别上限制访问,而不是 per-class 级别。通过控制访问器引用,您可以控制对对象的访问。您还可以创建多个访问器,并为每个访问器提供不同的访问权限,这样可以细粒度地控制哪些代码可以访问哪些内容:
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();
}
}
经过多次头撞(不是好的那种),这是我的最终解决方案,我非常喜欢它。它灵活、易于使用,并且可以很好地控制类访问。 (with reference only 访问非常有用。)如果您对访问器/引用使用 protected 而不是 private,则 Foo 的子类甚至可以从 getReference 返回扩展引用。它也不需要任何反射,因此可以在任何环境中使用。
【讨论】:
不使用关键字左右。
您可以使用反射等“作弊”,但我不建议“作弊”。
【讨论】:
我同意在大多数情况下朋友关键字是不必要的。
最后,如果真的有必要,还有其他答案中提到的朋友访问器模式。
【讨论】:
我认为 C++ 中的友元类就像 Java 中的内部类概念。使用内部类 您实际上可以定义一个封闭类和一个封闭类。封闭类可以完全访问其封闭类的公共和私有成员。 请参阅以下链接: http://docs.oracle.com/javase/tutorial/java/javaOO/nested.html
【讨论】:
您的问题有两种解决方案,它们不涉及将所有类保存在同一个包中。
第一种是使用(Practical API Design, Tulach 2008)中描述的 Friend Accessor/Friend Package 模式。
第二个是使用OSGi。有一篇文章here 解释了 OSGi 是如何做到这一点的。
【讨论】:
eirikma 的回答简单而出色。我可能会再添加一件事:不是使用公开访问的方法 getFriend() 来获取无法使用的朋友,您可以更进一步并禁止在没有令牌的情况下获取朋友:getFriend(Service.FriendToken)。这个 FriendToken 将是一个带有私有构造函数的内部公共类,因此只有 Service 可以实例化一个。
【讨论】:
在 Java 中,可以有一个“包相关的友好性”。 这对于单元测试很有用。 如果你没有在方法前面指定 private/public/protected,它将是“包中的朋友”。 同一个包中的类可以访问它,但它在类外是私有的。
这条规则并不总是已知的,它是 C++“朋友”关键字的一个很好的近似值。 我觉得它是一个很好的替代品。
【讨论】:
提供的解决方案可能不是最简单的。另一种方法基于与 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的包私有成员的Service:
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 中很有用,例如,可以将 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);
}
}
【讨论】:
我曾经看到一个基于反射的解决方案,它在运行时使用反射进行“朋友检查”,并检查调用堆栈以查看调用该方法的类是否被允许这样做。作为运行时检查,它有明显的缺点。
【讨论】:
Java 的设计者明确拒绝朋友的想法,因为它在 C++ 中工作。你把你的“朋友”放在同一个包裹里。作为语言设计的一部分,强制执行私有、受保护和打包的安全性。
James Gosling 希望 Java 成为没有错误的 C++。我相信他觉得那个朋友是个错误,因为它违反了 OOP 原则。包提供了一种合理的方式来组织组件,而不会过于纯粹地关注 OOP。
NR 指出您可以使用反射来作弊,但即使这样也只有在您不使用 SecurityManager 时才有效。如果您打开 Java 标准安全性,您将无法使用反射作弊,除非您编写安全策略以明确允许它。
【讨论】:
friend 违反了 OOP(尤其是比包访问更严重),那么 he really didn’t understand it(完全有可能,很多人误解了它)。
如果你想访问受保护的方法,你可以创建你想使用的类的子类,将你想使用的方法公开(或者在命名空间内部更安全),并拥有该类的实例在您的班级中(将其用作代理)。
就私有方法而言(我认为)你不走运。
【讨论】:
据我所知,这是不可能的。
也许,您可以向我们提供有关您的设计的更多详细信息。此类问题很可能是设计缺陷造成的。
考虑一下
【讨论】: