简洁地描述here,在Java中重写私有方法是无效的,因为父类的私有方法是“自动最终的,并且从派生类中隐藏”。我的问题主要是学术性的。
如何不违反封装,不允许父级的私有方法被“覆盖”(即,在子类中使用相同的签名独立实现)?根据封装原则,子类无法访问或继承父级的私有方法。它是隐藏的。
那么,为什么要限制子类使用相同的名称/签名实现自己的方法呢?有没有一个很好的理论基础,或者这只是某种实用的解决方案?其他语言(C ++或C#)对此有不同的规则吗?
答案 0 :(得分:70)
您不能覆盖私有方法,但您可以在派生类中引入一个没有问题。编译好了:
class Base
{
private void foo()
{
}
}
class Child extends Base
{
private void foo()
{
}
}
请注意,如果您尝试将@Override
注释应用于Child.foo()
,则会出现编译时错误。只要您的编译器/ IDE设置为在缺少 @Override
注释时为您提供警告或错误,一切都应该很好。不可否认,我更喜欢override
作为关键字的C#方法,但在Java中这样做显然为时已晚。
至于C#处理“覆盖”私有方法 - 私有方法首先不能是虚方法,但你肯定可以在基类中引入一个与私有方法同名的新私有方法。
答案 1 :(得分:27)
好吧,允许覆盖私有方法会导致封装泄漏或安全风险。如果我们假设可能,那么我们会遇到以下情况:
假设有一个私有方法boolean hasCredentials()
,那么扩展类可以简单地覆盖它:
boolean hasCredentials() { return true; }
因此破坏了安全检查。
原始类阻止这种情况的唯一方法是声明其方法final
。但是现在,这是通过封装泄漏实现信息,因为派生类现在不能再创建方法hasCredentials
- 它将与基类中定义的类冲突。
这很糟糕:假设这个方法最初在Base
中不存在。现在,实现者可以合法地派生一个类Derived
并为其提供一个按预期工作的方法hasCredentials
。
但现在,已发布原始Base
类的 new 版本。它的公共接口不会改变(也不会改变它的不变量)所以我们必须期望它不会破坏现有的代码。只有这样,因为现在与派生类中的方法存在名称冲突。
我认为问题源于误解:
如何/不/违反封装不允许父进程的私有方法被“覆盖”(即,在子类中使用相同的签名独立实现)
括号内的文字是前面文字的对面。 Java 确实允许您“在子类中独立实现具有相同签名的[私有方法]”。如上所述,不允许这样做会违反封装。
但是“不允许父母的私有方法被”覆盖“”是不同的,并且确保封装是必要的。
答案 2 :(得分:16)
“其他语言(C ++或C#)对此有不同的规定吗?”
嗯,C ++有不同的规则:静态或动态成员函数绑定过程和访问权限强制是正交的。
为成员函数提供private
访问权限修改意味着此函数只能由其声明类调用,而不能由其他函数调用(甚至不是派生类)。当您将private
成员函数声明为virtual
,甚至是纯虚拟(virtual void foo() = 0;
)时,您允许基类从专业化中受益,同时仍然强制执行访问权限。
对于virtual
成员函数,访问权限会告诉您应该执行的操作:
private virtual
表示允许您对行为进行专门化,但成员函数的调用是由基类进行的,当然是以受控方式进行的protected virtual
表示在覆盖成员函数时应该/必须调用成员函数的上层版本因此,在C ++中,访问权限和虚拟性是相互独立的。确定函数是静态还是动态绑定是解析函数调用的最后一步。
最后,模板方法设计模式应优先于public virtual
成员函数。
参考:Conversations: Virtually Yours
本文给出了private virtual
成员函数的实际用法。
ISO / IEC14882-2003§3.4.1
如果名称查找的名称是函数名称,则名称查找可以将多个声明与名称相关联;据说声明形成一组重载函数(13.1)。名称查找成功后,将发生重载分辨率(13.3)。访问规则(第11节)仅在名称查找和功能重载解析(如果适用)成功后才被考虑。只有在名称查找之后,函数重载解析(如果适用)和访问检查成功才会在表达式处理中进一步使用名称声明引入的属性(第5节)。
ISO / IEC14882-2003§5.2.2
通常根据对象表达式的静态类型(第10节)选择在成员函数调用中调用的函数,但如果未使用aqualified-id指定该函数是virtualand,则实际调用的函数将是最终的覆盖( 10.3)对象表达式的动态类型中所选函数的注意事项[注意:动态类型是对象表达式的当前值指向或引用的对象的类型。
答案 3 :(得分:7)
子类的父级私有方法无法通过封装原则进行访问或继承。它是隐藏的。
那么,孩子为什么要这样做呢 限制实施自己的 具有相同名称/签名的方法?
没有这样的限制。你可以毫无问题地做到这一点,它不会被称为“重写”。
重写的方法受动态调度的影响,即实际调用的方法是在运行时根据调用它的对象的实际类型选择的。使用私有方法,这不会发生(根据您的第一个语句,不应该发生)。这就是声明“私人方法不能被覆盖”的含义。
答案 4 :(得分:3)
我认为你误解了那篇文章的内容。它是不表示子类“被限制使用相同的名称/签名实现自己的方法。”
这是代码,稍加编辑:
public class PrivateOverride {
private static Test monitor = new Test();
private void f() {
System.out.println("private f()");
}
public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
});
}
}
class Derived extends PrivateOverride {
public void f() {
System.out.println("public f()");
}
}
引用:
您可能合理地期望输出为“public f()”,
该引用的原因是变量po
实际上包含Derived的实例。但是,由于该方法被定义为private,因此编译器实际上会查看变量的类型,而不是对象的类型。它将方法调用转换为 invokespecial (我认为这是正确的操作码,没有检查JVM规范)而不是 invokeinstance 。
答案 5 :(得分:2)
这似乎是一个选择和定义的问题。您在Java中无法做到这一点的原因是因为规范说明了这一点,但问题更多的是规范说明的原因。
C ++允许这一点(即使我们使用虚拟关键字强制动态调度)这一事实表明,没有任何内在的原因可以解释为什么。
然而,替换方法似乎是完全合法的:
class B {
private int foo()
{
return 42;
}
public int bar()
{
return foo();
}
}
class D extends B {
private int foo()
{
return 43;
}
public int frob()
{
return foo();
}
}
似乎编译好(在我的编译器上),但D.foo与B.foo无关(即它不会覆盖它) - bar()总是返回42(通过调用B.foo)无论是否在B或D实例上调用,frob()总是返回43(通过调用D.foo)。
Java不允许覆盖该方法的一个原因是他们不想像Konrad Rudolph的例子中那样改变方法。请注意,C ++与此不同,因为您需要使用"虚拟"关键字是为了获得动态调度 - 默认情况下它没有,所以你不能修改依赖于hasCredentials方法的基类中的代码。上面的例子也可以防止这种情况,因为D.foo不会从B替换对foo的调用。
答案 6 :(得分:0)
当该方法是私有的时,它的子对象是不可见的。所以没有任何覆盖它的意义。
答案 7 :(得分:0)
我很抱歉使用术语覆盖错误且与我的描述不一致。我的描述描述了该场景。以下代码扩展了Jon Skeet的示例以描绘我的场景:
class Base {
public void callFoo() {
foo();
}
private void foo() {
}
}
class Child extends Base {
private void foo() {
}
}
用法如下:
Child c = new Child();
c.callFoo();
我遇到的问题是调用了父foo()方法,即使代码显示,我在子实例变量上调用了callFoo()。我以为我在Child()中定义了一个新的私有方法foo(),继承的callFoo()方法会调用它,但我认为kdgregory所说的一些内容可能适用于我的方案 - 可能是由于派生类构造函数的方式正在调用super(),或者可能不是。
Eclipse中没有编译器警告,代码编译完成。结果出乎意料。
答案 8 :(得分:0)
除了之前所说的任何内容之外,还有一个非常语义的原因是不允许私有方法被覆盖......他们是私人的!!!
如果我写一个课程,并且我指出某个方法是“私人的”,那么外界就应该完全看不到它。没有人应该能够访问它,覆盖它或其他任何东西。我只是应该能够知道它只是我的方法而且没有其他人会用它来捣乱或依赖它。如果有人可以捣乱它,就不能认为它是私人的。我相信它真的很简单。
答案 9 :(得分:-1)
一个类由它提供的方法和它们的行为来定义。而不是内部如何实现(例如通过调用私有方法)。
因为封装与行为而不是实现细节有关,所以私有方法与构思封装无关。从某种意义上说,你的问题毫无意义。这就像问“如何将奶油放入咖啡中而不是违反封装?”
据推测,私有方法是由公开的东西使用的。你可以覆盖它。这样做,你改变了行为。