有没有办法在Java中模拟C ++的“朋友”概念?

我希望能够在一个包中编写一个Java类,它可以访问另一个包中的类的非公共方法,而不必将其作为另一个类的子类。 这可能吗?

这是我在JAVA中用来复制C ++朋友机制的一个小窍门。

可以说我有一classRomeo和另一classJuliet 。 由于仇恨的原因,他们处于不同的包裹(家庭)。

Romeocuddle JulietJuliet只想让Romeo cuddle她。

在C ++中, JulietRomeo宣布为(情人) friend但在java中没有这样的事情。

这里是类和技巧:

女士优先 :

 package capulet; import montague.Romeo; public class Juliet { public static void cuddle(Romeo.Love l) { l.hashCode(); System.out.println("O Romeo, Romeo, wherefore art thou Romeo?"); } } 

所以方法Juliet.cuddlepublic但是你需要一个Romeo.Love来调用它。 它使用这个Romeo.Love作为“签名安全性”,以确保只有Romeo可以调用这个方法,并简单地调用它的hashCode ,所以运行时将抛出一个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才能构build它。 我使用一个静态引用,所以从来没有使用过的Romeo.Love只构造一次,不影响优化。

因此, Romeo可以cuddle Juliet ,只有他可以,因为只有他可以构build和访问Romeo.Love ,这是Juliet要求cuddle她(否则她会用一个NullPointerException给你一个)。

Java的devise者明确地拒绝了在C ++中工作的朋友的想法。 你把你的“朋友”放在同一个包里。 作为语言devise的一部分,实施私有,受保护和打包的安全性。

James Gosling希望Java没有错误就成为C ++。 我相信他觉得这个朋友是一个错误,因为它违反了面向对象的原则。 包提供了一个合理的方式来组织组件,而不是过于纯粹的面向对象。

NR指出你可以使用reflection作弊,但即使这样做,只有在你不使用SecurityManager时才有效。 如果您打开Java标准安全性,除非您编写安全策略来明确允许,否则您将无法用reflection作弊。

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(); } } } 

提供“朋友”function的课程:

 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); } } 

对于你的问题有两个解决scheme,不涉及将所有类放在同一个包中。

首先是使用(Practical API Design,Tulach 2008)中描述的Friend Accessor / Friend Package模式。

其次是使用OSGi。 这里有一篇文章解释了OSGi如何完成这个任务。

相关的问题: 1,2和3 。

据我所知,这是不可能的。

也许,你可以给我们一些关于你的devise的更多细节。 像这样的问题可能是devise缺陷的结果。

考虑一下

  • 为什么这些类在不同的包中,如果它们如此密切相关呢?
  • 有A访问B的私人成员,还是应该把这个操作转移到B类,并由A触发?
  • 这是真的打电话还是事件处理更好?

我认为C ++中的朋友类就像Java中的内部类概念。 使用内部类可以实际定义一个封闭类和一个封闭类。 封闭式课程可以完全访问封闭式课程的公共和私人成员。 请参阅以下链接: http : //docs.oracle.com/javase/tutorial/java/javaOO/nested.html

提供的解决scheme可能不是最简单的。 另一种方法基于与C ++相同的思想:私有成员不能在包/私有范围外访问,除了拥有者自己创build好的特定类。

需要朋友访问成员的类应该通过返回实现访问实现方法的子类创build一个拥有隐藏属性的类可以导出访问的内部公共抽象“朋友类”。 朋友类的“API”方法可以是私有的,所以在需要朋友访问的类之外是不可访问的。 它唯一的声明是对导出类实现的抽象保护成员的调用。

代码如下:

首先validation这个实际工作的testing:

 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 (aka '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(); } }; } } 

好吧,我必须承认这比“朋友服务::服务”要长一点。 但是可以缩短它,同时通过使用注释来保持编译时检查。

eirikma的答案很简单,很好。 我可能会再添加一件事情:getFriend()获取一个无法使用的朋友,而不是使用公共方法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 is; public Friend(final Object is) { this.is = is.getClass(); } public void is(final Class... classes) { for (final Class c : classes) if (c == is) return; throw new ClassCastException(String.format("%s is not an expected friend.", is.getName())); } } 

在Java中,可以有一个“与包相关的友好”。 这可以用于unit testing。 如果你没有在方法前指定private / public / protected,它将是“包中的朋友”。 同一个包中的一个类将能够访问它,但在类之外是私有的。

这个规则并不总是已知的,它是一个C ++“朋友”关键字的近似值。 我觉得这是一个很好的替代品。

我认为,使用朋友访问器模式的方法太复杂了。 我不得不面对同样的问题,我解决了在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中定义它们。 在后来的应用程序中,您编写了一个私有包装类,并将受保护的方法公开。 而已。

如果你想访问受保护的方法,你可以创build一个你想使用的类的子类,公开你希望使用的方法作为public(或者名字空间的内部是安全的),并且在你的类中有一个这个类的实例(使用它作为代理)。

就私人方法而言(我认为)你运气不好。

我同意在大多数情况下,friend关键字是不必要的。

  • 在大多数情况下,包私有(又名缺省)就足够了,在这种情况下,你有一组严重交织的类
  • 对于想要访问内部部件的debugging类,我通常将该方法设为私有,并通过reflection来访问它。 在这里速度通常并不重要
  • 有时候,你实施的方法是“黑客”,否则可能会有所改变。 我把它公开,但使用@Deprecated来表明你不应该依赖这种方法存在。

最后,如果真的有必要,还有其他答案中提到的朋友访问器模式。

不使用关键字等。

你可以使用reflection等“作弊”,但我不会推荐“作弊”。

我发现解决这个问题的方法是创build一个访问器对象,如下所示:

 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()的第一个代码声明访问器的“所有权”。 通常,这是创build对象的代码。

 Foo bar = new Foo(); //This object is safe to share. FooAccessor barAccessor = bar.getAccessor(); //This one is not. 

与C ++的朋友机制相比,这也有一个优势,因为它允许您限制每个实例级别的访问,而不是每个级别的访问。 通过控制访问器引用,可以控制对对象的访问。 您还可以创build多个访问器,并对每个访问器进行不同的访问,以便对访问什么代码进行细粒度的控制:

 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(); } } 

最后,如果你想让事情更有条理,你可以创build一个引用对象,把所有东西放在一起。 这使您可以使用一个方法调用声明所有访问器,并将它们与链接的实例保持在一起。 一旦你有了参考,你可以将访问者传递给需要它的代码:

 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(); } } 

经过多次的敲打(不是好的一种),这是我的最终解决scheme,我非常喜欢它。 它是灵活的,使用简单,并允许非常好的类访问控制。 ( 只有引用访问是非常有用的。)如果你使用protected而不是private来访问/引用,Foo的子类甚至可以从getReference返回扩展引用。 它也不需要任何reflection,所以它可以在任何环境中使用。

从Java 9开始,在许多情况下,可以使用模块来使其成为非问题。

我更喜欢委派或组成或工厂类(取决于导致这个问题的问题),以避免使它成为公共类。

如果是“不同包中的接口/实现类”问题,那么我会使用一个与impl包在同一个包中的公共工厂类,并防止impl类的暴露。

如果是“我不希望公开这个类/方法只是为其他类提供这个function”,那么我会在同一个包中使用一个公共的委托类,只公开这个部分的function“外来者”class级所需要的。

其中一些决定是由目标服务器类加载体系结构(OSGi包,WAR / EAR等),部署和包命名约定驱动的。 例如,上面提出的解决scheme“Friend Accessor”模式对于普通的Java应用程序来说是聪明的。 我想知道在OSGi中实现它是否有点棘手,因为类加载的风格不同。

我曾经看到一个基于reflection的解决scheme,它在运行时使用reflection进行“朋友检查”,并检查调用堆栈以查看调用该方法的类是否被允许这样做。 作为一个运行时检查,它有明显的缺点。