Object-Oriented Programming (Java)

Chapter 4
Inheritance, Composition, and Polymorphism

Bachelors Program in IT
Object-Oriented Programming (Java)   •   Bachelor

Inheritance, Composition, and Polymorphism

Introduction and Context

Object-oriented programming (OOP) provides mechanisms for representing software systems as cooperating objects. In Java, three closely related design ideas are especially important: inheritance, composition, and polymorphism. Together, they determine how classes reuse behavior, how objects collaborate, and how programs remain flexible when requirements change.

Inheritance expresses an “is-a” relationship. A SavingsAccount may be a kind of BankAccount, and a Circle may be a kind of Shape. Composition expresses a “has-a” relationship. A Car has an Engine, and an Order has a collection of OrderLine objects. Polymorphism allows a variable or method parameter declared using a general type to refer to objects of more specific types, with the appropriate overridden behavior selected at run time.

These mechanisms are powerful but not interchangeable. Poorly designed inheritance creates rigid hierarchies, exposes implementation details, and can violate client expectations. Composition often provides greater flexibility, but may require additional delegation code and more explicit object coordination. Professional Java development therefore requires not merely knowing the syntax, but evaluating the design consequences of each choice.

Learning Objectives

After completing this chapter, you should be able to:

  • Construct and interpret Java inheritance hierarchies.
  • Explain superclass and subclass relationships.
  • Override methods correctly and distinguish overriding from overloading.
  • Describe dynamic binding and run-time polymorphism.
  • Design and use abstract classes and interfaces as abstractions.
  • Apply composition and delegation to build reusable objects.
  • Evaluate substitutability and identify violations of the Liskov Substitution Principle.
  • Perform safe casts using type tests and pattern matching.
  • Compare inheritance and composition in practical software designs.
  • Analyze design choices in a realistic application scenario.

1. Inheritance as Hierarchical Reuse

Inheritance enables a class to acquire accessible members from another class. The existing class is the superclass, base class, or parent class; the new class is the subclass, derived class, or child class.

public class BankAccount {
    private final String accountNumber;
    protected double balance;

    public BankAccount(String accountNumber, double openingBalance) {
        this.accountNumber = accountNumber;
        this.balance = openingBalance;
    }

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        balance += amount;
    }

    public double getBalance() {
        return balance;
    }
}

public class SavingsAccount extends BankAccount {
    private final double interestRate;

    public SavingsAccount(String number, double balance, double interestRate) {
        super(number, balance);
        this.interestRate = interestRate;
    }

    public void addInterest() {
        balance += balance * interestRate;
    }
}

SavingsAccount inherits public and protected behavior from BankAccount. The super(...) call invokes the superclass constructor and must be the first statement in the subclass constructor. Java does not support multiple class inheritance: a class can directly extend only one class. It can, however, implement multiple interfaces.

Inheritance establishes a type relationship, not simply a code-copying mechanism. Every SavingsAccount is expected to behave as a BankAccount. The superclass should represent a meaningful generalization, and the subclass should preserve the assumptions made by users of the superclass.

Access and construction

A subclass does not automatically gain access to private fields. Private state must be accessed through public or protected methods. In general, private fields with carefully designed methods provide stronger encapsulation than protected mutable fields. The example uses protected for brevity, but production designs often keep balance private and expose protected operations such as adjustBalance.

Constructors are not inherited. A subclass must initialize its own state and arrange for superclass initialization through super(...). If no explicit constructor is written, Java supplies a no-argument constructor only when the superclass also provides an accessible no-argument constructor.

Java classes ultimately derive from Object, directly or indirectly. Methods such as toString, equals, and hashCode originate there and may be overridden.

2. Overriding, Overloading, and Dynamic Binding

A subclass overrides an inherited instance method when it supplies a method with the same signature and a compatible return type. The @Override annotation asks the compiler to verify the programmer’s intention.

public class BankAccount {
    public double monthlyCharge() {
        return 5.00;
    }
}

public class PremiumAccount extends BankAccount {
    @Override
    public double monthlyCharge() {
        return 0.00;
    }
}

Overriding is different from overloading. Overloading uses the same method name with different parameter lists and is resolved primarily at compile time. Overriding uses an inherited signature and is selected according to the object’s run-time class.

BankAccount account = new PremiumAccount();
System.out.println(account.monthlyCharge()); // 0.00

The declared type of account is BankAccount, but the object created is a PremiumAccount. Java uses dynamic binding—also called late binding or dynamic dispatch—to select the overridden instance method. This is the foundation of run-time polymorphism.

A method call has two relevant types:

  • The static type is the type known by the compiler, such as BankAccount.
  • The dynamic type is the actual class of the object at run time, such as PremiumAccount.

The static type determines which members may be referenced. The dynamic type determines which overridden implementation executes. Static methods, private methods, and fields are not dynamically dispatched in the same way as overridable instance methods. Constructors also are not polymorphic.

Calling the superclass implementation

A subclass can extend rather than replace superclass behavior by using super.methodName().

@Override
public String toString() {
    return super.toString() + ", premium=true";
}

This is useful when the superclass establishes common behavior that the subclass wants to preserve. However, overriding methods should not unexpectedly weaken validation, alter important invariants, or change documented meanings.

Covariant return types and visibility

An overriding method may return a subtype of the original return type. It may not reduce visibility: a public method cannot be overridden as protected or private. Checked exceptions in an overriding method must also obey Java’s exception rules. The compiler catches many mistakes when @Override is used consistently.

3. Abstract Classes and Interfaces

An abstract class represents an incomplete abstraction. It cannot be instantiated directly and may contain fields, constructors, concrete methods, and abstract methods.

public abstract class Shape {
    private final String color;

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

    public String getColor() {
        return color;
    }

    public abstract double area();

    public void describe() {
        System.out.println(color + " shape, area = " + area());
    }
}

public final class Circle extends Shape {
    private final double radius;

    public Circle(String color, double radius) {
        super(color);
        if (radius < 0) {
            throw new IllegalArgumentException("Radius cannot be negative");
        }
        this.radius = radius;
    }

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

An abstract method specifies an obligation for concrete subclasses. The abstract class can provide shared state and invariant-preserving operations, while subclasses provide specialized behavior.

An interface specifies a contract that unrelated classes can implement. Interfaces may contain abstract methods, default methods, static methods, and constants. They are especially useful when the important relationship is a capability rather than a taxonomic identity.

public interface Payable {
    double amountDue();
}

public class Invoice implements Payable {
    private final double total;

    public Invoice(double total) {
        this.total = total;
    }

    @Override
    public double amountDue() {
        return total;
    }
}

A class can implement several interfaces, allowing a form of multiple type inheritance without multiple implementation inheritance. Abstract classes and interfaces are not competitors in every situation: an abstract class is useful for shared implementation and state, whereas an interface is useful for a broadly applicable contract.

4. Polymorphism and Substitutability

Polymorphism means that one general type can represent objects with different specific behavior. Consider a reporting service:

public class ReportService {
    public double totalArea(Shape[] shapes) {
        double total = 0.0;
        for (Shape shape : shapes) {
            total += shape.area();
        }
        return total;
    }
}

The service does not need to know whether each element is a Circle, Rectangle, or another concrete shape. It calls the common operation area(), and dynamic binding selects the correct implementation.

This design is extensible: a new Triangle can be added without modifying ReportService, provided it fulfills the Shape abstraction. This principle is related to the Open–Closed Principle: software entities should generally be open to extension but closed to unnecessary modification.

The Liskov Substitution Principle

The Liskov Substitution Principle (LSP) states that objects of a subtype should be usable wherever objects of the supertype are expected without invalidating the program’s correctness. Substitutability is behavioral, not merely syntactic.

A subclass should generally:

  • Preserve promises made by the superclass.
  • Accept inputs that are valid for the superclass rather than arbitrarily rejecting them.
  • Return results consistent with the superclass contract.
  • Preserve important invariants and side-effect expectations.

A classic warning is modeling Square as a subtype of a mutable Rectangle. If Rectangle allows width and height to be changed independently, code expecting those operations may violate the square’s equal-side invariant. Although a square is mathematically a rectangle, the mutable software contracts may not support substitution.

The practical lesson is that domain language alone does not prove a good inheritance relationship. The behavior expected by clients is decisive.

5. Composition and Delegation

Composition builds a class from objects of other classes. It expresses ownership, collaboration, or containment. A Car may contain an Engine; an Order may contain a PaymentProcessor; a Library may contain a collection of Book objects.

public class Engine {
    public void start() {
        System.out.println("Engine started");
    }
}

public class Car {
    private final Engine engine;

    public Car(Engine engine) {
        if (engine == null) {
            throw new IllegalArgumentException("Engine is required");
        }
        this.engine = engine;
    }

    public void start() {
        engine.start();             // delegation
        System.out.println("Car ready");
    }
}

The Car delegates engine-specific work to its Engine. Delegation means that one object forwards an operation to another object rather than inheriting its implementation. This supports encapsulation because Car controls the public operations it exposes instead of automatically exposing every engine method.

Composition is often implemented with constructor injection, as in the example. Constructor injection makes required dependencies explicit and supports testing with substitutes such as fake or mock engines.

Composition provides several advantages:

  • Components can be replaced at run time or between deployments.
  • Classes can combine behavior from multiple collaborators.
  • Implementation details remain behind stable interfaces.
  • Changes in one component are less likely to force changes in a hierarchy.
  • Responsibilities can be divided into smaller, testable units.

It also has costs. Developers must define collaboration protocols, manage object lifetimes, and sometimes write forwarding methods. Excessive delegation can create long call chains or “wrapper” classes with little independent value. Good composition therefore requires clear responsibility boundaries.

6. Inheritance Versus Composition

The choice should be guided by semantics, stability, variability, and control of dependencies.

CriterionInheritanceComposition
Relationship“Is-a” specialization“Has-a” collaboration or capability
Reuse mechanismInherited implementation and contractDelegated behavior through fields
CouplingOften tight to superclass detailsUsually looser through interfaces
FlexibilityMainly fixed by class hierarchyComponents can often be replaced
Multiple behaviorsLimited by single superclassMultiple collaborators are natural
Typical riskFragile base class and LSP violationExcessive delegation or coordination
Best useStable, genuine subtype relationshipChanging policies, services, and roles

Inheritance is appropriate when the superclass abstraction is stable, the subclass truly satisfies its contract, and shared implementation is meaningful. Composition is preferable when behavior varies independently, when a class needs several services, or when future replacement is likely.

A useful design heuristic is “prefer composition over inheritance” when both choices are plausible. This is not an absolute prohibition against inheritance. Framework extension points, domain taxonomies, template methods, and abstract data types may be well served by inheritance. The heuristic warns against using inheritance merely to reuse a few methods.

7. Safe Casting and Type Tests

A cast asks the compiler to treat a reference as another compatible type. Upcasting from a subtype to a supertype is safe and usually implicit:

Circle circle = new Circle("blue", 2.0);
Shape shape = circle;

Downcasting from a general type to a subtype requires a run-time check. An invalid cast throws ClassCastException.

Shape shape = getShape();
if (shape instanceof Circle) {
    Circle circle = (Circle) shape;
    System.out.println(circle.getColor());
}

Modern Java supports pattern matching for instanceof:

if (shape instanceof Circle circle) {
    System.out.println(circle.getColor());
}

The variable circle is available only when the test succeeds. A null reference does not satisfy instanceof.

Repeated downcasting can indicate a weak abstraction. If code frequently asks whether an object is a particular subtype, the common superclass or interface may lack an operation that clients genuinely need. Prefer polymorphic methods when behavior can be expressed through the abstraction. Use casts when subtype-specific behavior is truly required and the type distinction is part of the design.

For unrelated conversions, use explicit conversion methods or constructors rather than casts. A cast changes the compile-time view of a reference; it does not transform the object.

8. An Illustrative Case: A Configurable Notification System

Background

A university application sends notifications about enrollment, examination schedules, and account activity. Initial requirements support email messages, but mobile push notifications and text messages may be added later.

Problem

A first design places all notification logic in a superclass:

public abstract class Notification {
    public abstract void send(String recipient, String message);
}

Developers then create EmailNotification, SmsNotification, and PushNotification subclasses. This appears reasonable while the channel is the primary variation. However, the application soon needs independent features: retry policies, message formatting, auditing, rate limiting, and different delivery providers. Inheritance alone would encourage a growing number of subclasses such as RetryingEmailNotification, AuditedEmailNotification, and RateLimitedSmsNotification.

Analysis

Delivery channel, retry strategy, auditing, and formatting are separate dimensions of change. Combining them in a single inheritance hierarchy produces a combinatorial design problem. It also makes it difficult to change a policy for an existing notification object.

Approach and solution

Use an interface for the stable capability and composition for variable policies:

public interface MessageSender {
    void send(String recipient, String message);
}

public interface RetryPolicy {
    void execute(Runnable operation);
}

public class NotificationService {
    private final MessageSender sender;
    private final RetryPolicy retryPolicy;

    public NotificationService(MessageSender sender, RetryPolicy retryPolicy) {
        this.sender = sender;
        this.retryPolicy = retryPolicy;
    }

    public void notifyUser(String recipient, String message) {
        retryPolicy.execute(() -> sender.send(recipient, message));
    }
}

EmailSender, SmsSender, and PushSender implement MessageSender. A testing implementation can record messages without contacting an external provider. A production implementation can use a provider selected through configuration. The service delegates delivery and retry behavior to its collaborators.

Inheritance may still be useful inside a particular provider family—for example, a shared abstract client could centralize authentication and request formatting. The overall architecture, however, does not force every independent feature into one hierarchy.

Outcome

The design supports extension with fewer modifications to existing code. Each collaborator has a focused responsibility, and tests can substitute fake senders and retry policies. The trade-off is that object construction and dependency configuration are more explicit.

Learning points

  1. A generalization should represent a stable behavioral contract.
  2. Independent axes of variation usually belong in separate composed objects.
  3. Polymorphism can be achieved through interfaces as well as abstract classes.
  4. Dependency injection improves replaceability and testability.
  5. Inheritance and composition can coexist when each is used for an appropriate purpose.

9. Best Practices and Common Errors

Use @Override on every intended override. Keep fields private unless a carefully justified design requires otherwise. Favor immutable fields and constructor validation for required state. Document the behavioral contract of public and protected methods.

Do not confuse code reuse with substitutability. A subclass that inherits useful code but cannot honor the superclass contract is a poor subtype. Avoid calling overridable methods from constructors because the subclass portion of the object may not yet be initialized. Be cautious when overriding equals and hashCode; objects considered equal must produce the same hash code, and equality should follow the documented symmetry and consistency requirements.

Prefer interfaces at service boundaries when multiple implementations are expected. Keep abstractions small and cohesive. Avoid “god” superclasses that accumulate unrelated utilities. Use final classes or final methods when extension would be unsafe or semantically unsupported.

When casting is necessary, test the type first and consider whether a polymorphic operation would remove the need for the cast. Treat unchecked casts involving generics with particular caution because they can defer errors until later execution.

10. Professional, Ethical, and Contemporary Considerations

Object-oriented design affects maintainability, reliability, and the cost of correcting defects. A poorly chosen hierarchy can make security fixes difficult to apply consistently. Subclasses that override validation or authorization methods may accidentally bypass controls. Sensitive operations should therefore be centralized, explicitly tested, and protected against unintended extension.

In enterprise Java, dependency injection frameworks, service interfaces, records, sealed classes, and pattern matching provide modern alternatives or complements to traditional inheritance. Sealed classes restrict which classes may extend a superclass or implement an interface, making a closed hierarchy easier to reason about. Records provide concise immutable data carriers, although they should not be treated as a replacement for domain behavior. Pattern matching can make controlled type analysis clearer, but it should not become an excuse for abandoning useful abstractions.

Automated code-generation and AI-assisted programming tools can suggest inheritance structures or generate boilerplate delegation. Their output still requires human review for contract correctness, security, maintainability, and licensing or privacy concerns. A generated hierarchy may compile while violating substitutability or exposing sensitive state. Professional accountability remains with the development team.

Chapter Summary

Inheritance models a meaningful subtype relationship and enables shared implementation, but it creates coupling to a superclass contract. Abstract classes provide partial implementations and obligations; interfaces define capabilities and allow a class to participate in multiple contracts. Method overriding, @Override, and dynamic binding enable run-time polymorphism: the static type controls accessible operations, while the dynamic type selects the overridden implementation.

Composition models collaboration and “has-a” relationships. Delegation lets an object forward responsibilities to a collaborator while preserving encapsulation and replaceability. The Liskov Substitution Principle requires subclasses to preserve the behavioral expectations of their supertypes. Safe casting uses instanceof or pattern matching, although frequent downcasts often signal a missing abstraction.

Inheritance is most suitable for stable, genuine specialization. Composition is generally more flexible when behavior varies independently or dependencies must be replaceable. Effective Java design uses both, evaluates their trade-offs, and emphasizes clear contracts, cohesion, testability, and maintainability.

Key Terms

Inheritance: A mechanism by which a subclass acquires accessible behavior and type relationships from a superclass.

Superclass: A more general class extended by another class.

Subclass: A class that extends a superclass and may add or override behavior.

Overriding: Replacing an inherited instance method with a compatible implementation using the same signature.

Overloading: Defining methods with the same name but different parameter lists.

Dynamic binding: Run-time selection of an overridden instance method based on the object’s actual class.

Static type: The compile-time type of a reference or expression.

Dynamic type: The actual run-time class of an object.

Abstract class: A non-instantiable class that may contain abstract and concrete members.

Abstract method: A method declaration without an implementation that concrete subclasses must implement.

Interface: A contract that classes can implement, potentially alongside other interfaces.

Polymorphism: The ability to use a general type to represent objects with specialized behavior.

Substitutability: The ability to use a subtype wherever its supertype is expected without breaking correctness.

Liskov Substitution Principle: The principle that subtypes must preserve the behavioral contract of their supertypes.

Composition: Building an object from collaborating objects held as fields.

Delegation: Forwarding responsibility for an operation to another object.

Upcasting: Treating a subtype reference as a supertype reference.

Downcasting: Treating a general reference as a more specific subtype after a run-time check.

Dependency injection: Supplying an object’s dependencies from outside rather than constructing them internally.

Review Questions

Short Answer Questions

  1. What is the difference between a superclass and a subclass?
  2. How does method overriding differ from method overloading?
  3. What is dynamic binding in Java?
  4. Why can an abstract class not be instantiated directly?
  5. What does delegation mean in a composed design?

Long Answer Questions

  1. Explain how static type and dynamic type jointly determine the behavior of a polymorphic Java method call.
  2. Compare abstract classes and interfaces, including their appropriate uses and limitations.
  3. Explain the Liskov Substitution Principle and illustrate a possible violation.
  4. Evaluate the advantages and disadvantages of inheritance and composition.
  5. Describe how constructor injection supports composition, testing, and maintainability.

Analytical / Application Questions

  1. A ReadOnlyAccount extends BankAccount, but it overrides deposit by throwing an exception. Analyze whether this is a valid subtype and propose an alternative design.
  2. Design a Java structure for a document editor that supports PDF, HTML, and plain-text export, while allowing new export formats to be added. Justify whether you use inheritance, composition, interfaces, or a combination.
  3. A program contains many statements of the form if (x instanceof SpecificType). Analyze what this may indicate about the design and recommend two possible improvements.

Multiple Choice Questions with Answers

Question 1

Which statement best describes method overriding in Java?

A. A subclass defines a method with the same signature as an inherited method

B. A class defines two methods with different parameter lists

C. A superclass privately copies a method from its subclass

D. A method is converted from static to instance form at run time

Correct Answer: A. A subclass defines a method with the same signature as an inherited method

Question 2

What determines which overridden instance method executes?

A. The variable name

B. The object’s dynamic type

C. The order of import statements

D. The accessibility of the constructor

Correct Answer: B. The object’s dynamic type

Question 3

Which relationship is most directly represented by composition?

A. A Circle is a Shape

B. A SavingsAccount is an Account

C. A Car has an Engine

D. An Invoice is Payable

Correct Answer: C. A Car has an Engine

Question 4

What is a primary purpose of an abstract method?

A. To prevent all subclasses from compiling

B. To require concrete subclasses to provide an implementation

C. To make a method execute before every constructor

D. To allow unrelated classes to share private fields

Correct Answer: B. To require concrete subclasses to provide an implementation

Question 5

Which statement about Java downcasting is correct?

A. It always creates a new object

B. It is safe without a run-time check

C. An invalid downcast can throw ClassCastException

D. It changes the object’s actual class

Correct Answer: C. An invalid downcast can throw ClassCastException

Question 6

Which design most clearly uses delegation?

A. A subclass inherits a protected method

B. A car forwards engine-starting work to an Engine object

C. A method is overloaded with another parameter list

D. A constructor calls Object directly

Correct Answer: B. A car forwards engine-starting work to an Engine object

Question 7

The Liskov Substitution Principle primarily concerns whether:

A. A class has exactly one constructor

B. A subtype preserves the behavioral expectations of its supertype

C. All fields are declared public

D. Every interface has a default method

Correct Answer: B. A subtype preserves the behavioral expectations of its supertype

Question 8

Which annotation asks the compiler to verify that a method is intended to override an inherited method?

A. @Inherited

B. @Override

C. @Checked

D. @Dynamic

Correct Answer: B. @Override

Question 9

Why is composition often preferred when several behaviors vary independently?

A. It permits combining replaceable collaborators without a large subclass hierarchy

B. It prevents all run-time polymorphism

C. It automatically makes every field public

D. It eliminates the need for interfaces

Correct Answer: A. It permits combining replaceable collaborators without a large subclass hierarchy

Question 10

What is the safest typical approach before downcasting a Shape reference to Circle?

A. Compare the variable names

B. Use instanceof or an equivalent type check

C. Call the superclass constructor again

D. Convert the reference to String

Correct Answer: B. Use instanceof or an equivalent type check

Object-Oriented Programming (Java)
Chapter 4 of 15