Skip to content

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:

  1. Compile-time Polymorphism: Method overloading, where methods with the same name have different parameter lists or return types.

  2. 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.