Skip to content

Inheritance and Method Overriding in Java

Question

Explain the concept of inheritance in Java, how method overriding works, and what are the key rules and best practices for implementing inheritance and overriding methods?

Answer

Inheritance is a fundamental OOP concept that allows a class (subclass/child class) to inherit properties and behaviors from another class (superclass/parent class). Method overriding occurs when a subclass provides a specific implementation of a method that is already defined in its superclass.

Key Concepts of Inheritance:

  1. Basic Inheritance Syntax

    public class Animal {
        protected String name;
    
        public Animal(String name) {
            this.name = name;
        }
    
        public void makeSound() {
            System.out.println("Some sound");
        }
    }
    
    public class Dog extends Animal {
        public Dog(String name) {
            super(name);  // Call parent constructor
        }
    
        @Override
        public void makeSound() {
            System.out.println("Woof!");
        }
    }
    

  2. Types of Inheritance

  3. Single Inheritance: One class extends one class
  4. Multilevel Inheritance: Chain of inheritance
  5. Hierarchical Inheritance: Multiple classes extend one class
  6. Multiple Inheritance: Not supported in Java (but can be achieved through interfaces)

Method Overriding Rules:

  1. Access Modifier Rules
  2. Cannot reduce visibility of overridden method
  3. Can increase visibility

    class Parent {
        protected void method() {}
    }
    
    class Child extends Parent {
        @Override
        public void method() {}  // Valid
    
        @Override
        private void method() {}  // Invalid
    }
    

  4. Return Type Rules

  5. Must be same or covariant (subtype)

    class Parent {
        public Number getValue() { return 1; }
    }
    
    class Child extends Parent {
        @Override
        public Integer getValue() { return 1; }  // Valid
    
        @Override
        public String getValue() { return "1"; }  // Invalid
    }
    

  6. Exception Rules

  7. Cannot throw broader checked exceptions
  8. Can throw narrower or unchecked exceptions
    class Parent {
        public void method() throws IOException {}
    }
    
    class Child extends Parent {
        @Override
        public void method() throws FileNotFoundException {}  // Valid
    
        @Override
        public void method() throws Exception {}  // Invalid
    }
    

Best Practices:

  1. Use @Override Annotation

    class Child extends Parent {
        @Override  // Always use this annotation
        public void method() {}
    }
    

  2. Call Super Method When Needed

    class Child extends Parent {
        @Override
        public void method() {
            super.method();  // Call parent implementation
            // Add child-specific behavior
        }
    }
    

  3. Maintain Liskov Substitution Principle

    class Bird {
        public void fly() {
            // Implementation
        }
    }
    
    class Penguin extends Bird {
        @Override
        public void fly() {
            throw new UnsupportedOperationException("Penguins can't fly");
        }
    }
    

Common Pitfalls:

  1. Static Methods
  2. Static methods cannot be overridden
  3. They are hidden instead

    class Parent {
        public static void method() {
            System.out.println("Parent");
        }
    }
    
    class Child extends Parent {
        public static void method() {
            System.out.println("Child");
        }
    }
    

  4. Private Methods

  5. Private methods cannot be overridden

    class Parent {
        private void method() {}
    }
    
    class Child extends Parent {
        private void method() {}  // This is a new method, not an override
    }
    

  6. Final Methods

  7. Final methods cannot be overridden
    class Parent {
        public final void method() {}
    }
    
    class Child extends Parent {
        public void method() {}  // Compilation error
    }
    

Example of Good Inheritance Design:

public abstract class Shape {
    protected String color;

    public Shape(String color) {
        this.color = color;
    }

    public abstract double getArea();
    public abstract double getPerimeter();

    public String getColor() {
        return color;
    }
}

public class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }

    @Override
    public double getPerimeter() {
        return 2 * Math.PI * radius;
    }
}

This example demonstrates: - Proper inheritance hierarchy - Method overriding with @Override annotation - Abstract methods and classes - Constructor chaining - Access modifier usage