Index

Inheritance and Polymorphism Syntax

Java Syntax

8.1 Using extends

In Java, the extends keyword is used to create a subclass (also called a derived class) that inherits fields and methods from a superclass (also called a base class). This is the foundation of inheritance, one of the core principles of object-oriented programming.

Basic Syntax

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

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

In this example, Dog is a subclass of Animal. It inherits the speak() method from Animal, and adds a new method bark().

What Gets Inherited

When a class extends another:

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.speak(); // Inherited from Animal
        d.bark();  // Defined in Dog
    }
}
Click to view full runnable Code

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

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

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

Whats Not Inherited

Reflection on Code Reuse

Using extends helps avoid code duplication by centralizing shared behavior in a base class. Subclasses can:

This approach supports the "is-a" relationship, where Dog is an Animal, making the code more organized, modular, and reusable.

Use inheritance thoughtfully, ensuring it models a logical hierarchy. When used well, extends promotes clean, maintainable code through shared behavior and abstraction.

Index

8.2 Method Overriding

Method overriding allows a subclass to provide a specific implementation of a method that is already defined in its superclass. This is a key aspect of polymorphism in Java, enabling objects to exhibit different behaviors based on their actual type at runtime.

Basic Syntax and Example

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

class Dog extends Animal {
    @Override
    void speak() {
        System.out.println("The dog barks.");
    }
}

In this example, Dog overrides the speak() method inherited from Animal. The @Override annotation tells the compiler that this method is intended to override a superclass method, helping catch errors such as mismatched method signatures.

Runtime Behavior: Dynamic Dispatch

When you call an overridden method on a superclass reference that points to a subclass object, Java decides at runtime which version of the method to execute—this is called dynamic dispatch:

public class Main {
    public static void main(String[] args) {
        Animal myPet = new Dog();  // Upcasting
        myPet.speak();             // Outputs: "The dog barks."
    }
}

Although myPet is declared as type Animal, it actually refers to a Dog object. The overridden speak() method in Dog is executed.

Click to view full runnable Code

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

class Dog extends Animal {
    @Override
    void speak() {
        System.out.println("The dog barks.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myPet = new Dog();  // Upcasting
        myPet.speak();             // Outputs: The dog barks.
    }
}

Rules of Overriding

Reflection: Why Override?

Method overriding supports polymorphism, allowing one interface (e.g., Animal) to represent multiple behaviors (e.g., Dog.speak(), Cat.speak()).

This makes your code:

In essence, overriding allows subclasses to customize or extend behaviors, making your codebase better organized and more powerful.

Index

8.3 super Keyword Usage

The super keyword in Java is used to refer to the immediate superclass of a class. It is particularly useful in two main situations:

Calling the Superclass Constructor with super()

If a subclass needs to initialize part of the parent class, it can call the superclass constructor using super().

class Animal {
    Animal(String name) {
        System.out.println("Animal constructor: " + name);
    }
}

class Dog extends Animal {
    Dog() {
        super("Buddy"); // Calls Animal(String) constructor
        System.out.println("Dog constructor");
    }
}

When new Dog() is called, it first invokes the Animal constructor through super("Buddy"), then proceeds with the rest of the Dog constructor. This call must be the first statement in the subclass constructor.

Accessing Overridden Methods with super.methodName()

A subclass can override a method but still invoke the parent version using super.methodName():

class Animal {
    void speak() {
        System.out.println("Animal speaks");
    }
}

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

This is helpful when the subclass wants to extend rather than completely replace the behavior of the superclass method.

Click to view full runnable Code

class Animal {
    Animal(String name) {
        System.out.println("Animal constructor: " + name);
    }

    void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    Dog() {
        super("Buddy"); // Calls Animal(String) constructor
        System.out.println("Dog constructor");
    }

    @Override
    void speak() {
        super.speak(); // Calls Animal's speak method
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.speak();
    }
}

Reflection: When and Why to Use super

Using super is important when you:

However, overusing super can indicate tight coupling. It's best used deliberately, where superclass behavior is truly required in the subclass.

Index

8.4 Object Slicing (syntax implications)

Object slicing is a concept that arises when a subclass object is referenced by a superclass variable. In Java, all objects are accessed through references, so the term "object slicing" is often used more conceptually than literally—as Java does not physically slice objects like some other languages (e.g., C++). However, the effect is similar: when you use a superclass reference to hold a subclass object, you lose direct access to subclass-specific members.

Example:

class Animal {
    void speak() {
        System.out.println("Animal speaks");
    }
}

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

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

        myAnimal.speak();  // Works fine, Animal method accessible
        // myAnimal.bark(); // Compile-time error: method not found in Animal
    }
}

Here, myAnimal holds a reference to a Dog object, but only the methods declared in Animal are accessible. The subclass-specific bark() method cannot be called directly from the myAnimal reference.

Reflection:

This limitation is due to static typing: the compiler checks methods based on the reference type, not the actual object type. Although dynamic dispatch ensures overridden methods are correctly called at runtime, subclass-specific members require casting to access.

This behavior promotes type safety but restricts access to subclass features unless explicitly cast, reflecting a trade-off between flexibility and safety in Java's polymorphism model. Understanding this helps avoid confusion when working with inheritance and references.

Index

8.5 Final Classes and Methods (final)

In Java, the final keyword can be used to prevent inheritance and method overriding, helping enforce design constraints and improve code safety.

Declaring a final class

A final class cannot be subclassed. This means no class can extend it:

public final class ImmutableClass {
    // Class content
}

// The following will cause a compile-time error:
// public class ExtendedClass extends ImmutableClass { }

Making a class final is useful when you want to create immutable or security-sensitive classes that shouldn't be altered by inheritance.

Declaring a final method

A final method cannot be overridden by subclasses, even if the class itself is not final:

class Base {
    public final void display() {
        System.out.println("Base display method");
    }
}

class Derived extends Base {
    // The following will cause a compile-time error:
    // public void display() { System.out.println("Derived display"); }
}

Using final on methods ensures critical behavior remains unchanged in subclasses.

Click to view full runnable Code

final class ImmutableClass {
    public void show() {
        System.out.println("ImmutableClass method");
    }
}

// Uncommenting this will cause a compile-time error:
// class ExtendedClass extends ImmutableClass {}

class Base {
    public final void display() {
        System.out.println("Base display method");
    }
}

class Derived extends Base {
    // This override would cause a compile-time error:
    // public void display() {
    //     System.out.println("Derived display");
    // }
}

public class Main {
    public static void main(String[] args) {
        ImmutableClass imm = new ImmutableClass();
        imm.show();

        Derived d = new Derived();
        d.display();
    }
}

Reflection

Restricting inheritance or overriding with final helps maintain design stability, preventing unintended modifications or misuse. For example, security-sensitive classes like java.lang.String are final to guarantee consistent, reliable behavior.

At the same time, overusing final can reduce flexibility, so it's best applied thoughtfully, balancing safety and extensibility.

Index

8.6 instanceof Operator

The instanceof operator in Java is used to test whether an object is an instance of a specific class or interface before performing operations like casting. This helps avoid ClassCastException at runtime by ensuring the cast is safe.

Traditional Usage

Here is a typical example where instanceof is used to check the type of an object before casting it:

Object obj = "Hello, Java!";

if (obj instanceof String) {
    String str = (String) obj;  // Safe cast after check
    System.out.println("String length: " + str.length());
} else {
    System.out.println("Not a String object");
}

In this example, obj is declared as Object but actually references a String. The instanceof check confirms this before casting, preventing runtime errors.

How instanceof Enables Safe Polymorphism

In polymorphic code, you often have variables of a superclass or interface type pointing to subclass instances. The instanceof operator allows you to:

This improves flexibility by letting a single reference hold various subclass types, while still enabling safe and controlled behavior specific to those types.

Enhanced Pattern Matching with instanceof (Java 16)

Starting with Java 16, instanceof was enhanced with pattern matching, which simplifies type checks and casts into one step:

if (obj instanceof String str) {
    System.out.println("String length: " + str.length());
}

Here, if obj is a String, it's automatically cast and assigned to str, removing the need for a separate cast line. This feature improves readability and reduces boilerplate.

Click to view full runnable Code

public class InstanceofExample {
    public static void main(String[] args) {
        Object obj = "Hello, Java!";

        // Traditional usage
        if (obj instanceof String) {
            String str = (String) obj;  // Safe cast after check
            System.out.println("Traditional: String length = " + str.length());
        } else {
            System.out.println("Traditional: Not a String object");
        }

        // Enhanced pattern matching (Java 16+)
        if (obj instanceof String str) {
            System.out.println("Pattern matching: String length = " + str.length());
        }
    }
}

Reflection

Using instanceof judiciously enhances type safety and clarity in polymorphic code. While it helps prevent errors, excessive use may indicate design issues—sometimes better addressed with polymorphic methods or interfaces. However, combined with Java's newer pattern matching, instanceof remains a powerful tool for safe and readable type-dependent logic.

Index