Diamond inheritance problem and Polymorphism
Inheritance
1. Inheritance in Java
Definition
Java implements single inheritance through the extends
keyword, meaning a class can only directly inherit from one parent class. However, it can implement multiple behavioral contracts through interfaces (implements
).
Characteristics
- Single Inheritance Structure: A child class can only have one direct parent class.
- Multiple Interface Inheritance: A class can implement multiple interfaces (a collection of method signatures).
- Implicit Virtual Methods: All non-private, non-static, and non-final methods are virtual methods by default (i.e., they can be overridden).
- Automatic Invocation of Parent Constructor: A child class constructor must explicitly or implicitly call a parent class constructor via
super()
.
Core Mechanisms of Java Inheritance
(1) Single Inheritance Structure
- Java does not support multiple inheritance (a class can only directly inherit from one parent class), but it can implement multiple interfaces via
implements
. - All classes implicitly inherit from the
Object
class, forming a strict single-inheritance chain.
(2) Semantics of Inheritance
- Inheritance of Fields and Methods: A child class automatically inherits the non-private (public/protected/default access) fields and methods of its parent class.
- Method Overriding (Override): A child class can rewrite the methods of its parent class to achieve polymorphism.
(3) Memory Model
- Object instances are allocated on the heap and accessed via references.
- Storage of Parent Class Fields: An instance of a child class contains the fields declared in its parent class within its memory. However, these fields are part of the child object itself, not independent sub-objects.
2. Inheritance in C++
Definition
C++ implements multiple inheritance using the :
symbol, meaning a class can inherit from multiple parent classes simultaneously. This also introduces complexity (such as the diamond inheritance problem). The essence of C++ inheritance is that the derived class contains sub-objects of the base classes.
Characteristics
- Multiple Inheritance Support: A child class can have multiple direct parent classes.
- Explicit Virtual Function Declaration: Methods that can be overridden must be marked with the
virtual
keyword; otherwise, it is static binding. - Constructor Order Control: The order of parent class construction is determined by the order in which they are declared in the inheritance list and must be explicitly called in the child class's constructor initializer list.
- Virtual Inheritance: Used to solve the diamond inheritance problem (see below).
Key Differences Between Java and C++ Inheritance
Feature | Java | C++ |
---|---|---|
Multiple Inheritance Support | Not supported (only single class inheritance, but can implement multiple interfaces) | Supports multiple inheritance (of classes) |
Memory Layout | Does not explicitly contain base class sub-objects | A derived class instance contains base class sub-objects |
Accessing Parent Members | Via the super keyword |
Via the scope resolution operator :: |
Virtual Function Mechanism | All methods are overridable by default (implicitly virtual) | Requires explicit declaration of virtual methods |
Diamond Problem | Naturally avoided (single inheritance + interfaces have no member variables) | Needs to be solved via virtual inheritance |
The Diamond Problem in C++ (Diamond Inheritance)
In C++, multiple inheritance allows a child class to inherit from multiple parent classes, thus containing instances of those multiple parent classes. Diamond inheritance can lead to ambiguity in member access (ambiguity due to multiple inheritance).
A
/ \
B C
\ /
D
Assuming this inheritance relationship exists, due to C++'s inheritance characteristics, B will contain an instance of A.
C will also contain an instance of A. Then, after D inherits from both B and C, it will contain an instance of A from B and an instance of A from C. If the path is not specified (using ::
), a direct attempt to access a member of A from D will result in a compiler error because the compiler cannot determine whether to access the A instance via B or C. In reality, D contains two instances of A.
Memory layout of a D object:
[B::A::data] [B's other members] [C::A::A::data] [C's other members] [D's other members]
To solve this problem, virtual inheritance was introduced.
Definition: After B and C declare their inheritance from A using the virtual
keyword, D will retain only one instance of A. That is, B and C share a single instance of A.
Memory layout of a D object (with virtual inheritance):
[A] [B's virtual base pointer] [B's other members] [C's virtual base pointer] [C's other members] [D's other members]
This way, the compiler no longer has a path problem, because there is only one instance of A.
Example
Without virtual
#include <iostream>
class PoweredDevice {
public:
void power_on() {
std::cout << "PoweredDevice: Powering on." << std::endl;
}
};
class Scanner : public PoweredDevice {
public:
void scan() {
std::cout << "Scanner: Scanning a document." << std::endl;
}
};
class Printer : public PoweredDevice {
public:
void print() {
std::cout << "Printer: Printing a document." << std::endl;
}
};
// D inherits from B and C
class Copier : public Scanner, public Printer {};
int main() {
Copier my_copier;
// This line will cause a COMPILE ERROR!
// my_copier.power_on();
// ERROR: request for member 'power_on' is ambiguous
// The compiler doesn't know which 'power_on' to call:
// The one from Scanner's PoweredDevice part?
// Or the one from Printer's PoweredDevice part?
// You would have to specify the path, which is clumsy:
my_copier.Scanner::power_on();
my_copier.Printer::power_on(); // This calls it a second time on a different subobject!
return 0;
}
With Virtual
#include <iostream>
class PoweredDevice {
public:
void power_on() {
std::cout << "PoweredDevice: Powering on ONE shared device." << std::endl;
}
};
// Use virtual inheritance here
class Scanner : public virtual PoweredDevice {
public:
void scan() {
std::cout << "Scanner: Scanning a document." << std::endl;
}
};
// And use virtual inheritance here too
class Printer : public virtual PoweredDevice {
public:
void print() {
std::cout << "Printer: Printing a document." << std::endl;
}
};
// Copier now inherits from the "virtual" base classes
class Copier : public Scanner, public Printer {};
int main() {
Copier my_copier;
// This now works perfectly! No ambiguity.
my_copier.power_on();
// Output: PoweredDevice: Powering on ONE shared device.
// We can still use the other functions
my_copier.scan();
my_copier.print();
return 0;
}
Polymorphism
Polymorphism is one of the core concepts of Object-Oriented Programming (OOP), referring to the ability of the same operation or method to exhibit different behaviors on different objects. Its core idea is: to process multiple types through a unified interface, with the specific behavior determined by the actual type of the object. Inheritance is the foundation for implementing polymorphism.
The core of polymorphism is "the same operation exhibits different behaviors on different objects."
Polymorphism is generally divided into two types:
-
Compile-time Polymorphism: Method overloading, where methods with the same name have different parameter lists or return types.
-
Run-time Polymorphism: Method overriding and using a parent class reference to point to a child class object (a parent class variable pointing to a child class object). This can also involve casting, as well as an interface variable pointing to an instance of an implementing class.
1. Upcasting
Upcasting of Parent and Child Classes:
Treating a child class object as its direct parent class type (through the class inheritance relationship).
In terms of semantics, this is similar to "is a"; a child class instance is a parent class variable (e.g., dog
is an animal
).
Code representation:
Animal animal = new Dog();
At this point, the animal
parent class variable points to an instance of the Dog
class. When calling methods, you can call methods that are unique to the Animal
class. You can also call methods that exist in both the parent and child, in which case the child's overridden method will be executed. If the parent's field
is not private
, it can be accessed directly. However, note that if the parent and child have fields with the same name, you need to use the super
keyword to access the parent's field
. But this animal
variable cannot directly call methods that are unique to the child class.
2. Downcasting
To be able to call methods that are unique to the child class, we need to cast the parent class variable to a child class variable.
// Upcasting
ParentType parentRef = new ChildType();
// Downcasting
ChildType childRef = (ChildType) parentRef;
This way, the child class reference can call methods unique to the child class. Downcasting is often used in conjunction with upcasting.