Inheritance, Interfaces, and PolymorphismIntroduction and ContextObject-oriented programming (OOP) organizes software around objects that combine state and behavior. Java supports OOP through classes, objects, encapsulation, inheritance, interfaces, abstraction, and polymorphism. These mechanisms allow developers to model related concepts and build systems that can change without requiring extensive rewriting. Inheritance is useful when one type is genuinely a specialized form of another. Interfaces define capabilities or contracts that unrelated classes may implement. Abstract classes provide shared structure for a family of related classes. Polymorphism allows code to work with a general type while executing behavior supplied by a specific object. Composition, in contrast, constructs a class from other objects and is often preferable when a relationship represents “has-a” rather than “is-a.” This chapter also introduces sealed classes, which allow developers to restrict which classes may extend or implement a type. Finally, these language features are examined through SOLID-oriented design principles, helping programmers produce extensible and maintainable applications rather than merely using inheritance mechanically. Learning ObjectivesBy the end of this chapter, you should be able to: - distinguish inheritance, composition, abstraction, and interface-based design;
- define and use abstract classes and interfaces;
- override methods correctly and explain dynamic method dispatch;
- apply compile-time and run-time polymorphism;
- use sealed classes and interfaces to control a type hierarchy;
- select inheritance or composition using sound design reasoning;
- apply relevant SOLID principles to Java designs;
- identify limitations, maintenance risks, and testing implications of inheritance-based systems.
1. Inheritance as a Type RelationshipInheritance enables a class to acquire accessible fields and methods from a superclass. The syntax is: class SavingsAccount extends Account {
// additional state and behavior
}SavingsAccount is the subclass, or derived class; Account is the superclass, or base class. A subclass may add members, override inherited methods, and call superclass behavior using super.
A subclass should satisfy the substitutability expectation: wherever an Account is required, a SavingsAccount should behave as a valid account. This is more demanding than merely sharing code. If the subclass violates assumptions made about the superclass, the hierarchy is poorly designed. Java supports single class inheritance: a class can directly extend only one class. This avoids ambiguity that could occur if two parent classes supplied conflicting implementations. Java does, however, support multiple interface implementation. class Account {
private final String number;
public Account(String number) {
this.number = number;
}
public String getNumber() {
return number;
}
public void describe() {
System.out.println("General account");
}
}
class SavingsAccount extends Account {
private final double interestRate;
public SavingsAccount(String number, double interestRate) {
super(number);
this.interestRate = interestRate;
}
@Override
public void describe() {
System.out.println("Savings account at " + interestRate + "% interest");
}
}The super(...) call invokes the superclass constructor and must be the first statement in a subclass constructor. If a superclass has no accessible no-argument constructor, the subclass must explicitly invoke an available constructor. Visibility and inheritanceprivate members belong to the declaring class and are not directly accessible in subclasses. protected members are accessible within the same package and in subclasses, but excessive use of protected can expose implementation details. Public methods are inherited subject to overriding rules. Package-private members are accessible only within the same package.
A class declared final cannot be extended. A method declared final cannot be overridden. These restrictions can protect invariants, but they should be applied deliberately. 2. Composition: Building Objects from ObjectsComposition means that a class contains another object and delegates part of its work to that object. For example, a Car has an Engine; it is not a specialized kind of engine. class Engine {
public void start() {
System.out.println("Engine started");
}
}
class Car {
private final Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
System.out.println("Car ready");
}
}Composition provides several advantages: - components can be replaced independently;
- dependencies can be supplied through constructors;
- classes are less tightly coupled to a parent implementation;
- behavior can be combined without creating a large hierarchy;
- unit testing is often easier because collaborators can be substituted.
A practical design rule is: prefer composition over inheritance when the relationship is primarily “has-a,” when behavior may change independently, or when subclassing would expose methods that do not make sense for the specialized type. Inheritance remains appropriate when there is a stable conceptual hierarchy and the subtype truly conforms to the parent contract. Composition and inheritance are not mutually exclusive; a class may extend an abstract domain type while composing services, repositories, or strategies. 3. Abstract Classes and AbstractionAn abstract class represents an incomplete concept. It cannot be instantiated directly and may contain fields, constructors, concrete methods, and abstract methods. abstract class Payment {
private final double amount;
protected Payment(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
this.amount = amount;
}
public double getAmount() {
return amount;
}
public abstract void process();
public void printReceipt() {
System.out.println("Receipt amount: " + amount);
}
}
class CardPayment extends Payment {
public CardPayment(double amount) {
super(amount);
}
@Override
public void process() {
System.out.println("Processing card payment");
}
}An abstract method has no implementation in the abstract class. A non-abstract subclass must implement every inherited abstract method unless it is itself abstract. Abstract classes are useful when several related classes share state, validation, or common workflow while differing in one or more operations. A common pattern is the template method pattern: a superclass defines the overall algorithm and delegates selected steps to subclasses. abstract class ReportGenerator {
public final void generate() {
fetchData();
formatData();
export();
}
protected abstract void fetchData();
protected abstract void formatData();
private void export() {
System.out.println("Exporting report");
}
}The final method prevents subclasses from changing the essential sequence. This can protect business rules, although overly rigid templates may reduce flexibility. 4. Interfaces as Contracts and CapabilitiesAn interface specifies operations that implementing classes agree to provide. It is a reference type, but it is not instantiated directly. A class can implement several interfaces. interface Payable {
void pay(double amount);
}
interface Auditable {
String auditRecord();
}
class Invoice implements Payable, Auditable {
@Override
public void pay(double amount) {
System.out.println("Invoice paid: " + amount);
}
@Override
public String auditRecord() {
return "Invoice payment recorded";
}
}Interfaces are especially useful for programming to an abstraction. A method can accept Payable rather than a particular payment class: class CheckoutService {
public void complete(Payable payment, double amount) {
payment.pay(amount);
}
}Any suitable implementation can be supplied without changing CheckoutService. Modern Java interfaces may contain: - abstract methods, which implementing classes must provide;
default methods, which provide inherited behavior;static methods, called through the interface name;private methods, used internally by default methods;- constants, which are implicitly
public, static, and final.
interface Describable {
String name();
default String description() {
return "Item: " + name();
}
}Default methods support API evolution, but adding substantial behavior to interfaces can create surprising dependencies. If two interfaces provide conflicting default methods, the implementing class must resolve the conflict explicitly. interface A {
default void show() { System.out.println("A"); }
}
interface B {
default void show() { System.out.println("B"); }
}
class C implements A, B {
@Override
public void show() {
A.super.show();
}
}An interface can extend multiple interfaces, while a class can extend one class and implement multiple interfaces. 5. Method Overriding and OverloadingOverriding occurs when a subclass supplies a new implementation of an inherited instance method with the same signature. The return type must be identical or covariant, meaning the overriding method may return a subtype of the original return type. The overriding method cannot reduce visibility or broaden checked exceptions beyond what the parent method permits. Use @Override because the compiler then detects spelling and signature errors. Overloading is different: methods have the same name but different parameter lists. Overloading is resolved primarily at compile time; overriding is resolved at run time. class Printer {
public void print(String value) { }
public void print(int value) { }
}Constructors are not inherited and cannot be overridden. Static methods are hidden rather than overridden. Private methods are not overridden because they are not inherited by subclasses. Fields are also hidden rather than polymorphically overridden, so fields should generally be private and accessed through methods. 6. Polymorphism and Dynamic DispatchPolymorphism means “many forms.” In Java, a superclass or interface reference can refer to an object of a compatible subclass or implementation. Payment payment = new CardPayment(250.00);
payment.process(); The reference type determines which members are available at compile time, but the actual object type determines which overridden instance method executes at run time. This is dynamic method dispatch. class CashPayment extends Payment {
public CashPayment(double amount) {
super(amount);
}
@Override
public void process() {
System.out.println("Processing cash payment");
}
}
List<Payment> payments = List.of(
new CardPayment(100),
new CashPayment(50)
);
for (Payment p : payments) {
p.process();
}The loop is open to new payment types. A new subclass can be added without changing the loop, provided it respects the Payment contract. Upcasting, assigning a subtype to a supertype reference, is safe and implicit. Downcasting is potentially unsafe and should be avoided unless the type relationship is known. The instanceof operator can test compatibility, and modern Java supports pattern matching: if (payment instanceof CardPayment card) {
System.out.println("Card payment amount: " + card.getAmount());
}Downcasting may indicate that the abstraction is too weak. Prefer adding an operation to a suitable interface or using polymorphic behavior instead of repeatedly testing concrete types. 7. Sealed Classes and Controlled HierarchiesA sealed class or interface restricts which types may directly extend or implement it. This feature was finalized in Java 17. public sealed interface Result
permits Success, Failure {
}
final class Success implements Result {
}
final class Failure implements Result {
}Every permitted direct subtype must be declared final, sealed, or non-sealed. sealed class Document permits InvoiceDocument, ReportDocument { }
final class InvoiceDocument extends Document { }
non-sealed class ReportDocument extends Document { }A non-sealed subtype reopens extension beneath that branch. Sealed types are useful when the domain has a deliberately closed set of alternatives, such as a compiler’s expression categories, a fixed protocol state model, or known result variants. Sealed types work particularly well with pattern matching and exhaustive switch expressions. The exact syntax depends on the Java version, so projects should compile against the documented language level. A sealed hierarchy improves reasoning and may allow the compiler to detect missing cases, but it reduces third-party extensibility. It should not be used simply because a hierarchy currently has two classes. 8. SOLID-Oriented DesignSOLID is a group of design principles. They are guidelines, not laws, and must be balanced against simplicity and project constraints. Single Responsibility PrincipleA class should have one coherent responsibility and one primary reason to change. A Report entity should not also send email, format PDF files, and store itself in a database. Separating these concerns reduces change impact. Open–Closed PrincipleSoftware entities should be open for extension but closed for modification. Polymorphism supports this principle: new implementations can be added behind an interface without changing stable client code. The principle does not mean that existing code must never change; it means that variation should be designed into appropriate extension points. Liskov Substitution PrincipleSubtypes must preserve the behavioral expectations of their base types. A classic violation is making a subtype reject operations that the parent contract promises to support. A subtype may strengthen internal implementation, but it should not unexpectedly weaken valid client assumptions. Interface Segregation PrincipleClients should not be forced to depend on methods they do not use. Several focused interfaces are often better than one large interface. interface Printable {
void print();
}
interface Scannable {
void scan();
}A simple printer may implement only Printable, while a multifunction device can implement both. Dependency Inversion PrincipleHigh-level policy should depend on abstractions, not concrete low-level details. Constructor injection makes dependencies visible and replaceable. interface NotificationSender {
void send(String message);
}
class AlertService {
private final NotificationSender sender;
public AlertService(NotificationSender sender) {
this.sender = sender;
}
public void alert(String message) {
sender.send(message);
}
}Testing AlertService now requires only a small test double implementing NotificationSender. 9. Worked Application: Extensible Delivery PricingConsider a delivery platform that initially supports standard and express delivery. A poorly designed implementation might use a large method containing conditional logic: if (type.equals("standard")) { ... }
else if (type.equals("express")) { ... }Each new delivery option requires modifying the method and risks affecting existing behavior. An interface-based design separates the pricing policy. interface DeliveryStrategy {
double calculateCost(double distanceKm, double weightKg);
}
final class StandardDelivery implements DeliveryStrategy {
@Override
public double calculateCost(double distanceKm, double weightKg) {
return 5.00 + 0.50 * distanceKm + 0.20 * weightKg;
}
}
final class ExpressDelivery implements DeliveryStrategy {
@Override
public double calculateCost(double distanceKm, double weightKg) {
return 10.00 + 0.80 * distanceKm + 0.35 * weightKg;
}
}
class DeliveryOrder {
private final DeliveryStrategy strategy;
public DeliveryOrder(DeliveryStrategy strategy) {
this.strategy = strategy;
}
public double cost(double distanceKm, double weightKg) {
if (distanceKm < 0 || weightKg < 0) {
throw new IllegalArgumentException("Values cannot be negative");
}
return strategy.calculateCost(distanceKm, weightKg);
}
}For a standard delivery of 20 km and 3 kg: cost = 5.00 + (0.50 × 20) + (0.20 × 3) = 15.60 The variables are distanceKm, measured in kilometres, and weightKg, measured in kilograms. The coefficients are illustrative pricing rates, not universal industry values. The formula applies only when the stated pricing policy is used and inputs are non-negative. An OvernightDelivery class can be added without changing DeliveryOrder. This illustrates polymorphism, dependency inversion, the open–closed principle, and composition: DeliveryOrder has a strategy rather than being a subclass of one. Case study: illustrative university library systemBackground. A university library lends books, journals, and digital media. Each item has a title and identifier, but lending rules differ. Problem. A single LibraryItem class contains conditionals for every media type. Adding a new media type makes the class longer and increases regression risk. Analysis. Common identity and catalogue behavior can be placed in an abstract class. Lending behavior is expressed through an interface because some items may be reference-only and others may be renewable. abstract class LibraryItem {
private final String id;
private final String title;
protected LibraryItem(String id, String title) {
this.id = id;
this.title = title;
}
public String getId() { return id; }
public String getTitle() { return title; }
}
interface Loanable {
int loanDays();
default boolean renewable() { return true; }
}
final class Book extends LibraryItem implements Loanable {
public Book(String id, String title) {
super(id, title);
}
@Override
public int loanDays() { return 21; }
}
final class ReferenceJournal extends LibraryItem {
public ReferenceJournal(String id, String title) {
super(id, title);
}
}Approach and outcome. The circulation service accepts Loanable objects rather than testing whether an item is a book or journal. Reference journals cannot accidentally be loaned because they do not implement Loanable. New loanable types can be added independently. Learning points. The design uses an abstract class for shared identity, an interface for a capability, composition for service collaboration, and polymorphism for variable loan periods. It also demonstrates that not every related object belongs in the same inheritance branch. 10. Testing, Maintenance, and Design RisksInheritance can reduce duplication, but it also creates coupling. Changes to a superclass may affect many subclasses. A fragile base class is one whose internal changes unexpectedly alter subclass behavior. Protected mutable state, undocumented override requirements, and constructors that call overridable methods are common hazards. Testing should include: - unit tests for each concrete implementation;
- contract tests for behavior promised by an interface or abstract class;
- substitution tests that use every subtype through the parent abstraction;
- boundary tests for invalid values and empty collections;
- integration tests where composed collaborators interact.
Prefer immutable fields, explicit constructors, small interfaces, and clear contracts. Document whether methods may return null, throw exceptions, mutate state, or be called during construction. Avoid deep inheritance trees unless each level adds a meaningful abstraction. Ethical and governance considerationsSoftware hierarchies can encode real-world classifications. Developers should question whether categories are accurate, inclusive, and appropriate for the application. A fixed sealed hierarchy may unintentionally exclude valid future cases. In systems affecting people—such as eligibility, education, employment, or finance—polymorphic rules should be auditable, tested for discriminatory effects, and separated from user data where possible. AI-generated code should be reviewed for incorrect assumptions, insecure access control, licensing concerns, and inappropriate class relationships; generated code is not a substitute for design review or testing. 11. Best Practices- Use inheritance for genuine substitutable “is-a” relationships.
- Prefer composition when behavior is a replaceable capability or service.
- Program to interfaces where variation is expected.
- Keep interfaces cohesive and small.
- Use
@Override on every intended override. - Keep fields private and protect invariants through methods.
- Favor immutable state where practical.
- Avoid downcasting and type-based conditional chains.
- Use sealed types for intentionally closed, well-defined alternatives.
- Document contracts, exceptions, mutability, and extension requirements.
- Test implementations through their abstractions.
- Apply SOLID principles pragmatically rather than creating unnecessary layers.
Contemporary Java PerspectiveRecent Java releases combine traditional OOP with records, pattern matching, sealed types, and improved switch expressions. Records are useful for immutable data carriers, but they do not replace domain classes when behavior, validation, or identity rules are complex. Sealed hierarchies support controlled polymorphism, while interfaces remain valuable for open extension points. Effective Java design therefore combines language features according to the stability of the domain: use open abstractions where external extension is expected and closed hierarchies where completeness and control matter. Chapter SummaryInheritance models specialization, whereas composition models collaboration or ownership. Abstract classes provide shared state and partial implementation for related types. Interfaces define contracts and capabilities and permit a class to implement multiple abstractions. Overriding changes inherited instance behavior, and dynamic dispatch enables a general reference to invoke the specific object’s implementation. Overloading is a separate compile-time mechanism. Polymorphism reduces conditional logic and supports extensible designs. Sealed classes and interfaces restrict a hierarchy to known alternatives, improving control and exhaustiveness at the cost of extensibility. SOLID principles help developers decide where abstractions belong, how responsibilities should be separated, and how dependencies should be supplied. Good Java design does not maximize inheritance; it selects inheritance, composition, abstract classes, interfaces, and sealed types according to domain meaning, change patterns, testability, and maintenance risk. Key TermsInheritance: Mechanism by which a class derives accessible behavior from a superclass. Superclass: The class being extended. Subclass: A class that extends another class. Composition: Building a class from collaborating object instances. Abstraction: Representing essential behavior while hiding implementation detail. Abstract class: A non-instantiable class that may contain abstract and concrete members. Interface: A contract that classes or other interfaces can implement or extend. Capability: A behavior expressed independently through an interface. Overriding: Replacing inherited instance-method behavior in a subclass. Overloading: Defining methods with the same name but different parameter lists. Polymorphism: Using one general type to represent objects of multiple specific types. Dynamic dispatch: Run-time selection of an overridden instance method. Upcasting: Treating a subtype object as a compatible supertype. Downcasting: Converting a reference to a more specific type. Sealed class: A class that restricts its permitted direct subclasses. Default method: An interface method with an implementation. Substitutability: The expectation that a subtype can validly replace its parent type. Dependency injection: Supplying an object’s collaborators from outside the object. SOLID: A set of principles for maintainable object-oriented design. Review QuestionsShort Answer Questions- What is the difference between inheritance and composition?
- Why should the
@Override annotation be used? - What is dynamic method dispatch?
- What members can an abstract class contain?
- What restriction does a sealed interface impose?
Long Answer Questions- Explain how interfaces support programming to abstractions, using a Java example.
- Compare abstract classes and interfaces in terms of state, implementation, inheritance, and use cases.
- Explain the difference between method overriding and method overloading.
- Discuss the benefits and risks of deep inheritance hierarchies.
- Explain how the Open–Closed, Liskov Substitution, and Dependency Inversion principles relate to polymorphism.
Analytical / Application Questions- Design a Java model for notifications that supports email, SMS, and push delivery. Decide which relationships should use interfaces, composition, or inheritance, and justify your choices.
- A subclass throws
UnsupportedOperationException for a method that its superclass promises every object can use. Analyze the design using the Liskov Substitution Principle and propose a redesign. - A compiler must process exactly four expression categories, but external plugins must not add new categories. Explain how a sealed hierarchy and polymorphic processing could model this requirement.
Multiple Choice Questions with AnswersQuestion 1Which relationship is best represented by composition? A. A SavingsAccount is an Account B. A Car has an Engine C. A Circle is a Shape D. A Manager is an Employee Correct Answer: B. A Car has an Engine Question 2What does dynamic method dispatch select at run time? A. The constructor with the fewest parameters B. The field declared in the reference type C. The overridden instance method belonging to the actual object D. The private method in the superclass Correct Answer: C. The overridden instance method belonging to the actual object Question 3Which statement about Java class inheritance is correct? A. A class can extend multiple classes B. A class can extend one class and implement multiple interfaces C. An interface can extend only one interface D. A final class must be abstract Correct Answer: B. A class can extend one class and implement multiple interfaces Question 4What is the primary purpose of an abstract method? A. To require a concrete subclass to provide an implementation B. To prevent a class from being inherited C. To create a static utility method D. To hide a field from all subclasses Correct Answer: A. To require a concrete subclass to provide an implementation Question 5Which annotation helps the compiler verify that a method overrides an inherited method? A. @Inherited B. @Override C. @FunctionalInterface D. @Deprecated Correct Answer: B. @Override Question 6A class implements two interfaces that define conflicting default methods with the same signature. What must the class do? A. Ignore both methods B. Become abstract automatically C. Override the method to resolve the conflict D. Convert both interfaces into abstract classes Correct Answer: C. Override the method to resolve the conflict Question 7For the standard delivery formula cost = 5.00 + 0.50 × distanceKm + 0.20 × weightKg, what is the cost for 20 km and 3 kg? A. 15.60 B. 16.60 C. 14.60 D. 20.60 Correct Answer: A. 15.60 Question 8Which SOLID principle states that clients should not be forced to depend on methods they do not use? A. Single Responsibility Principle B. Open–Closed Principle C. Interface Segregation Principle D. Dependency Inversion Principle Correct Answer: C. Interface Segregation Principle Question 9Which declaration restricts the direct subclasses of a class or interface? A. final B. sealed C. static D. transient Correct Answer: B. sealed Question 10Why is constructor injection useful in the AlertService example? A. It prevents all polymorphism B. It makes the dependency explicit and replaceable C. It allows a class to extend multiple classes D. It automatically makes every method static Correct Answer: B. It makes the dependency explicit and replaceable
|