有没有一种方法可以模拟Java中的C ++“朋友”概念?


Answers:


466

这是我在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)。


107
+1表示“用NullPointerException打你”。非常令人印象深刻。
Nickolas 2014年

2
@Steazy有:查找NotNull,NonNull和CheckForNull批注。请查阅IDE的文档,以了解如何使用和实施这些注释。我知道IntelliJ默认情况下会嵌入它,而eclipse需要一个插件(例如FindBugs)。
Salomon BRYS 2014年

27
你可以做RomeoLove用于Julia改变永恒的love领域是final;-)。
Matthias

5
@Matthias爱情领域是静态的……我将编辑答案使其最终;)
Salomon BRYS 2015年

12
对于幽默和很好的例子,所有答案都应该像这样(Y)+1。
Zia Ul Rehman Mughal

54

Java的设计者明确拒绝了Friend的想法,因为它可以在C ++中使用。您将“朋友”放在同一包中。私有,受保护和打包的安全性是语言设计的一部分。

詹姆斯·高斯林(James Gosling)希望Java成为无错的C ++。我相信他认为朋友是一个错误,因为它违反了OOP原则。包提供了一种合理的方式来组织组件,而又不必太过依赖OOP。

NR指出,您可以使用反射作弊,但即使不使用SecurityManager,这也才有效。如果启用Java标准安全性,则除非编写专门允许它的安全性策略,否则您将无法通过反射来作弊。


11
我并不是要成为学徒,但是访问修饰符不是安全机制。
格雷格D

6
访问修饰符是Java安全模型的一部分。我特指反射的java.lang.RuntimePermission:accessDeclaredMembers和accessClassInPackage。
David G

54
如果Gosling真的以为friend违反了OOP(尤其是超出了包访问权限),那么他真的不理解它 (很可能,很多人会误解它)。
康拉德·鲁道夫

8
类组件有时需要分开(例如,实现和API,核心对象和适配器)。包级保护同时又太宽松和太严格,以致无法正确执行此操作。
dhardy 2014年

2
@GregD 从某种意义上说,它们可以被视为一种安全机制,有助于防止开发人员错误地使用类成员。我认为最好将它们称为安全机制
2014年

45

“朋友”概念在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
因为我不知道“暴露”类中的“静态”是什么意思:静态块是Java类内部的语句块,当一个类首次加载到JVM时将执行该语句块。有关更多信息,请参见javatutorialhub。 com /…
Guy L

有趣的模式,但是它要求Exposed和Accessor类是公共的,而实现API的类(即,实现一组公共Java接口的一组Java类)最好受到“默认保护”,因此,客户端无法访问将类型与其实现分开。
晏-盖尔Guéhéneuc

8
我对Java相当生锈,因此请原谅我的无知。Salomon BRYS发布的解决方案比“ Romeo and Juliet”解决方案有什么优势?如果我在代码库中偶然发现了此实现,那么它会吓我一跳(不附加您的解释,即不加评论)。罗密欧与朱丽叶的方法很容易理解。
Steazy 2014年

1
这种方法将使问题仅在运行时可见,而Romeo和Juliet的不当使用将使问题在编译时得以开发。
ymajoros

1
@ymajoros Romeo和Juliet示例在编译时不会使滥用可见。它依赖于正确传递的参数和引发的异常。这些都是运行时操作。
Radiodef


7

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

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

考虑一下

  • 如果它们紧密相关,为什么将这些类放在不同的程序包中?
  • 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();
    }
}

最后:实体类,仅对类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;”更长。但是可以通过使用批注在保留编译时检查的同时缩短它。


这不太正常,因为同一包中的普通类只能通过getFriend()然后绕过私有方法来调用受保护的方法。
user2219808

1

在Java中,可能具有“与包装相关的友好性”。这对于单元测试可能非常有用。如果未在方法前指定私有/公共/受保护,则它将是“包中的朋友”。同一包中的类可以访问它,但在该类之外是私有的。

该规则并不总是已知的,它很好地近似于C ++“ friend”关键字。我发现它是一个很好的替代品。


1
的确如此,但是我真的在问代码驻留在不同的程序包中吗
Matthew Murdoch 2010年

1

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


嗯,不,不是。它更像是现实生活中的友谊:它可以但不一定是相互的(A是B的朋友并不意味着B被视为A的朋友),而且您和您的朋友可以来自完全不同的地方家庭,并且有自己的朋友圈,可能(但不一定)有重叠的圈子。(并不是我想和很多朋友一起上课。这可能是一个有用的功能,但应谨慎使用。)
Christopher Creutzig 2015年

1

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


1
但是ProtectedContainer可以在包外部进行子类化!
拉斐尔

0

如果要访问受保护的方法,则可以创建要使用的类的子类,该子类公开要用作公共(或为了更安全而在名称空间内部使用)的方法,并在您的类中具有该类的实例(将其用作代理)。

就私有方法而言(我认为),您很不幸。


0

我同意,在大多数情况下,不需要Friend关键字。

  • 在大多数情况下,如果您有一组紧密交织的类,则Package-private(也称为默认值)就足够了
  • 对于想要访问内部的调试类,我通常将方法设为私有,并通过反射对其进行访问。速度通常在这里并不重要
  • 有时,您实现的方法是“ hack”,否则可能会发生变化。我将其公开,但是使用@Deprecated表示您不应该依赖现有的此方法。

最后,如果确实有必要,则在其他答案中提到朋友访问者模式。


0

不使用关键字左右。

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


3
我认为这是个坏主意,甚至暗示我对此感到厌恶。显然,这充其量是杂物,不应作为任何设计的一部分。
shsteimer

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 ++的友好机制相比,这还具有一个优势,因为它允许您限制对 每个实例级别(而不是每个类级别)。通过控制访问者引用,可以控制对对象的访问。您还可以创建多个访问器,并为每个访问器赋予不同的访问权限,从而可以对哪些代码可以访问哪些内容进行细粒度的控制:

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。它也不需要任何反射,因此可以在任何环境中使用。


0

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


0

我更喜欢委派,组成或工厂班级(取决于导致此问题的问题),以避免使其成为公共班级。

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

如果是“我不想公开这个类/方法只是为了在不同的程序包中为某些其他类提供此功能”问题,那么我将在同一程序包中使用一个公共委托类,并且仅公开部分功能“局外人”类需要的。

其中一些决定是由目标服务器类加载体系结构(OSGi捆绑软件,WAR / EAR等),部署和程序包命名约定驱动的。例如,上面提出的解决方案“朋友访问者”模式对于普通的Java应用程序来说很聪明。我不知道由于类加载样式的差异而在OSGi中实现它是否棘手。


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

-1

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.