Inheritance in OOPS: An Idea of Code Reusability

What is Inheritance?

Inheritance is a core principle of object-oriented programming (OOP) that allows us to derive a class from another class or a hierarchy of classes that share a set of attributes and methods. It is a relationship between a superclass (a generalized class) and a subclass (a specialized class), where subclasses inherits data and behavior from the superclass.

  • Inheritance represents the IS-A relationship.
  • A class derived from another class is called a subclass (derived class or child class), and a class from which the subclass is derived is called a superclass (base class or parent class).

Inheritance in oops with example

How to use inheritance?

We use keyword extends to implement inheritance in Java.

class Superclass
{
    //methods and attributes
}

class Subclass extends Superclass
{
    //methods and attributes
}
  • In the subclass, the inherited attributes or methods can be used directly like any other attributes or methods.
  • The subclass can declare new attributes or methods that are not in the superclass.
  • The subclass can write a new method with the same signature as the one in the superclass, a process called method overriding.
  • It is not recommended, but the subclass can also declare an attribute with the same name as the one in the superclass, hiding it.

Inheritance in Java: code example 1

class Calculator { 
    int c; 
    public void add(int a, int b) {
        c = a + b; 
        System.out.println("Sum:" + c); 
    } 
  
    public void subtract(int a, int b) { 
        c = a - b; 
        System.out.println("Subtraction:" + c); 
    }
}
 
public class AdvancedCalculator extends Calculator { 
    public void multiplication(int a, int b) { 
        c = a * b; 
        System.out.println("Multiplication:" + c); 
    }
   
    public void division(int a , int b){
        c = a / b;
        System.out.println("division:" + c); 
    }
}
  
public class CalculatorDemo {
    public static void main(String args[]) { 
        int a = 5, b = 4; 
        AdvancedCalculator Cal = new AdvancedCalculator(); 
        Cal.add(a, b); 
        Cal.subtract(a, b); 
        Cal.multiplication(a, b); 
        Cal.division(a, b);
    }
}

In the above program, when an object of AdvancedCalculator class is created, a copy of all methods and fields of the superclass Calculator acquire memory in this object. So by using an object of a subclass we can also access the members of a superclass.

Shared object memory in inheritance

When to use Inheritance?

When classes are closely related, we can identify common attributes and methods and add them to a superclass. We can then use inheritance to define subclasses and specialize them with additional capabilities beyond those inherited from the superclass.

On the other hand, if subclasses are larger than necessary, they can waste memory and processing resources. In this case, we can extend the superclass to include only the functionality that is needed. This helps avoid unnecessary complexity and resource usage.

Advantages of Inheritance

Code reusability: One of the primary purposes of inheritance is code reuse. If we have an existing class A and we want to create a class B that includes some of the code from class A, we can derive class B from class A and reuse the data and methods of class A.

Avoiding code duplication: Inheritance helps reduce the amount of duplicate code by sharing common code among multiple subclasses. If similar code exists in two related classes, we can move the common code to a shared superclass.

Improving code flexibility and extensibility: Any changes made to the attributes and methods of a superclass will be automatically applied to the derived class. This means that the common attributes and methods of all classes in the hierarchy can be declared in a superclass, and if changes are needed, they can be made in the superclass and inherited by the subclasses. The subclass can also add new attributes or methods if needed.

Provide better code structure and management: Inheritance also makes the sub-classes follow a standard interface. It provides a clear code structure that is easy to understand because classes become grouped together in a hierarchical tree structure.

Help to achieve run time polymorphism: Inheritance provides the capability of a subclass to override a superclass method by providing a new implementation.

Avoid possible code errors: Without inheritance, we need to make changes to all the existing source code files that contain the same logic. Copying and pasting code from one class to another may spread errors across multiple source code files. So inheritance helps us to avoid possible errors as well.

Preserves the integrity of the superclass: Declaring a subclass does not affect its superclass’s source code. So inheritance preserves the integrity of superclass. 

Data hiding: The base class can be set to keep some data private so that it cannot be altered by the derived class. This is an example of encapsulation, where access to data is restricted to only classes that need it for their role.

Disadvantages of Inheritance

Tight coupling: Parent and child class can get tightly coupled and both cannot be used independently. The idea is simple: When we inherit something from a parent class, we inherit every public or protected declaration, whether we need it or not. A change in the parent class will affect all child classes.

Slow performance of inherited methods: Inherited methods work slower than normal class methods due to several levels of indirection. It takes program to jump through all levels of inherited classes. If a given class has five levels of hierarchy above it, it will take five jumps to run through a function defined in each of those classes.

Extra maintenance effort on code change: Adding new features during maintenance, we need to change both parent and child classes. If a method is removed from the parent class, we need to re-factor code in case of using that method. Here things can get a bit complicated. The idea is simple: Improper use of inheritance can lead to wrong solutions!

Method Overriding in Java

We can override superclass methods so that meaningful implementation of superclass method can be defined in subclass. This is also known as runtime polymorphism. In other words, method overriding helps us to implement the idea of polymorphism, which allows different classes to have unique implementations for the same method.

If there is a requirement to override a method:

  • We can write a new method in subclass by using the same name, same access modifier, same parameters, and same return type as in the superclass.
  • The method in the derived class or classes must have a different implementation.

    class Superclass {
      // Other methods and attributes
      ......
      void someMethod() {
          //original implementation
      }
    }
    
    class Subclass extends Superclass {
      // Other methods and attributes
      ......
      @override
      void someMethod() {
      //new implementation
      }
    }

Typecasting in Java

Typecasting is a process to reference a subclass as an instance of its superclass i.e. treating the subclass as if it were of superclass type. This is a good way to create a modular code as we can write code that will work for any subclass of the same superclass. There are two types of typecasting:

Upcasting: We can create an instance of a subclass and then assign it to a superclass variable, this is called upcasting.

Dog dog = new Dog();
Animal animal = dog;
// It is okay becasue Dog is also an Animal

Downcasting: When an instance of a superclass is assigned to a subclass object, then it’s called downcasting. We need to explicitly cast this to subclass type.

Dog dog1 = new Dog();
Animal animal = dog1;

// downcast to dog again
Dog dog2 = (Dog) animal;

Some important notes

  • We can upcast any subclass to its superclass but only those objects can be downcast that were originally of the subclass type.
  • The compiler will throw an ClassCastException error at runtime when we try to perform the wrong typecasting. Below are some of the cases:
//ClassCastException case 1
Dog dog = new Dog();
Animal animal = dog;
Lion lion = (Lion) animal;

//ClassCastException case 2
Animal animal = new Animal
Lion lion = (Lion) animal;
  • The upcast object still retains the fields it had and therefore can be added back to make it a valid object of the child class type again.

Constructors in Inheritance

When we instantiate a subclass object, chain of constructor calls happens: The subclass constructor first invokes superclass constructor, and the last constructor call completed in the chain is the subclass constructor.

Constructors in inheritance visualization

A compilation error occurs if a subclass constructor calls one of its superclass constructors with arguments that do not match exactly the number and types of parameters specified in one of the superclass constructor declarations.

Inheriting constructors: A subclass inherits all members (fields, methods, and nested classes) from its superclass. Constructors are not members, so they are not inherited by subclasses. But the constructor of superclass can be invoked from the subclass either implicitly or by using the keyword super.

super keyword in Java

The super keyword allows child class to access features from parent class regardless of their value in child class. In other words, subclasses can define new local methods or attribute to use or they can use the super keyword to call inherited methods or attributes or the superclass constructor. 

So the super keyword is used for three purposes:

  • Access superclass attributes: super.variablename allows the subclass to access the value of variablename set in the superclass.
  • Calling a superclass method: super.method() allow the subclass to access the superclass implementation of the method(). This is only required if child class also has a method with the same name.
  • Using constructors: This allows us to create new instances of superclass from within a subclass. Calling the super constructor creates a new object that requires all the fields defined in the superclass constructor.
public Dog(String color, String owner){
    super(color); //parent class constructor
    this.owner = owner;
}

Note: When superclass method is overridden in subclass, sometimes subclass version often calls the superclass version to do a portion of the work. Failure to use the keyword super with the superclass method name when referencing the superclass method causes the subclass method to call itself, creating an error called infinite recursion!

Inheritance and access modifiers in java

As we have seen during encapsulation, access modifiers help us implement an information-hiding mechanism. They can also affect access to data and methods within inheritance hierarchy.

  • Private attributes or methods can only be accessed within the same class.
  • Methods and attributes without access modifier can be accessed within the same class and all other classes within the same package.
  • Protected methods and attributes can be accessed within the same class, by all subclasses, and by all classes within the same package.
  • All classes can access public attributes and methods.

Access to data and methods using inheritance and access modifiers in java

Inheritance in java: Code example 2

Animal class code

public class Animal {
    private String color;
    public Animal(){}
    public Animal(String color){
        this.color = color;
    }
    public boolean getColor() {
        return color;
    }
    public void setColor(String color) {
    this.color = color;
    }
    // method in the superclass
    public void eat() {
        System.out.println("I can eat");
    }
}

Dog class code: Inheriting Animal class

public class Dog extends Animal {
    private String owner;
    public Dog(String color, String owner){
        super(color); //parent class constructor
        this.owner = owner;
    }
    public String getOwner() {
        return owner;
    }
    public void setOwner(String owner) {
        this.owner = owner;
    }
    // overriding the eat() method
    @Override
    public void eat() {
        System.out.println("Eat both bread and meat");
    }
    
    // new method in subclass
    public void bark() {
        System.out.println("A dog can bark");
    }
}

Lion class code: Inheriting Animal class

public class Lion extends Animal {
    private String jungleName;
    public Lion(String color, String jungleName){
        super(color);//parent class constructor
        this.jungleName = jungleName;
    }
    public String getJungle() {
        return jungleName;
    }
    public void setJungle(String jungleName) {
        this.jungleName = jungleName;
    }
    // overriding the eat() method
    @Override
    public void eat() {
        System.out.println("Only eat meat");
    }
    
    // new method in subclass
    public void roar() {
        System.out.println("A lion can roar");
    }
}

Demo code for inheritance

public class AnimalInheritance {
    public static void main(String[] args) {
        Dog dog = new Dog("Black", "Shubham Gautam");
        System.out.println("Dog color" + dog.getColor());
        System.out.println("What dog eat? " + dog.eat());
        
        Lion lion = new Lion("Brown", "Africa");
        System.out.println("Lion color" + lion.getColor());
        System.out.println("What lion eat? " + lion.eat());
       
        //upcasting
        Animal animal = dog;
        System.out.println("Dog sound " + animal.bark());
        System.out.println("Dog owner " + animal.getOwner());
        
        animal = lion;
        System.out.println("Lion sound? " + lion.roar());
        System.out.println("Lion Jungle Name " + lion.getJungle());
    }
}

Types of Inheritance in Java

Single Inheritance: Subclasses inherit characteristics from a single superclass.

Multilevel Inheritance: A subclass may have its own subclasses. In other words, a subclass of a superclass can itself be a superclass to other subclasses.

Hierarchical Inheritance: A base class acts as the parent superclass to multiple levels of subclasses.

Hybrid Inheritance: A combination of one or more of the other inheritance types. Mostly, it is a situation of single and multiple inheritances. In Java, hybrid inheritance is also not possible with classes, but it can be achieved through Interfaces.

Multiple Inheritance: A subclass may have more than one superclass and inherit characteristics from all of them. Java does not support multiple inheritance with classes, but it can be achieved through Interfaces.

Types of inheritance in java

Default superclass: Except Object class, which has no superclass, every class has one and only one direct superclass (single inheritance). In the absence of any other explicit superclass, every class is implicitly a subclass of the Object class.

Important facts about inheritance

  • Declaring superclass attributes private enables the superclass implementation to change these attributes without affecting subclass implementations. 
  • Methods of a subclass cannot directly access private members of their superclass. A subclass can only change the state of private attributes in superclass through non-private getters and setters methods provided in the superclass. When possible, we can avoid protected attributes in a superclass. Instead, we can include non-private getters and setters methods that access private attributes.
  • A public method of a superclass cannot become a protected or private method in the subclass. Similarly, a protected method of the superclass cannot become a private method in the subclass. Doing this would break the IS-A relationship because it is required that all subclass objects be able to respond to method calls that are declared public in the superclass.
  • Although inheriting from a class does not require access to the class source code. If needed, we should go through its source code to understand how the class is implemented. The idea is simple: We need to ensure that we extend a class that performs well and is securely implemented.
  • If superclass doesn’t have a default constructor, subclass also needs to have an explicit constructor defined. Otherwise, it will throw compile time exception. In this case, a call to the superclass constructor is mandatory in the subclass constructor (Using super keyword) and it should be the first statement in the subclass constructor. For example, If super(name, "Dog") is not included as the first statement, the following code will not compile and will throw an error like "Implicit super constructor Animal() is undefined. Must explicitly invoke another constructor".
class Animal {
    String name;
    String species;

    Animal(String name, String species) {
        this.name = name;
        this.species = species;
    }
}

class Dog extends Animal {
    Dog(String name) {
        // Call to superclass constructor
        super(name, "Dog");
    }
}

Critical ideas to think about!

  • Why multiple and hybrid Inheritance are not supported in Java?
  • How do we use the final keyword in inheritance?
  • Which SOLID principle uses the idea of Inheritance?

Please share your feedback and insights. Enjoy learning, Enjoy oops!

More from EnjoyAlgorithms

Self-paced Courses and Blogs