Index

Inheritance and Polymorphism

Java for Beginners

7.1 Understanding extends and super

Inheritance is a fundamental concept in object-oriented programming that allows one class to inherit fields and methods from another. In Java, inheritance is expressed using the extends keyword, enabling you to create hierarchical relationships between classes and promote code reuse.

What is Inheritance?

Inheritance allows a new class (called a child or subclass) to acquire properties and behaviors from an existing class (called the parent or superclass). This helps avoid duplication and makes your code more organized.

Using extends to Define a Child Class

The extends keyword indicates that a class inherits from another:

public class Animal {
    public void eat() {
        System.out.println("This animal eats food.");
    }
}

public class Dog extends Animal {
    public void bark() {
        System.out.println("The dog barks.");
    }
}

Here, Dog inherits the eat() method from Animal and also adds its own method bark().

Example Usage

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.eat();   // Inherited from Animal
        dog.bark();  // Defined in Dog
    }
}

Output:

This animal eats food.
The dog barks.
Click to view full runnable Code

class Animal {
    public void eat() {
        System.out.println("This animal eats food.");
    }
}

class Dog extends Animal {
    public void bark() {
        System.out.println("The dog barks.");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.eat();   // Inherited from Animal
        dog.bark();  // Defined in Dog
    }
}

The super Keyword: Accessing Parent Class Members

super allows subclasses to refer to their superclass, which is useful in two common scenarios:

Calling the Parent Constructor

When a subclass has a constructor, it can call the parent class constructor using super(...). This initializes the parent part of the object properly.

public class Animal {
    String name;

    public Animal(String name) {
        this.name = name;
    }
}

public class Dog extends Animal {
    public Dog(String name) {
        super(name);  // Calls Animal's constructor
    }

    public void printName() {
        System.out.println("Dog's name: " + name);
    }
}

Usage:

Dog dog = new Dog("Buddy");
dog.printName();  // Output: Dog's name: Buddy

Calling an Overridden Method from the Parent Class

If a subclass overrides a method, it can still access the original version using super.methodName().

public class Animal {
    public void sound() {
        System.out.println("Animal makes a sound");
    }
}

public class Dog extends Animal {
    @Override
    public void sound() {
        super.sound();  // Calls Animal's sound()
        System.out.println("Dog barks");
    }
}

Usage:

Dog dog = new Dog();
dog.sound();

Output:

Animal makes a sound
Dog barks
Click to view full runnable Code

class Animal {
        String name;

        public Animal(String name) {
            this.name = name;
        }

        public void sound() {
            System.out.println("Animal makes a sound");
        }
    }

    class Dog extends Animal {
        public Dog(String name) {
            super(name);
        }

        public void printName() {
            System.out.println("Dog's name: " + name);
        }

        @Override
        public void sound() {
            super.sound(); // Calls Animal's sound()
            System.out.println("Dog barks");
        }
    }
public class Main {
    public static void main(String[] args) {
        Dog dog1 = new Dog("Buddy");
        dog1.printName(); // Output: Dog's name: Buddy
        dog1.sound();     // Output: Animal makes a sound
                          //         Dog barks
    }
}

Benefits of Inheritance

Summary

Try creating your own class hierarchies using extends and practice using super to access parent class constructors and methods. This foundational skill is key to mastering Java’s object-oriented programming.

Index

7.2 Overriding Methods

In object-oriented programming, method overriding allows a subclass to provide a specific implementation of a method that is already defined in its superclass. This lets subclasses customize or extend behavior inherited from parent classes, enabling polymorphism and flexible code design.

What is Method Overriding?

When a subclass defines a method with the same signature (name, return type, and parameters) as a method in its superclass, it overrides that method. Calls to the method on an object of the subclass will execute the subclass’s version instead of the superclass’s.

The @Override Annotation

The @Override annotation is optional but highly recommended. It tells the compiler that you intend to override a method, enabling it to check your method signature for correctness and avoid mistakes such as misspellings or incorrect parameters.

Example:

@Override
public void draw() {
    // subclass-specific code
}

If the method doesn't correctly override a superclass method, the compiler will raise an error.

Rules for Overriding

Example: Shape, Circle, and Rectangle

public class Shape {
    public void draw() {
        System.out.println("Drawing a generic shape");
    }
}

public class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

public class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

Polymorphic Behavior with Overriding

Because all these classes share the same method signature, you can write code that treats all shapes uniformly while still calling their specific implementations:

public class Main {
    public static void main(String[] args) {
        Shape[] shapes = { new Shape(), new Circle(), new Rectangle() };

        for (Shape shape : shapes) {
            shape.draw();  // Calls the appropriate method based on the object’s actual type
        }
    }
}

Output:

Drawing a generic shape
Drawing a circle
Drawing a rectangle

This demonstrates polymorphism — the ability of different objects to respond uniquely to the same method call.

Click to view full runnable Code

class Shape {
        public void draw() {
            System.out.println("Drawing a generic shape");
        }
    }

    class Circle extends Shape {
        @Override
        public void draw() {
            System.out.println("Drawing a circle");
        }
    }

    class Rectangle extends Shape {
        @Override
        public void draw() {
            System.out.println("Drawing a rectangle");
        }
    }


public class Main {


    public static void main(String[] args) {
        Shape[] shapes = { new Shape(), new Circle(), new Rectangle() };

        for (Shape shape : shapes) {
            shape.draw();  // Polymorphic call
        }
    }
}

Why Is Overriding Important?

Summary

Experiment by creating your own class hierarchy with overridden methods and observe how Java chooses which method to call at runtime. This technique is foundational for advanced Java programming.

Index

7.3 Using instanceof and Type Casting

In Java’s inheritance hierarchy, objects of subclasses can be treated as instances of their superclass, but sometimes you need to identify an object’s actual type at runtime or convert references between classes. Java provides the instanceof operator for type checking and supports type casting to convert references safely.

What is instanceof?

The instanceof operator tests whether an object is an instance of a specified class or interface. It returns true if the object can be safely treated as that type, otherwise false.

Syntax:

objectRef instanceof ClassName

Example:

if (obj instanceof Dog) {
    System.out.println("obj is a Dog");
}

Upcasting and Downcasting Explained

Upcasting (Implicit)

Assigning a subclass object to a superclass reference is called upcasting and happens automatically (implicitly):

Dog dog = new Dog();
Animal animal = dog;  // Upcasting Dog to Animal

Upcasting is safe because a Dog is an Animal, so you can use the animal reference to access only the members defined in Animal.

Downcasting (Explicit)

Assigning a superclass reference back to a subclass reference is called downcasting and requires an explicit cast because it’s potentially unsafe:

Animal animal = new Dog();
Dog dog = (Dog) animal;  // Downcasting Animal to Dog

If animal actually refers to a Dog, the cast works. If it refers to a different subclass or a plain Animal, a ClassCastException will be thrown at runtime.

Using instanceof to Ensure Safe Downcasting

Before downcasting, you should check the object’s type using instanceof to avoid exceptions:

if (animal instanceof Dog) {
    Dog dog = (Dog) animal;  // Safe downcast
    dog.bark();
} else {
    System.out.println("The animal is not a dog");
}

Practical Example with Inheritance Hierarchy

class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    public void bark() {
        System.out.println("Dog barks");
    }
}

class Cat extends Animal {
    public void meow() {
        System.out.println("Cat meows");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Dog();  // Upcasting

        myAnimal.makeSound();  // Animal method or overridden version

        // myAnimal.bark(); // Compile error: method not visible via Animal reference

        if (myAnimal instanceof Dog) {
            Dog myDog = (Dog) myAnimal;  // Downcasting after check
            myDog.bark();
        }

        Animal anotherAnimal = new Cat();

        if (anotherAnimal instanceof Dog) {
            Dog dog = (Dog) anotherAnimal;  // This block won't execute
        } else {
            System.out.println("Not a Dog, cannot cast");
        }
    }
}

Output:

Animal makes a sound
Dog barks
Not a Dog, cannot cast

Common Pitfalls and Best Practices

Summary

Try experimenting by creating your own class hierarchy and practice safe casting with instanceof. Understanding these concepts is vital for mastering Java’s polymorphic behaviors.

Index

7.4 Dynamic Method Dispatch

Java’s power in object-oriented programming largely comes from its ability to support polymorphism, allowing a single reference type to point to objects of multiple types. The core mechanism enabling this is called dynamic method dispatch. This runtime process decides which overridden method implementation to execute based on the actual object type, not the declared reference type.

What is Dynamic Method Dispatch?

Dynamic method dispatch is Java’s way of selecting the correct version of an overridden method when a superclass reference points to a subclass object. At compile time, the compiler checks that the method exists in the reference type, but the actual method called at runtime depends on the object’s real class.

This makes Java’s method calls dynamic and flexible.

Example: Shape Drawing

Consider a simple hierarchy of shapes:

class Shape {
    public void draw() {
        System.out.println("Drawing a generic shape");
    }
}

class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

Here, all subclasses override the draw() method to provide their specific drawing behavior.

Demonstration of Dynamic Dispatch

public class Main {
    public static void main(String[] args) {
        Shape shape;

        shape = new Shape();
        shape.draw();  // Calls Shape's draw()

        shape = new Circle();
        shape.draw();  // Calls Circle's draw()

        shape = new Rectangle();
        shape.draw();  // Calls Rectangle's draw()
    }
}

Output:

Drawing a generic shape
Drawing a circle
Drawing a rectangle

Despite shape being declared as type Shape, the runtime object type determines which draw() method runs.

Click to view full runnable Code

class Shape {
    public void draw() {
        System.out.println("Drawing a generic shape");
    }
}

class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape shape;

        shape = new Shape();
        shape.draw();  // Drawing a generic shape

        shape = new Circle();
        shape.draw();  // Drawing a circle

        shape = new Rectangle();
        shape.draw();  // Drawing a rectangle
    }
}

Why Does This Matter?

If Java used the reference type to decide which method to call (static dispatch), the output would always be:

Drawing a generic shape
Drawing a generic shape
Drawing a generic shape

This would ignore the subclasses’ custom behavior, making polymorphism impossible.

Dynamic dispatch enables:

How Does Dynamic Dispatch Work Internally?

Practical Use: Polymorphic Collections

Shape[] shapes = { new Circle(), new Rectangle(), new Shape() };

for (Shape s : shapes) {
    s.draw();  // Calls the appropriate draw() method dynamically
}

Output:

Drawing a circle
Drawing a rectangle
Drawing a generic shape

This pattern is common in frameworks, GUI toolkits, and APIs where objects behave differently but share a common interface.

Summary

Experiment by creating your own class hierarchy with overridden methods and try calling those methods through superclass references. Watch how Java dynamically selects the correct implementation — this is the heart of polymorphism in Java!

Index