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

时间:2008-10-08 11:37:53

标签: java c++ friend accessor

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

19 个答案:

答案 0 :(得分:404)

这是我在JAVA中用来复制C ++好友机制的一个小技巧。

假设我有一个班级Romeo和另一个班级Juliet。出于仇恨原因,他们在不同的包裹(家庭)中。

Romeo希望cuddle JulietJuliet只想让她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.cuddlepublic,但您需要Romeo.Love来调用它。它使用此Romeo.Love作为“签名安全性”,以确保只有Romeo可以调用此方法并检查爱是否真实,以便运行时如果NullPointerExceptionnull {1}}。

现在男孩们:

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打你。)

答案 1 :(得分:50)

Java的设计者明确拒绝了朋友的想法,因为它在C ++中有效。你把你的“朋友”放在同一个包里。私有,受保护和打包的安全性是语言设计的一部分。

James Gosling希望Java能够成为C ++而不会犯错。我相信他觉得朋友是个错误,因为它违反了OOP原则。包提供了一种合理的方式来组织组件,而不是过于纯粹的OOP。

NR指出你可以使用反射作弊,但即使只是你没有使用SecurityManager也是如此。如果您打开Java标准安全性,除非您编写安全策略以明确允许,否则您将无法使用反射作弊。

答案 2 :(得分:44)

“朋友”概念在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);
    }
}

答案 3 :(得分:10)

您的问题有两种解决方案,不涉及将所有类保留在同一个包中。

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

第二种是使用OSGi。有一篇文章here解释了OSGi如何实现这一目标。

相关问题:123

答案 4 :(得分:8)

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

也许,您可以向我们提供有关您的设计的更多详细信息。像这样的问题可能是设计缺陷的结果。

考虑一下

  • 如果这些课程密切相关,为什么这些课程在不同的课程中?
  • 有A访问B的私人成员,还是应该将操作移至B类并由A?
  • 触发
  • 这真的要求或事件处理更好吗?

答案 5 :(得分:3)

eirikma的答案很简单,也很出色。我可能会添加一件事:不是使用可公开访问的方法,getFriend()来获取一个无法使用的朋友,你可以更进一步,不允许没有令牌的朋友:getFriend(Service.FriendToken)。这个FriendToken将是一个带有私有构造函数的内部公共类,因此只有Service可以实例化一个。

答案 6 :(得分:3)

这是一个带有可重用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类有一个私有构造函数,因此这使得它更安全。

答案 7 :(得分:2)

提供的解决方案可能不是最简单的。另一种方法基于与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();
    }
}

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

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

答案 8 :(得分:1)

在Java中,可以拥有“与包相关的友谊”。 这可以用于单元测试。 如果您未在方法前指定private / public / protected,则它将是“包中的朋友”。 同一个包中的一个类将能够访问它,但它在类外是私有的。

此规则并不总是已知,并且它是C ++“friend”关键字的良好近似值。 我认为这是一个很好的替代品。

答案 9 :(得分:1)

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

答案 10 :(得分:1)

我认为,使用好友访问者模式的方法太复杂了。我不得不面对同样的问题,我解决了使用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;
        }
    }
}

此方法的优点是只有您的应用程序才能访问受保护的数据。它并不完全取代了friend关键字。但是我认为在编写自定义库时需要访问受保护的数据非常合适。

每当你必须处理ProtectedContainer的实例时,你可以将ProtectedAccessor包裹起来并获得访问权。

它也适用于受保护的方法。您可以在API中定义它们。稍后在您的应用程序中,您编写一个私有包装类并将受保护的方法公开为public。就是这样。

答案 11 :(得分:0)

我更喜欢代表团或作曲或工厂类(取决于导致此问题的问题),以避免使其成为公共类。

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

如果它是“我讨厌公开这个类/方法只为其他类中的其他类提供此功能”的问题,那么我会在同一个包中使用公共委托类并仅公开该部分“局外人”课程所需的功能。

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

答案 12 :(得分:0)

如果要访问受保护的方法,可以创建要使用的类的子类,以公开要用作公共的方法(或命名空间的内部更安全),并具有该类的实例在你的班级(用作代理人)。

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

答案 13 :(得分:0)

我同意在大多数情况下不需要使用friend关键字。

  • 在大多数情况下,如果你有一组交错缠绕的类,那么Package-private(也就是默认值)已经足够了
  • 对于想要访问内部的调试类,我通常将该方法设为私有并通过反射访问它。这里的速度通常并不重要
  • 有时,您实施的方法是“黑客”或其他可能会发生变化的方法。我将其公开,但使用@Deprecated表示您不应该依赖此方法。

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

答案 14 :(得分:0)

不使用关键字左右。

你可以使用反射等“欺骗”,但我不建议“作弊”。

答案 15 :(得分:0)

我找到解决此问题的方法是创建一个访问者对象,如下所示:

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();
    }
}
经过多次头撞(不是好的),这是我的最终解决方案,我非常喜欢它。它灵活,易于使用,并且可以很好地控制类访问。 (仅带参考访问的非常有用。)如果对访问者/引用使用protected而不是private,则Foo的子类甚至可以从getReference返回扩展引用。它也不需要任何反射,因此可以在任何环境中使用。

答案 16 :(得分:0)

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

答案 17 :(得分:0)

我不知道它是否对任何人有用,但是我通过以下方式处理它:

我创建了一个界面(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;
    }
}

答案 18 :(得分:-1)

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