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.
We use keyword extends to implement inheritance in Java.
class Superclass
{
//methods and attributes
}
class Subclass extends Superclass
{
//methods and attributes
}
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.
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.
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.
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!
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:
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 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;
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;
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.
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.
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:
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!
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.
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());
}
}
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.
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.
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");
}
}
Please share your feedback and insights. Enjoy learning, Enjoy oops!