在方法签名中公开声明一个包私有类型

9
这在Java中是可以实现的:
package x;

public class X {

    // How can this method be public??
    public Y getY() {
        return new Y();
    }
}

class Y {}

那么,Java编译器为什么允许我将getY()方法声明为public呢?让我感到困惑的是:类Y是包私有的,但访问器getY()在其方法签名中声明了它。但在x包之外,我只能将该方法的结果分配给Object

// OK
Object o = new X().getY();

// Not OK:
Y y = new X().getY();

好的,现在我可以尝试举一个例子来解释方法结果协变。但更糟糕的是,我还可以这样做:

package x;

public class X {
    public Y getY(Y result) {
        return result;
    }
}

class Y {}

现在我无法从x包之外调用getY(Y result)。为什么会这样?为什么编译器允许我以一种无法调用方法的方式声明一个方法?


我们不知道Java语言设计者的想法。可能他们从未考虑过这个问题,如果以后出现了也觉得不重要就没有修复它。 - bmargulies
@bmargulies:如果那是真的,那也是一个答案。一旦它在规范中了,你就不能再移除它,否则会破坏大量代码。我只是想知道是否有某种原因,比如某种协变性或其他什么... - Lukas Eder
如果问题是“为什么Java的开发者决定允许这样做”,那么这个论坛无法回答。他们可能知道,除非他们已经发布了一个解释,也许可以通过谷歌找到答案。 - bmargulies
您以为自己是正确的,没有人能轻易地找出这个问题。但也许我们只是忽略了一些非常有趣的东西。我认为这是一个有趣的问题,我不希望在今天得到答案。也许在2-3个月内,有人会碰巧发现这个问题并知道答案。我不认为stackoverflow只是用来回答简单的问题的吧?因为容易的问题可以在Google上找到答案,而不是困难的问题...除非你能向我展示一个Google查询来找到这样的解释? :-) - Lukas Eder
不,我的建议是这个问题属于类似问题的长期惯例。除非有其他四个人同意我的看法,否则它将保持不变。 - bmargulies
显示剩余3条评论
5个回答

6
设计Java时,我们付出了很多思考,但有时候一些次优设计还是会溜掉。著名的Java Puzzlers就清楚地证明了这一点。
另一个包仍然可以调用带有包私有参数的方法。最简单的方法是将其传递为null。但并不是因为你仍然可以调用它,这样的构造才真正有意义。它破坏了包私有背后的基本思想:只有包本身应该看到它。大多数人都会认为使用此构造的任何代码至少是令人困惑的,并且有一种不好的气味。最好不要允许它。
顺便说一下,允许这个构造的事实开启了更多的边角案例。例如,从一个不同的包中执行Arrays.asList(new X().getY())虽然编译通过,但在执行时会抛出IllegalAccessError,因为它试图创建一个不可访问的Y类的数组。这表明这种泄漏不可访问类型的方式不符合语言设计的其他假设。
但是,就像Java中的其他不寻常规则一样,它在Java的第一个版本中被允许。因为这不是一个很大的问题,而且向后兼容对于Java更重要,所以改善这种情况(禁止它)已经不值得了。

+1 很棒的回答。关于 Arrays.asList(...) 结构的有趣观察! - Lukas Eder
嘿。稍微编辑了描述......我试图举一个我很久以前报告过的错误示例(Sun Bug 6495506),它已经被修复,但无意中又出现了另一种仍然存在的变体。 - Wouter Coekaerts
嗯,这非常有趣。所以我们真的可以说,声明我的 getY() 方法为公共的是不可能的,但也不会被修复... - Lukas Eder
asList()示例是javac的一个bug。根据15.12.4.2,它不应该编译。getY()方法不能负责这个问题。 - irreputable

2

首先,您可以调用该方法。一个简单的例子是在同一个包内调用它。

一个非常规的例子:

package x;
public class Z extends Y {}

package x2;
    x.getY( new Z() ); // compiles

但这并不是重点。

重点是,Java试图禁止一些明显荒谬的设计,但它不能禁止所有荒唐的设计。

  1. 什么是荒唐的?那非常主观。

  2. 如果太严格了,在事物还很活跃的时候,对开发来说是不好的。

  3. 语言规范已经太复杂了;增加更多规则已经超出人类的能力。[1]

[1] http://java.sun.com/docs/books/jls/third_edition/html/names.html#6.6


+1!谢谢,兄弟。这个非平凡的例子很有道理。我完全没想到。无论如何,这使得我的例子稍微不那么荒谬了。我提出这个问题的原因是因为我在自己的代码中发现了这个非常的例子(当然,使用了比“X”、“Y”更有意义的名称),我感到非常惊讶。尽管如此,我甚至没有想过要以这种方式编写它... - Lukas Eder

2
有时候拥有一个返回非public类型实例的公开方法是很有用的,例如如果这个类型实现了一个接口。通常工厂就是这样工作的:
public interface MyInterface { }

class HiddenImpl implements MyInterface { }

public class Factory {
    public HiddenImpl createInstance() {
        return new HiddenImpl();
     }
}

当然,有人可能会认为,在这种情况下编译器应该强制createInstance()的返回值为MyInterface。然而,允许它返回HiddenImpl至少有两个优点。其一是,HiddenImpl可以实现多个独立的接口,调用者可以自由选择使用哪种类型的返回值。另一个优点是,包内的调用者可以使用相同的方法获取HiddenImpl的实例并将其用作此类实例,无需将其转换或在工厂中拥有两个方法(一个公共的MyInterface createInstance()和一个包保护的HiddenImpl createPrivateInstance())来完成相同的事情。
允许像public void setY(Y param)这样的方法也有类似的原因。可能存在Y的公共子类型,来自包外的调用者可能会传递这些类型的实例。同样,上述两个优点也适用于此处(可能存在多个这样的子类型,来自同一包的调用者可以直接传递Y实例)。

+1:好答案,还可以看看irreputable的回答。很有道理,我没有想到Y可能有公共子类型。 - Lukas Eder

1
允许它的一个重要原因是为了允许不透明类型。想象以下场景:
package x;

public interface Foo;

class X
{
    public Foo getFoo ( )
    {
        return new Y( );
    }

    class Y implements Foo { }
}

在这里,我们有你的情况(通过公共API导出的包保护内部类),但这是有道理的,因为对于调用者而言,返回的Y是不透明的类型。话虽如此,我记得NetBeans会对这种行为发出警告。


嗯...这里,Foo是一个public类型。public方法在其签名中具有public类型,这完全没问题。但在我的情况下,一个public方法在其签名中具有package privateopaque类型。我不明白,这怎么可能是正确的。 - Lukas Eder
@Lukas:在我看来,这基本上是一样的。我同意:在签名中使用公共类型是更好/更清洁的方法,但如果您愿意,语言也允许您不使用公共类型。 - Mac
好的。但我们仍然不知道为什么会这样...(即为什么语言设计者没有限制这些东西) - Lukas Eder

1

你不能做这样的事情吗:

Object answer = foo1.getY();  
foo2.setY(  foo1.getY().getClass().cast(answer)  );

是的,它确实很丑陋、愚蠢和毫无意义,但你仍然可以做到。

话虽如此,我相信你的原始代码会产生一个编译器警告。


你打开它们了吗?我知道看到过“公共方法暴露非公共API”的警告,但也许那是来自我的IDE... - Sled
嗯,我在我的Eclipse Helios中打开了几乎所有可能的警告,但我没有遇到这样的情况。虽然有这样的功能会很好!你用的是什么? - Lukas Eder
NetBeans http://www.netbeans.org 也是开源的,我非常喜欢它;当然,你的看法可能不同。 - Sled

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