Java的final方法与c++的非虚函数



Java的final方法和c++的非虚方法是不同的还是相同的?如何?

它们是不同的。

  • c++的非虚方法不会被分派,也不会覆盖任何东西。

  • Java的最终方法是被分派的,并且可以覆盖它们类的父类中的方法。

然而,它们的相似之处在于c++的非虚方法和Java的final方法都不能被重写。它们在以下意义上也是相似的:如果你有一个对象的静态类型是所讨论的类型,运行时系统不需要来分派方法调用。为了说明它们的区别,考虑以下两个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.

在等效的c++中,B::toString()是非虚拟的,我认为a.toString()不能分派给B::toString()。(我的c++有点生疏了…)


(事实上,Java JIT编译器能够检测不需要虚拟调度的情况…没有你声明类或方法为final。因此,final的真正目的是指定一个方法不能被重写或者一个类不能被扩展…

您仍然可以在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的派生类之间,仍然会发生虚拟分派(以及动态多态意义上的重写)。只是以后对函数的重写是严格禁止的(即,在基类中将foo()标记为final的情况下尝试上述代码会在派生类的声明中给出错误,因为那里的foo()是不允许的)。

如你所见,这两个概念是完全不同的。

  • 在c++中,对于非虚成员函数,不会发生虚调度(因此,没有传统意义上的"重写"),但允许具有同名成员函数的派生类(并且它有时在"静态多态性"中很有用)。
  • 在Java中,对于final成员函数,仍然会发生虚拟调度,但严格禁止在后续派生类中重写。

在c++中使用虚函数和非虚函数会产生性能差异,相反在Java中可能没有性能差异。在Java中,将方法标记为final纯粹是为了代码的清晰度和可维护性(它不是默认行为,相对较少使用),而在c++中,非虚函数是默认行为,通常使用部分是因为它们具有更好的性能特征。

在Java中,生成的代码可以根据它的使用方式而有所不同,而c++必须在编译时生成正确性。

。如果JVM检测到一个"虚拟"方法只有一个或两个常用的实现,它可以内联这些方法,或者将只使用一个实现的"虚拟"方法视为final。

最新更新