我希望能够在一个程序包中编写一个Java类,该程序包可以访问另一个程序包中某个类的非公共方法,而不必使其成为另一个类的子类。这可能吗?
我希望能够在一个程序包中编写一个Java类,该程序包可以访问另一个程序包中某个类的非公共方法,而不必使其成为另一个类的子类。这可能吗?
Answers:
这是我在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
是public,但是其构造函数是private
。因此,任何人都可以看到它,但只能Romeo
构造它。我使用静态引用,因此Romeo.Love
从未使用过的引用只会被构建一次,并且不会影响优化。
因此,Romeo
只能cuddle
Juliet
并且只能他可以,因为只有他可以构造和访问Romeo.Love
实例,这Juliet
对于cuddle
她是必需的(否则她会用来给您打耳光NullPointerException
)。
Romeo
的Love
用于Julia
改变永恒的love
领域是final
;-)。
Java的设计者明确拒绝了Friend的想法,因为它可以在C ++中使用。您将“朋友”放在同一包中。私有,受保护和打包的安全性是语言设计的一部分。
詹姆斯·高斯林(James Gosling)希望Java成为无错的C ++。我相信他认为朋友是一个错误,因为它违反了OOP原则。包提供了一种合理的方式来组织组件,而又不必太过依赖OOP。
NR指出,您可以使用反射作弊,但即使不使用SecurityManager,这也才有效。如果启用Java标准安全性,则除非编写专门允许它的安全性策略,否则您将无法通过反射来作弊。
“朋友”概念在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。有文章在这里解释的OSGi是如何实现这一点。
这是一个带有可重用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());
}
}
然后需要朋友访问实体的包私有成员的服务:
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();
}
}
最后:实体类,仅对类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中,可能具有“与包装相关的友好性”。这对于单元测试可能非常有用。如果未在方法前指定私有/公共/受保护,则它将是“包中的朋友”。同一包中的类可以访问它,但在该类之外是私有的。
该规则并不总是已知的,它很好地近似于C ++“ friend”关键字。我发现它是一个很好的替代品。
我认为C ++中的朋友类就像Java中的内部类概念。使用内部类,您实际上可以定义一个封闭的类和一个封闭的类。封闭类可以完全访问封闭类的公共和私人成员。请参见以下链接:http : //docs.oracle.com/javase/tutorial/java/javaOO/nested.html
我认为,使用好友访问器模式的方法太复杂了。我不得不面对同样的问题,并使用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 ++的友好机制相比,这还具有一个优势,因为它允许您限制对 每个实例级别(而不是每个类级别)。通过控制访问者引用,可以控制对对象的访问。您还可以创建多个访问器,并为每个访问器赋予不同的访问权限,从而可以对哪些代码可以访问哪些内容进行细粒度的控制:
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();
}
}
经过大量的撞击(不是很好)之后,这是我的最终解决方案,我非常喜欢它。它灵活,易于使用,并且可以很好地控制类访问。(仅带有引用的访问权限非常有用。)如果对访问者/引用使用protected而不是private,则Foo的子类甚至可以从返回FULL的扩展引用getReference
。它也不需要任何反射,因此可以在任何环境中使用。
我更喜欢委派,组成或工厂班级(取决于导致此问题的问题),以避免使其成为公共班级。
如果是“不同程序包中的接口/实现类”的问题,那么我将使用一个公共工厂类,该类将与impl程序包放在同一程序包中,并防止暴露impl类。
如果是“我不想公开这个类/方法只是为了在不同的程序包中为某些其他类提供此功能”问题,那么我将在同一程序包中使用一个公共委托类,并且仅公开部分功能“局外人”类需要的。
其中一些决定是由目标服务器类加载体系结构(OSGi捆绑软件,WAR / EAR等),部署和程序包命名约定驱动的。例如,上面提出的解决方案“朋友访问者”模式对于普通的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;
}
}