Java Programming

Inheritance, Polymorphism, Abstract Types, Interfaces and Inner Classes

PGCP-BDA

inheritance

Java inheritance lets a class extend one superclass and inherit accessible state and behavior while interfaces supply additional type contracts.

abstract class

An abstract class cannot be instantiated and may combine abstract method contracts with shared fields, constructors and implemented behavior.

interface

An interface defines a type contract and can contain abstract, default, static and private methods plus constants under Java’s rules.

implementation class

An implementation class fulfills interface methods and supplies concrete state and behavior for instances.

method overriding

Method overriding supplies a subclass implementation with the same compatible signature, selected at runtime from the actual receiver object.

dynamic dispatch

Dynamic dispatch selects an overridden instance method from the runtime class of the receiver even when the reference has a superclass or interface type.

upcasting and downcasting

Upcasting treats a subclass object as a supertype safely; downcasting requires a compatible runtime object and can fail with ClassCastException.

Assigning a subtype to a supertype reference is an implicit, safe upcast:

Shape shape = new Square("blue", 4);
System.out.println(shape.area());             // 16.0

if (shape instanceof Square square) {
    System.out.println(square.area());
}

The Shape reference exposes Shape's declared contract, while overridden calls reach Square. A downcast from a general reference to a specific type requires an explicit cast. The JVM checks the actual object; incompatibility throws ClassCastException. Pattern matching with instanceof combines the test and cast. Frequent downcasts often reveal a missing polymorphic operation in the abstraction.

polymorphism

Polymorphism lets one operation work with objects supporting the required protocol regardless of their concrete class.

inner class

A non-static nested class whose instance is associated with an enclosing object and can access that object’s members.

Abstract classes

An abstract class cannot be instantiated directly. It may have state, constructors, concrete methods and abstract methods.

abstract class Shape {
    private final String colour;
    protected Shape(String colour) { this.colour = colour; }
    public String colour() { return colour; }
    public abstract double area();
}
class Square extends Shape {
    private final double side;
    Square(String colour, double side) {
        super(colour);
        this.side = side;
    }
    @Override public double area() { return side * side; }
}

A concrete subclass must implement every inherited abstract method. Otherwise, it must also be abstract. Choose an abstract class when closely related implementations share state, construction rules or reusable behaviour.

Interface contracts

An interface defines a capability. A class can implement several interfaces even though it extends only one class.

interface Printable { void print(); }
interface Scannable { void scan(); }

class MultifunctionDevice implements Printable, Scannable {
    @Override public void print() { System.out.println("Printing"); }
    @Override public void scan()  { System.out.println("Scanning"); }
}

An ordinary interface method is implicitly public abstract, so its implementation must be public. Interface fields are implicitly public static final; interfaces have no instance fields or constructors.

Modern interfaces may contain abstract methods, inherited default implementations, static utility methods and private helpers used by default or static methods. Use an interface when callers need a small contract or unrelated classes share a capability. Use an abstract class when related implementations need common state.

Overriding rules

Overriding supplies a new implementation of an inherited instance method with the same signature and a compatible return type.

class Employee {
    public Number annualBonus() { return 0; }
}
class Manager extends Employee {
    @Override
    public Integer annualBonus() { return 50_000; }
}

Integer is a covariant return because it is a subtype of Number. An override must follow these rules:

  1. The name and parameter types must match.
  2. The return type must be identical or covariant for reference types.
  3. Access cannot be reduced; a public method remains public.
  4. A broader checked exception cannot be introduced. Narrower, fewer or unchecked exceptions are permitted.
  5. An instance method cannot override a static method or vice versa.

Use @Override so the compiler verifies the intention. A final method cannot be overridden. A private method is not inherited as an accessible operation; a same-signature subclass method is a separate declaration.

Overloading versus overriding

Overloading uses the same name with different parameter lists. The compiler selects an overload from the declared argument types and available conversions.

static void inspect(Employee e) { System.out.println("employee"); }
static void inspect(Manager m)  { System.out.println("manager"); }

Employee e = new Manager();
inspect(e);                       // employee

The expression e has compile-time type Employee, so inspect(Employee) is chosen. The runtime object does not restart overload selection. Solve mixed questions in two stages: choose the overload at compilation, then apply dynamic dispatch if the selected instance method is overridden.

Runtime polymorphism

A reference has a compile-time type, while its object has a runtime type:

class Notification {
    void send() { System.out.println("General"); }
}
class EmailNotification extends Notification {
    @Override void send() { System.out.println("Email"); }
}

Notification n = new EmailNotification();
n.send();                         // Email

The reference type determines which members the compiler permits. The actual object determines which overridden instance-method body runs. This dynamic dispatch allows a method accepting Notification to work with every valid subtype. A subtype must preserve the promises of its supertype so callers can substitute it safely.

Functional interfaces

A functional interface has one abstract method after inherited declarations and relevant Object method rules are considered. It may contain several default and static methods.

@FunctionalInterface
interface PriceRule {
    double apply(double amount);
}
PriceRule discount = amount -> amount * 0.90;

The annotation is optional but asks the compiler to enforce the rule. Functional interfaces supply the target type for lambda expressions and method references.

Practical considerations

MistakeCorrection
Treating overload selection as runtime polymorphismSelect the overload from compile-time types first
Omitting public on an interface implementationDeclare the implementation public
Assuming private fields are absent from subclass objectsThe state exists but requires superclass operations
Calling static methods through objectsUse TypeName.method()
Blindly downcastingUse instanceof or improve the abstraction
Expecting an import to grant accessChoose a suitable access modifier
Extending solely for code reusePrefer composition for a non-is-a relationship

Inheritance and the is-a relationship

A class extends one direct superclass:

class Account {
    private double balance;
    Account(double opening) { balance = opening; }
    public double getBalance() { return balance; }
}

class SavingsAccount extends Account {
    private final double rate;
    SavingsAccount(double opening, double rate) {
        super(opening);
        this.rate = rate;
    }
}

SavingsAccount extends Account means that every savings account is an account. Its object contains state defined by both classes, although subclass code cannot directly access Account's private field. Java permits one direct superclass, but a chain can contain several levels. Every class except Object ultimately extends java.lang.Object.

Use inheritance only when the subtype can honour the supertype's contract. Extending an unrelated class merely to reuse code creates a false model. Composition, where one object stores and delegates to another, is then more appropriate.

Default-method conflicts

Default methods let an interface add behaviour without immediately breaking all implementations. Conflicts follow three rules:

  1. A concrete class method wins over an interface default.
  2. A more specific subinterface default wins over its parent interface.
  3. Unrelated interfaces with the same default signature require the class to override it.
interface Camera { default void start() { System.out.println("Camera"); } }
interface Player { default void start() { System.out.println("Player"); } }

class Phone implements Camera, Player {
    @Override public void start() {
        Camera.super.start();
        Player.super.start();
    }
}

InterfaceName.super.method() explicitly selects a default in the implementing class.

Local and anonymous classes

A local class is declared inside a method, constructor or block:

Runnable task(String label) {
    class LabeledTask implements Runnable {
        @Override public void run() {
            System.out.println(label);
        }
    }
    return new LabeledTask();
}

An anonymous class declares and instantiates an unnamed subclass or interface implementation in one expression:

Runnable task = new Runnable() {
    @Override public void run() {
        System.out.println("Working");
    }
};

Anonymous classes are suitable for a small one-off implementation with state or multiple methods. A lambda is usually clearer for a stateless functional-interface implementation.

Both local and anonymous classes may capture local variables only when those variables are final or effectively final. A variable is effectively final when it is assigned once and never reassigned. The restriction gives captured code a stable value even after the declaring method has returned.

Constructor chaining

Creating a subclass first initializes its superclass portion. A constructor can use super(arguments) to call a superclass constructor or this(arguments) to call another constructor in the same class. The call must be the first statement and only one can appear.

class Vehicle {
    Vehicle(String registration) {
        System.out.println("Vehicle: " + registration);
    }
}
class Bus extends Vehicle {
    Bus(String registration) {
        super(registration);
        System.out.println("Bus ready");
    }
}

new Bus("MH-01") prints the superclass message first. If a constructor contains neither call, the compiler inserts super(). Compilation fails if no accessible no-argument superclass constructor exists. Avoid calling overridable methods from constructors: dynamic dispatch may reach subclass code before subclass fields are initialized.

Worked trace

abstract class Message {
    abstract String text();
    void show() { System.out.println(text()); }
}
class Alert extends Message {
    @Override String text() { return "Warning"; }
    void show(String prefix) { System.out.println(prefix + text()); }
}
Message message = new Alert();
message.show();

The variable type Message permits the no-argument show(). That inherited method runs because Alert has not overridden its signature. Inside it, text() is dynamically dispatched to Alert.text(). The overload show(String) is irrelevant because the call has no argument. The output is Warning.

this, super and final

this refers to the current object. super selects the immediate superclass constructor or member implementation; it is not a separate object.

final means different things by context:

  • a final variable can be assigned once;
  • a final method cannot be overridden;
  • a final class cannot be extended.

A final reference cannot point to a different object, but the referenced object's mutable state may still change.

Access control

Access is checked at the use site. Imports never change it.

ModifierSame classSame packageSubclass, other packageUnrelated, other package
privateYesNoNoNo
package-privateYesYesNoNo
protectedYesYesYes, with subclass rulesNo
publicYesYesYesYes

Package-private access is created by omitting a modifier. It supports collaboration inside a package without publishing an API.

Cross-package protected access is subtle. A subclass may use the member through its inheritance context, usually through this, super or a reference of the subclass type or its subtype. It cannot treat that member as public on an arbitrary superclass object. Inside the declaring package, normal package access applies. Top-level classes can be public or package-private; members and nested classes may use all four levels.

Field and static-method hiding

Fields are resolved from the reference type. Static methods belong to classes and may be hidden, not overridden.

class Parent {
    String name = "parent";
    static void identify() { System.out.println("Parent"); }
}
class Child extends Parent {
    String name = "child";
    static void identify() { System.out.println("Child"); }
}
Parent p = new Child();
System.out.println(p.name);       // parent
Parent.identify();                // Parent

Calling static methods through the class name makes their compile-time selection clear. Avoid same-named fields because both fields continue to exist and the visible one depends on the reference expression.

Designing a sound hierarchy

Before using inheritance, ask whether the subclass is truly a specialized superclass and whether it can satisfy every public superclass contract. Prefer private fields, stable operations and shallow hierarchies. Program to the smallest useful interface.

Composition is often more flexible: the containing object delegates work to a collaborator that can be replaced independently. Inheritance is strongest when identity and substitutability matter, rather than when the only goal is saving a few lines of code.

Inherited members and private state

A subclass can use accessible superclass members. Precise wording matters:

  • public members are accessible wherever their declaring type is accessible;
  • protected members are accessible in the same package and under subclass rules elsewhere;
  • package-private members are accessible only within their package;
  • private members remain part of the object's superclass state but cannot be named in subclass code;
  • constructors are never inherited.

Private state should normally be changed through superclass operations. Making fields protected may appear convenient, but it lets subclasses bypass validation and couples them to representation details.

Continue learning

Related notes

Put this topic into timed practice

Open mock tests when you want full-exam pacing, or keep drilling in practice mode.