【问题标题】:Why all java methods are implicitly overridable?为什么所有 java 方法都是隐式可覆盖的?
【发布时间】:2009-05-07 19:56:51
【问题描述】:

在 C++ 中,我必须明确指定“virtual”关键字才能使成员函数“可覆盖”,因为当成员函数可覆盖时,会产生创建虚拟表和 vpointer 的开销(因此每个成员函数都是隐式的出于性能原因不可覆盖)。

当子类提供具有相同名称和签名的单独实现时,它还允许隐藏成员函数(如果未覆盖)。

在 C# 中也使用了相同的技术。我想知道为什么 Java 放弃了这种行为,并在默认情况下使每个方法都可覆盖,并提供了在显式使用“final”关键字时禁用覆盖行为的能力。

【问题讨论】:

标签: java c++ virtual-functions


【解决方案1】:

更好的问题可能是“为什么 C# 有非虚拟方法?”或者至少,为什么默认情况下它们不是虚拟的,并且可以选择将它们标记为非虚拟?

在 C++ 中,有一个想法(正如 Brian 很好地指出的那样),如果你不想要它,你就不用付钱。问题是,如果你确实想要它,这通常意味着你最终会为此付出代价。在大多数 Java 实现中,它们是为大量虚拟调用而明确设计的; vtable 实现往往很快,几乎不比非虚拟调用贵,这意味着失去了非虚拟函数的主要优势。此外,JIT 编译器可以在运行时内联虚函数。因此,出于效率原因,实际上几乎没有理由使用非虚拟函数。

因此,它在很大程度上归结为最小意外原则。它告诉我们所有方法的行为方式都相同,不是一半是虚拟的,一半是非虚拟的。由于我们至少需要一些虚拟方法来实现这种多态性,所以让它们都是虚拟的是有意义的。此外,具有相同签名的两种方法只是要求在脚上开枪。

多态性还规定对象本身应该可以控制它所做的事情。它的行为不应该取决于客户认为它是 FooParent 还是 FooChild。

编辑:所以我的断言被要求。下一段是我的推测,不是事实陈述。

所有这一切的一个有趣的副作用是 Java 程序员倾向于大量使用接口。由于虚方法优化使接口的成本基本上不存在,它们允许您使用 List(例如)而不是 ArrayList,并在以后通过简单的一行更改将其切换为 LinkedList没有额外的惩罚。

编辑:我还会提供一些资源。虽然不是原始来源,但它们确实来自 Sun,解释了 HotSpot 的一些工作原理。

Inlining

VTable

【讨论】:

  • 你做了很多假设和过度概括。愿意支持您的任何断言吗?告诉 Java 程序员他们作为一个社区(例如使用接口)的勤奋编程实践是从 Java 令人眼花缭乱的快速 vtable 方法调用演变而来的,这对 Java 程序员来说是多么侮辱。虽然你的论点听起来很有说服力,但这是一堆垃圾。只有傻瓜才会关心虚拟调用与非虚拟调用的微优化。如果您担心速度,您将不会使用 Java。我叫 BS。
  • 我认为说 Java 程序员不关心速度更侮辱人。 Sun Java 实现往往与 C++ 实现一样快,具体取决于任务。 Java 的优点之一是我们不必担心像这样的微优化:编译器和 JIT 编译器足够聪明,可以为我们完成大部分工作
  • 如果速度很重要,你应该坚持使用纯 C。:-)
  • @James,我唯一侮辱的就是你的断言。编译器,JIT'er,对你有什么不同?这都是别人编程的黑匣子。您是说 C++ 程序员以这种方式进行微优化吗?我从来没见过。除非您正在编写编译器,否则没人在乎。 Java 所做的与它为什么这样做是完全不同的答案。
  • 如果速度很重要,你应该坚持使用 asm。 :-)(或者更好的是,verilog/vhdl 并将其转储到 FPGA。)
【解决方案2】:

取自here (#34)

Java 中没有 virtual 关键字 因为所有非静态方法总是 使用动态绑定。在 Java 中, 程序员不必决定 是否使用动态绑定。这 C++ 中存在虚拟的原因是这样的 可以稍微增加一点 调优时的效率 性能(或者,换一种说法,“如果 你不使用它,你不付钱 它”),这通常会导致混乱 和不愉快的意外。决赛 关键字为 效率调整——它告诉 编译器认为这种方法不能 被覆盖,因此它可能是 静态绑定(并内联, 因此使用相当于 C++ 非虚拟通话)。这些优化 由编译器决定。

可能有点循环。

【讨论】:

  • 困惑和不愉快的意外?
  • 例如非虚拟基础::foo(),你有类派生:公共基础。你添加了derived::foo(),做一个base *d = new derived(); d->foo();由于指针的静态类型,它调用 base::foo 而不是派生的。
  • 确实是圆形的,但这意味着它是一种设计选择。那么问题就变成了他们为什么要选择它? Java 有一个复杂的运行时系统(VM),动态分派所有方法调用是一种简化的选择。当 C++ 代码被编译时,没有运行时,程序就是整个系统,并且做出始终动态的假设不会使生成的机器代码变得更简单,因为调用逻辑只是根据需要在各处复制。
  • 该说法不正确,Java 并不总是使用动态绑定。它实际上只用于重载方法,被覆盖的方法在编译时是静态绑定的。
【解决方案3】:

所以 Java 的基本原理可能是这样的:面向对象语言的全部意义在于可以扩展事物。所以就纯粹的设计而言,将可扩展性视为“特例”确实没有什么意义。

请记住,Java 可以在运行时进行编译。因此,C++ 编译中的一些性能参数被忽略了。在 C++ 中,如果一个类可能被覆盖,那么编译器必须采取额外的步骤。在 Java 中,这并不神秘:在任何给定的时刻,JVM 都知道某个特定的方法/类 是否被覆盖,这本质上是重要的是什么。

请注意,final 关键字本质上是关于程序设计,而不是优化。 JVM 不需要这些信息来查看类/方法是否已被覆盖!!

【讨论】:

  • “JVM 不需要此信息来查看类/方法是否已被覆盖”。尽管通过类加载,将非最终方法视为最终方法的 JVM,因为它碰巧没有被覆盖尚未,但以后需要一种方法来更改它。当然,更聪明的 JVM 大概可以做到这一点,并且可以在加载新子类时返回并重写对虚拟调用的非虚拟调用。但如果语言设计者不想指望这一点,我可以理解。
  • 是的,我想当然地认为 JVM 可以按照您的说法重新编译。 Sun 的 JVM 可以;我假设您在其上运行中等性能关键应用程序的任何其他 JVM 也可以。我承认可能有一些更简单的 JVM 在运行,例如在(较旧的?)移动设备上,实际上可能会根据“最终”进行优化。
【解决方案4】:

如果问题是要问 java 和 C++/C# 之间更好的方法是什么,那么它已经在另一个线程中以相反的方向讨论过,并且网上有很多可用资源

Why C# implements methods as non-virtual by default?

http://www.artima.com/intv/nonvirtual.html

最近引入的@Override 注释及其在新代码中的广泛采用,暗示了对“为什么所有 java 方法都可以隐式覆盖?”这个问题的确切答案。确实是因为设计师搞错了。 (他们已经修好了)

哦!我会为此投反对票。

【讨论】:

  • @Override 是为了确保您确实覆盖了该方法,并且不会因为错误而失败,因为它是为了确保您只是故意覆盖。
【解决方案5】:

Java 试图向更动态的语言定义靠拢,在这种定义中,一切都是对象,一切都是虚拟方法。它还希望避免模棱两可和难以理解的构造,它的设计者将其视为 C++ 中的一个缺陷,因此没有运算符重载,在这种情况下,不能在一个类层次结构上拥有两个公共方法签名,根据类型调用不同的方法引用它的变量。

C# 更关心子类的稳定性并确保子类的行为可预测。 C++ 关心性能。

三种不同的设计优先级,导致不同的选择。

【讨论】:

    【解决方案6】:

    我会说,与整个 VM 成本相比,Java 中虚拟方法的成本较低。与类似汇编的 C 背景相比,在 C++ 中,它的成本很高。由于 C 到 C++ 的迁移,没有人会决定默认通过指针调用所有方法。变化太大了。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-20
      • 1970-01-01
      • 1970-01-01
      • 2015-05-06
      相关资源
      最近更新 更多