覆盖Java中的私有方法

Posted

技术标签:

【中文标题】覆盖Java中的私有方法【英文标题】:Overriding private methods in Java 【发布时间】:2011-01-01 06:58:27 【问题描述】:

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

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

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

【问题讨论】:

我想你可能对“覆盖”的含义有些困惑。 谢谢 mmyers .. 我不相信我对“覆盖”的含义感到困惑,尽管不可否认,我的描述有歧义,主要是由于对 java 中 @Override 语法的需求的混淆实现覆盖。 我这么说的原因是您一开始就说“在 Java 中重写私有方法是无效的”——这要么是误导,要么是错误的,这取决于您所说的“无效”是什么意思。我看到你已经在下面澄清了。 【参考方案1】:

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

class Base

   private void foo()
   
   


class Child extends Base

    private void foo()
    
    

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

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

【讨论】:

据我了解,如果您坚持在子类中的方法上添加@override 注释,您只会收到错误消息。 你的答案让我想起了***.com/questions/1953530/… 的答案——大多数答案只是断言“不,你不能”的事实,并解释你可以做些什么来解决这些事实,但它们并不是真的尝试回答“为什么会这样决定?”的问题 @Gregory:我不明白为什么。在这种情况下,我正在纠正 OP 的误解——他认为你不能做事实上你可以做的事情,这意味着没有“为什么”可以回答。 需要注意的是,如果有一个方法Base.boo()调用foo(),那么它将调用Base.foo()而不是Child.foo() @Sara:这是一个非常广泛的问题。如果将其设为保护,any 子类不仅可以覆盖它,还可以调用它。它成为 API 的一部分,以后很难更改。这对您来说是否是一个问题是非常具体的……如果这一切都在公司内部,那可能是可行的。如果它是一个开源项目,这是一个更大的决定。【参考方案2】:

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

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

    boolean hasCredentials()  return true; 
    

    从而破坏了安全检查。

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

    这很糟糕:假设这个方法最初在 Base 中不存在。现在,实现者可以合法地派生一个类Derived,并为其提供一个方法hasCredentials,它可以按预期工作。

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

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

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

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

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

【讨论】:

@rsp:没有。相反,乔恩和我的回答并没有分歧。 好吧,我误读了您的第 1 点和第 2 点。乍一看,我并不清楚您是在描述假设的情况。 @Konrad:这有助于阐明“为什么”。但是,对于第 1 项,这不能通过查看调用该方法的对象的类型(或调用一个公共方法,而该方法又调用私有方法)来解决吗? @Konrad:正如我在自己的回答 (***.com/questions/2000137/…) 中详述的那样,我创建了一个场景,我正在调用包含其自己的新私有方法(未覆盖)的派生类的实例,但是,它继承了一个称为私有方法的公共方法。我在派生类的实例上调用了公共方法,但它执行了父类的私有方法。我对这个结果感到惊讶。【参考方案3】:

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

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

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

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

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

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

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

参考:Conversations: Virtually Yours

文章给出了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) 在对象表达式的动态类型中选择函数[注:动态类型是对象表达式的当前值所指向或引用的对象的类型。

【讨论】:

【参考方案4】:

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

那么,为什么子类应该是 限制实施自己的 具有相同名称/签名的方法?

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

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

【讨论】:

【参考方案5】:

我认为您误解了该帖子的内容。 不是说子类“被限制使用相同的名称/签名实现自己的方法。”

这是代码,稍作修改:

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

【讨论】:

@kdgregory:我认为这与我的经历很接近,但正如我在我自己的帖子(***.com/questions/2000137/…)的回答中所描述的,我实例化了一个子类的实例并访问了一个继承的公共方法调用了有问题的私有方法,但给出的结果是父母的私有方法结果,而不是孩子的。我认为我在孩子中设置构造函数的方式可能会创建您在此处描述的相同场景,即使我的对象创建代码与您显示的语法不同。【参考方案6】:

这似乎是一个选择和定义的问题。你不能在 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 () 总是返回 43(通过调用 D.foo),无论是在 B 还是 D 实例上调用。

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

【讨论】:

【参考方案7】:

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

【讨论】:

【参考方案8】:

对于错误地使用“覆盖”一词并且与我的描述不一致,我深表歉意。我的描述描述了这个场景。以下代码扩展了 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 中没有编译器警告,代码确实编译了。结果出乎意料。

【讨论】:

正如我在回答中所说,在 C++ 中 callFoo() 会调用 Child::foo() 是的,你必须使用 protected 来处理 Java 中的这类事情。并且编译器无法警告您,因为它无法知道您的意图;您的代码完全有效。【参考方案9】:

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

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

【讨论】:

【参考方案10】:

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

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

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

【讨论】:

以上是关于覆盖Java中的私有方法的主要内容,如果未能解决你的问题,请参考以下文章

覆盖(非)静态类中的私有方法

Java继承方法隐藏(覆盖)

派生类中的抽象方法覆盖,如何使私有

超类中的私有final可以被覆盖吗?

如何覆盖私有方法并快速调用超级?

覆盖私有方法的缺陷