有没有一种方法在Java中模拟C++中的“friend”概念?

239

我想在一个Java包中编写一个类,可以访问另一个包中的类的非公共方法,而不必将其作为另一个类的子类。这种做法是否可行?

18个回答

601

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

假设我有一个类Romeo和另一个类Juliet。由于仇恨的原因,它们位于不同的包(家族)中。

Romeo想要cuddle Juliet,而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.cuddlepublic的,但你需要一个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可以cuddleJuliet,只有他可以,因为只有他可以构造和访问一个Romeo.Love实例,这是Juliet需要的,以便她可以cuddle他(否则她会用NullPointerException打你)。

157
对于“用NullPointerException抽你耳光”这个点赞,非常令人印象深刻。 - Nickolas
46
你可以通过将 RomeoJuliaLove 字段更改为 final 来使他们之间的爱情永恒 ;-). - Matthias
7
@Matthias 爱的领域是静态的......我会编辑答案,使其最终确定 ;) - Salomon BRYS
20
好的,我会尽力以最简洁、准确的方式翻译,并保持原意不变。以下是需要翻译的内容:Can you translate this paragraph into Chinese?Sure, I can do that. Please provide the paragraph you would like me to translate. - Zia Ul Rehman Mughal
显示剩余18条评论

63

Java的设计者明确拒绝了C++中“友元”的概念。你可以把你的“朋友”放在同一个包中。私有、受保护和包级别的安全性是作为语言设计的一部分强制执行的。

James Gosling希望Java成为没有错误的C++。我相信他认为“友元”是一个错误,因为它违反了面向对象编程原则。包提供了一种合理的组织组件的方式,而不必过于纯粹地遵循面向对象编程原则。

NR指出,您可以使用反射来欺骗,但即使如此,只有在您未使用SecurityManager时才有效。如果您启用Java标准安全性,除非编写特定的安全策略来允许其,否则您将无法通过反射来欺骗。


14
访问修饰符不是安全机制,我不是要挑剔。 - Greg D
6
访问修饰符是Java安全模型的一部分。我特别指的是java.lang.RuntimePermission关于反射的两个权限:accessDeclaredMembers和accessClassInPackage。 - David G
61
如果Gosling真的认为friend违反了面向对象编程(特别是超过了包访问权限),那么他真的没有理解它(这是完全可能的,很多人都会误解它)。 - Konrad Rudolph
12
类组件有时需要分离(例如实现和API,核心对象和适配器)。包级别的保护对于正确进行此操作既过于宽松又过于严格。 - dhardy
4
它们可以被视为安全机制,因为它们有助于防止开发人员错误使用类成员。我认为最好将它们称为"安全机制"。 - crush
我不得不点赞,因为这篇文章写得很好,提供了创造性的解决方案。但是,我希望自己永远不需要在生产环境中做这样的事情。 - kap

48

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

1
因为我不知道“Exposed”类中的“static”是什么意思:静态块是Java类中的一组语句,在类首次加载到JVM时将被执行。阅读更多内容,请访问http://www.javatutorialhub.com/java-static-variable-methods.html#VlA0qtKsqfFuFo8r.99 - Guy L
有趣的模式,但它需要将Exposed和Accessor类设置为public,而实现API的类(即实现一组公共Java接口的一组Java类)最好是“默认受保护的”,因此对客户端不可访问,以将类型与其实现分离。 - Yann-Gaël Guéhéneuc
8
我的Java水平有些生疏,请原谅我的无知。相比Salomon BRYS提出的“罗密欧与朱丽叶”方案,这种方法有什么优势?如果我在代码库中偶然发现这个实现(没有您的解释和注释),这会让我非常害怕。而“罗密欧与朱丽叶”方法则非常容易理解。 - Steazy
1
这种方法只会在运行时显示问题,而罗密欧和朱丽叶的误用会在开发时编译时就暴露出来。 - ymajoros
1
@ymajoros Romeo和Juliet的例子在编译时无法显示误用。它依赖于正确传递参数和抛出异常。这两个都是运行时操作。 - Radiodef
显示剩余3条评论

11

你的问题有两个解决方案,不需要将所有类都放在同一个包中。

第一个解决方案是使用《实用API设计》(Tulach 2008)中描述的Friend Accessor/Friend Package模式。

第二个解决方案是使用OSGi。这里有一篇文章here解释了OSGi如何实现这一点。

相关问题:1, 2, 和 3


8
据我所知,这是不可能的。 也许,您可以给我们更多有关您设计的细节。像这样的问题很可能是设计缺陷的结果。 请考虑以下几点: - 如果这些类如此相关,为什么它们在不同的包中? - A需要访问B的私有成员,还是应该将操作移动到B类并由A触发? - 这个真的是调用吗?还是事件处理更好呢?

3

eirikma的回答非常简单且出色。我可能还需要补充一点:不要使用无法使用的公共可访问方法getFriend()来获取朋友,您可以更进一步,禁止在没有令牌的情况下获取朋友:getFriend(Service.FriendToken)。这个FriendToken将是一个具有私有构造函数的内部公共类,因此只有Service才能实例化它。


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 类具有私有构造函数,因此这是一种更安全的做法。

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类提供友好访问仅限于应用程序.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;”有点长,但通过使用注释可能可以缩短它,同时保留编译时检查。


这并不像普通类那样工作,因为在同一包中的普通类可以调用getFriend()方法并绕过私有方法。 - user2219808

1
我认为使用友元访问器模式的方法过于复杂。我曾经遇到同样的问题,我使用了在C++中广为人知的好用的旧式拷贝构造函数,在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。就是这样。

1
但是 ProtectedContainer 可以在包外被子类化! - Raphael

1
在Java中,可以实现“包相关的友好性”。 这对于单元测试非常有用。 如果您在方法前面不指定private/public/protected,则它将成为“包中的友好方法”。 同一包中的类将能够访问它,但在类外部它将是私有的。
这个规则并不总是被人们所知,它是C++“friend”关键字的一个很好的近似。 我认为它是一个很好的替代品。

1
这是正确的,但我真正想问的是驻留在不同包中的代码... - Matthew Murdoch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接