java final 方法和 c++ 非虚拟方法是不同还是相同?如何?
4 回答
它们是不同的。
C++ 非虚拟方法不会被调度,也不会覆盖任何东西。
Java final 方法被调度,并且可以覆盖其类超类中的方法。
但是,它们在 C++ 非虚拟方法或 Java 最终方法都不能被覆盖方面是相似的。它们在某种意义上也是相似的,如果您有某个对象的静态类型是所讨论的类型,则运行时系统不需要调度方法调用。
为了说明区别,请考虑以下两个 Java 类:
public class A {
public String toString() {
return "A";
}
}
public class B extends A {
public final String toString() {
return "B";
}
}
A a = ...
B b = ...
a.toString(); // could call A.toString() or B.toString() - dispatch
b.toString(); // could only call B.toString() - no dispatch required
// but it will behave the same as if dispatching had occurred.
在 B::toString() 是非虚拟的 C++ 等效项中,我相信a.toString()
无法分派到B::toString()
. (我的 C++ 有点生疏……)
(事实上,Java JIT 编译器能够检测不需要虚拟分派的情况......而无需将类或方法声明为final
。因此,真正的目的final
是指定不应重写方法或应不被扩展......并让 Java 编译器为你检查这个。)
您仍然可以在 C++ 中继承类时声明具有相同签名的非虚拟成员函数,因为 Java 明确禁止声明具有相同签名的方法,其中基类声明该方法为 final。C++ 中的虚拟性只是帮助在处理继承/多态性时找到要调用的正确函数。
例子:
#include <iostream>
class Base
{
public:
void doIt()
{
std::cout << "from Base.doIt()" << std::endl;
}
};
class Child : public Base
{
public:
void doIt()
{
std::cout << "from Child.doIt()" << std::endl;
}
};
int main()
{
Base a;
a.doIt(); // calls Base.doIt()
Child b;
b.doIt(); // calls Child.doIt()
Base *c = new Base();
c->doIt(); // calls Base.doIt()
Child *d = new Child();
d->doIt(); // calls Child.doIt()
Base *e = new Child();
e->doIt(); // calls Base.doIt()
std::cin.ignore();
return 0;
}
Java 中使用 final 的类似示例将导致编译器错误:
public class Base
{
public final void doIt()
{
System.out.println("In Base.doIt()");
}
}
public class Child extends Base
{
public void doIt() // compiler error: Cannot overload the final method from Base
{
System.out.println("In Child.doIt()");
}
}
有关 C++ 中多态性的更多说明,请参阅cplusplus.com:多态性
但实际上,这两种方法都有相似的目标:防止覆盖基类中的函数。他们只是以稍微不同的方式去做。
它们是非常不同的,事实上,我会说,完全不相关。
在 C++ 中,如果基类具有非虚拟成员函数,则可以在派生类中声明具有相同名称的非虚拟成员函数。效果将是派生类的成员函数将隐藏基类的成员函数。并且不会发生虚拟调度。如下所示:
struct Base {
void foo() {
std::cout << "Base::foo called!" << std::endl;
};
};
struct Derived : Base {
void foo() {
std::cout << "Derived::foo called!" << std::endl;
};
};
int main() {
Derived d;
d.foo(); //OUTPUT: "Derived::foo called!"
Base* b = &d;
b->foo(); //OUTPUT: "Base::foo called!"
};
上面展示了派生类的成员函数是如何隐藏基类函数的。如果您有一个指向基类的指针,由于函数是非虚拟的,因此虚拟表不用于解析调用,因此将调用基类中的 foo 函数。这里的要点是,在 C++ 中,没有什么可以阻止您在派生类中创建另一个同名函数(请注意,不同的签名仍会导致隐藏所有基类的同名成员函数)。您将得到的只是一个编译器警告,告诉您派生类的成员函数隐藏了基类的成员函数。
Java 中的 final 成员函数完全不同。在 Java 中,所有成员函数都是虚函数。所以你不能像在 C++ 中那样关闭虚拟调度。final 成员函数只是意味着不允许任何后续派生类(将发生错误)声明具有相同名称(和签名)的成员函数。但是在声明原始成员函数的接口/基类和将其标记为 final 的派生类之间仍然发生虚拟调度(因此,在动态多态意义上,重写)。只是后来严格禁止对函数的覆盖(即在 Base 类中尝试使用 foo() 标记为 final 的上述代码会在 Derived 类的声明中出错,因为 foo() 不会有允许)。
如您所见,这两个概念完全不同。
- 在 C++ 中,使用非虚拟成员函数,不会发生虚拟调度(因此,没有传统意义上的“覆盖”),但您可以拥有具有同名成员函数的派生类(有时它可能很有用在“静态多态性”中)。
- 在 Java 中,使用 final 成员函数,仍然会发生虚拟调度,但严格禁止在后续派生类中覆盖。
使用虚拟与非虚拟函数是 C++ 可以产生性能差异,相反,Java 可能没有性能差异。在 Java 中,将方法标记final
为纯粹是为了代码的清晰性和可维护性(它不是默认行为并且相对很少使用),而在 C++ 中,非虚拟函数是默认行为并且常用部分是因为它们具有更好的性能特征。
在 Java 中,生成的代码可能会根据其使用方式而有所不同,而 C++ 必须在编译时生成正确性。
例如,如果 JVM 检测到“虚拟”方法只有一个或两个常用实现,它可以内联这些方法或将仅使用一个实现的“虚拟”方法视为最终实现。