Почему наследование ведет себя по-разному в Java и C++ с суперклассами, вызывающими (или не) подклассами?
Я написал - как бы то ни было - точно такой же пример наследования как на Java, так и на C++. Я очень удивлен, увидев различные результаты этих программ. Позвольте мне поделиться как фрагментами кода, так и соответствующими выводами.
C++ Код:
class A
{
public:
A() {}
void sleep() {
cout << "A.Sleep" << endl;
eat();
}
void eat() {cout << "A.Eat" << endl;}
};
class B: public A
{
public:
B() {}
void sleep() {
A::sleep();
cout << "B.Sleep " <<endl;
this->eat();
}
void eat() {
cout << "B.Eat" << endl;
run();
}
void run() {
A::sleep();
cout << "B.run" << endl;
}
};
int main()
{
B *b = new B();
b->sleep();
}
Выход:
A.Sleep
A.Eat
B.Sleep
B.Eat
A.Sleep
A.Eat
B.run
executed successfully...
Код Java:
class A
{
A() {}
void sleep() {
System.out.println("A.Sleep");
this.eat();
}
void eat() { System.out.println("A.Eat");}
};
class B extends A
{
B() {}
@Override
void sleep() {
super.sleep();
System.out.println("B.Sleep");
this.eat();
}
@Override
void eat() {
System.out.println("B.Eat");
run();
}
void run() {
super.sleep();
System.out.println("B.Run");
}
}
public class Test {
public static void main(String[] args) {
B b = new B();
b.sleep();
}
}
Выход:
A.Sleep
B.Eat
A.Sleep
B.Eat
A.Sleep
......
......
......
(Exception in thread "main" java.lang.StackOverflowError)
Я не знаю, почему эти два примера наследования ведут себя по-разному. Не должно ли оно работать аналогичным образом? Мне очень интересно узнать... что объясняет этот сценарий?
Ответы
Ответ 1
В примере C++ вы скрываете базовые методы, но вы не переопределяете их. Таким образом, они на самом деле разные методы, которые имеют одно и то же имя. Если вы звоните
A* a = new B();
a->sleep();
он на самом деле напечатает "A.Sleep"
. Если вы хотите переопределить метод, вам нужно объявить его virtual
в базовом классе (автоматически сделать его виртуальным во всех подклассах тоже). Вы можете больше узнать о функции, скрывающейся против переопределения в C++ в этом сообщении.
В вашем примере Java вы фактически переопределяете методы, поэтому они являются одним и тем же методом. Один заменил старый. Вы можете думать об этом так: все функции Java тайно обозначаются как virtual
, то есть они могут быть переопределены. Если вы хотите, чтобы метод не был переопределяемым на Java, вы должны объявить его final
.
Ответ 2
Примечание: будьте осторожны, каждый язык - это собственный способ мышления. Существует много способов интерпретировать/реализовать OO. Даже если C++ и Java выглядят одинаково, они далеки от аналогичных.
На обоих языках компилятор проверяет во время компиляции, если вы можете вызвать метод, изучив класс (и тот, который унаследован от текущего и т.д.) Для метода правильной подписи и видимости. Что отличает разные вещи от того, как действительно вызывается звонок.
C++:
В случае не виртуальных методов вызванный метод полностью определяется во время компиляции. Вот почему, даже если объект имеет класс B
, когда он выполняет A::sleep
вызов eat
разрешен как вызов A::eat
(eat
не виртуально, а компилятор вызывает A::eat
потому что вы находитесь в уровень A
). В B::sleep()
вызов this->eat()
разрешен как вызов B.eat()
потому что в этом месте this
тип B
Вы не можете спуститься в иерархию наследования (вызов, чтобы eat
в классе A
, никогда не будет называть метод eat
в классе ниже).
Имейте в виду, что в случае виртуальных методов все по-другому (оно больше похоже на случай Java, будучи другим).
Java:
В Java указанный метод определяется во время выполнения и является тем, который наиболее связан с экземпляром объекта. Поэтому, когда в A.sleep
вызов для eat
будет вызовом, связанным с типом текущего объекта, это означает тип B
(поскольку текущий объект имеет тип B
), тогда B.eat
.
Затем у вас переполнение стека, потому что, когда вы играете с объектом типа B
вызов B.sleep()
вызовет A.sleep()
, который вызовет B.eat()
, который, в свою очередь, вызовет B.run()
который вызовет A.sleep()
и т.д. в бесконечном цикле.