226

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

4

18 回答 18

557

这是我在 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.cuddlepublic,但你需要Romeo.Love调用它。它使用这个Romeo.Love作为“签名安全”来确保只有Romeo可以调用这个方法并检查爱是真实的,这样运行时就会抛出一个NullPointerExceptionif it is 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构建它。我使用静态引用,因此Romeo.Love从未使用过的引用只构造一次,不会影响优化。

因此,Romeo可以cuddle Juliet而且只有他可以,因为只有他可以构造和访问一个Romeo.Love实例,这是她所需要的Julietcuddle否则她会打你一巴掌NullPointerException)。

于 2013-09-05T10:39:37.790 回答
57

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

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

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

于 2008-10-08T12:00:54.050 回答
47

“朋友”概念在 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);
    }
}
于 2008-11-25T09:44:13.493 回答
11

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

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

二是使用OSGi。这里有一篇文章解释了 OSGi 是如何做到这一点的。

相关问题:123

于 2011-05-27T15:44:03.760 回答
8

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

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

只考虑

  • 如果它们密切相关,为什么这些类在不同的包中?
  • A 可以访问 B 的私有成员,还是应该将操作移至 B 类并由 A 触发?
  • 这真的是调用还是事件处理更好?
于 2008-10-08T11:48:25.447 回答
3

eirikma 的回答既简单又出色。我可能会再添加一件事:不是使用公开访问的方法 getFriend() 来获取无法使用的朋友,您可以更进一步并禁止在没有令牌的情况下获取朋友:getFriend(Service.FriendToken)。这个 FriendToken 将是一个带有私有构造函数的内部公共类,因此只有 Service 可以实例化一个。

于 2011-08-18T02:27:23.900 回答
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该类有一个私有构造函数,因此这是一种更安全的方法。

于 2015-08-13T08:57:38.930 回答
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();
            }
        };
    }
}

好吧,我必须承认它比“friend service::Service;”要长一点。但是可以通过使用注释在保留编译时检查的同时缩短它。

于 2009-10-07T18:37:40.413 回答
1

不使用关键字左右。

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

于 2008-10-08T11:39:35.947 回答
1

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

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

于 2010-12-13T10:57:42.617 回答
1

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

于 2012-03-26T18:58:49.807 回答
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 中定义它们受保护。稍后在您的应用程序中,您编写一个私有包装类并将受保护的方法公开为公共的。就是这样。

于 2016-08-01T16:01:29.633 回答
0

如果你想访问受保护的方法,你可以创建一个你想使用的类的子类,它将你想使用的方法公开(或者在命名空间内部更安全),并在你的类中有一个该类的实例(将其用作代理)。

就私有方法而言(我认为)你不走运。

于 2008-10-08T11:48:57.800 回答
0

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

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

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

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

于 2008-12-08T04:07:06.023 回答
0

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

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

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

于 2012-08-17T02:57:00.507 回答
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();
    }
}

经过多次头部撞击(不是好的那种),这是我的最终解决方案,我非常喜欢它。它灵活、易于使用,并且可以很好地控制类访问。(with reference only访问非常有用。)如果您对访问器/引用使用 protected 而不是 private,则 Foo 的子类甚至可以从getReference. 它也不需要任何反射,因此可以在任何环境中使用。

于 2014-04-08T21:45:26.230 回答
0

从 Java 9 开始,在许多情况下,可以使用模块来解决这个问题。

于 2017-10-11T07:05:30.450 回答
-1

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

于 2008-10-08T14:44:01.253 回答