62

正如这里简洁描述的那样,在 Java 中重写私有方法是无效的,因为父类的私有方法是“自动最终的,并且对派生类隐藏”。我的问题主要是学术性的。

不允许父类的私有方法被“覆盖”(即,在子类中独立实现,具有相同的签名)如何不违反封装?父类的私有方法不能被子类访问或继承,符合封装原则。它是隐藏的。

那么,为什么要限制子类实现具有相同名称/签名的自己的方法呢?这是否有良好的理论基础,或者这只是某种务实的解决方案?其他语言(C++ 或 C#)对此有不同的规则吗?

4

10 回答 10

70

您不能覆盖私有方法,但您可以在派生类中引入一个私有方法而不会出现问题。这编译得很好:

class Base
{
   private void foo()
   {
   }
}

class Child extends Base
{
    private void foo()
    {
    }
}

请注意,如果您尝试将@Override注释应用于Child.foo()您将收到编译时错误。只要您将编译器/IDE 设置为在缺少注释时向您发出警告或错误@Override,一切都应该很好。诚然,我更喜欢 C#override作为关键字的方法,但在 Java 中这样做显然为时已晚。

至于 C# 对“覆盖”私有方法的处理 - 私有方法首先不能是虚拟的,但您当然可以在基类中引入与私有方法同名的新私有方法。

于 2010-01-04T15:20:59.547 回答
29

好吧,允许覆盖私有方法将导致封装泄漏或安全风险。如果我们假设这是可能的,那么我们会得到以下情况:

  1. 假设有一个私有方法boolean hasCredentials(),那么扩展类可以像这样简单地覆盖它:

    boolean hasCredentials() { return true; }
    

    从而打破了安全检查。

  2. 原始类防止这种情况的唯一方法是声明它的方法final。但是现在,这是通过封装泄漏实现信息,因为派生类现在不能再创建方法hasCredentials——它会与基类中定义的方法发生冲突。

    这很糟糕:假设这个方法一开始在Base. 现在,实现者可以合法地派生一个类Derived并给它一个hasCredentials按预期工作的方法。

    但是现在,发布了原始类的新版本。Base它的公共接口不会改变(它的不变量也不会改变)所以我们必须期望它不会破坏现有的代码。只有它会这样做,因为现在与派生类中的方法存在名称冲突。

我认为这个问题源于一个误解:

不允许父类的私有方法被“覆盖”(即,在子类中独立实现,具有相同的签名)如何/不是/违反封装

括号内的文字与前面的文字相反。Java确实允许您“在子类中以相同的签名独立实现 [私有方法]”。正如我上面解释的那样,不允许这样做会违反封装。

但是“不允许父级的私有方法被“覆盖””是不同的,并且是确保封装所必需的。

于 2010-01-04T15:26:03.337 回答
17

“其他语言(C++ 或 C#)对此有不同的规则吗?”

好吧,C++ 有不同的规则:静态或动态成员函数绑定过程和访问权限强制执行是正交的。

赋予成员函数private访问权限修饰符意味着该函数只能被其声明类调用,而不能被其他人(甚至派生类)调用。当您将private成员函数声明为virtual,甚至是纯虚拟 ( virtual void foo() = 0;) 时,您允许基类从特化中受益,同时仍然强制执行访问权限。

当涉及到virtual成员函数时,访问权限会告诉你应该做什么:

  • private virtual意味着您可以专门化行为,但成员函数的调用是由基类进行的,肯定是以受控方式进行的
  • protected virtual意味着您应该/必须在覆盖成员函数时调用它的上层版本

因此,在 C++ 中,访问权限和虚拟性是相互独立的。确定函数是静态绑定还是动态绑定是解决函数调用的最后一步。

最后,模板方法设计模式应该优先于public virtual成员函数。

参考:对话:几乎是你的

文章给出了private virtual成员函数的实际使用。


ISO/IEC 14882-2003 §3.4.1

如果名称查找发现名称是函数名称,则名称查找可能会将多个声明与名称相关联;据说这些声明形成了一组重载函数(13.1)。名称查找成功后发生重载解析 (13.3)。只有在名称查找和函数重载解析(如果适用)成功后才考虑访问规则(第 11 条)。只有在名称查找、函数重载解析(如果适用)和访问检查成功之后,名称声明引入的属性才会在表达式处理中进一步使用(第 5 条)。

ISO/IEC 14882-2003 §5.2.2

在成员函数调用中调用的函数通常是根据对象表达式的静态类型(第 10 条)选择的,但如果该函数是虚拟的并且没有使用 aqualified-id 指定,那么实际调用的函数将是最终的覆盖器(10.3)对象表达式的动态类型中的选定函数 [注:动态类型是对象表达式的当前值所指向或引用的对象的类型。

于 2010-01-04T15:49:13.073 回答
7

父类的私有方法不能被子类访问或继承,符合封装原则。它是隐藏的。

那么,为什么要限制子类实现具有相同名称/签名的自己的方法呢?

没有这样的限制。您可以毫无问题地做到这一点,只是不称为“覆盖”。

被覆盖的方法受动态调度的影响,即实际调用的方法是在运行时根据调用它的对象的实际类型来选择的。使用私有方法,这不会发生(并且不应该,根据您的第一条语句)。这就是声明“不能覆盖私有方法”的含义。

于 2010-01-04T15:34:14.757 回答
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 的实例。但是,由于该方法被定义为私有,编译器实际上查看的是变量的类型,而不是对象的类型。并将方法调用转换为invokespecial(我认为这是正确的操作码,尚未检查 JVM 规范)而不是invokeinstance

于 2010-01-04T15:35:58.907 回答
3

这似乎是一个选择和定义的问题。你不能在java中这样做的原因是因为规范是这样说的,但问题更多的是规范为什么这么说。

C++ 允许这样做的事实(即使我们使用 virtual 关键字来强制动态调度)表明没有内在的理由不允许这样做。

但是,替换该方法似乎是完全合法的:

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)和 frob() 总是无论是在 B 还是 D 实例上调用,都返回 43(通过调用 D.foo)。

Java 不允许覆盖该方法的一个原因是他们不喜欢像 Konrad Rudolph 的示例中那样允许更改该方法。请注意,C++ 在这里有所不同,因为您需要使用“virtual”关键字才能获得动态调度 - 默认情况下它没有,因此您无法修改依赖于 hasCredentials 方法的基类中的代码。上面的示例也防止了这种情况,因为 D.foo 不会替换从 B 对 foo 的调用。

于 2015-05-04T07:25:51.737 回答
0

当方法是私有的时,它的子进程不可见。所以没有覆盖它的意义。

于 2010-01-04T15:36:18.247 回答
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 中没有编译器警告,并且代码确实编译了。结果出乎意料。

于 2010-01-04T22:52:58.973 回答
0

除了之前所说的之外,不允许覆盖私有方法还有一个非常语义化的原因......他们是私有的!

如果我写了一个类,并且我指出一个方法是“私有的”,那么外界应该完全看不到它。没有人应该能够访问它、覆盖它或其他任何东西。我只是应该能够知道这是我独有的方法,没有其他人会使用它或依赖它。如果有人可以弄脏它,它就不能被认为是私人的。我相信这真的很简单。

于 2013-11-15T15:05:22.617 回答
-1

一个类由它提供的方法以及它们的行为方式定义。不是这些是如何在内部实现的(例如,通过调用私有方法)。

因为封装与行为有关,而不是实现细节,所以私有方法与思想封装无关。从某种意义上说,你的问题毫无意义。这就像在问“如何在咖啡中加入奶油不违反封装?”

据推测,私有方法被公共的东西使用。你可以覆盖它。这样做,你改变了行为。

于 2010-01-04T15:32:15.717 回答