Attic
OOPs

Core Principles & Patterns

Comprehensive exploration of Encapsulation, Abstraction, Inheritance, and Polymorphism in Java

Object-Oriented Programming (OOP) structures software architecture around self-contained entities (objects) that bundle state with behavior. This guide details the four foundational pillars—Encapsulation, Abstraction, Inheritance, and Polymorphism—alongside JVM runtime dispatch mechanics and clean architecture guidelines.


The Four Foundational Pillars

These four core principles work cohesively to maximize reusability, safeguard data integrity, and decouple implementation details from public contracts.


Encapsulation vs Abstraction

While often conflated, Encapsulation focuses on data protection (how an object hides its internal representation), whereas Abstraction focuses on interface simplification (what capabilities an object reveals to the outside world):

DimensionEncapsulationAbstraction
Primary GoalProtect internal state from arbitrary mutationReduce cognitive complexity by hiding implementation details
Java MechanismAccess modifiers (private, protected), getters, defensive copiesinterface, abstract class
FocusHow an object fulfills its responsibilities internallyWhat contract an object presents to consumers
Key AdvantageMaintain invariants and enforce validation constraintsFreedom to refactor underlying algorithms without breaking clients

Modern Immutability with Records: In Java 16+, prefer record declarations for transparent data carriers. Records automatically enforce private final fields, accessor methods, and robust equals/hashCode contracts.


Polymorphism: Compile-Time vs Runtime

Polymorphism allows objects of disparate types to be addressed through a common interface, with concrete behaviors resolved either statically by the compiler or dynamically by the JVM runtime.

Static polymorphism resolves which method signature to invoke at compile time based on parameter types and arity:

LoggerOverloading.java
public class Logger {
    public void log(String message) {
        System.out.println("[INFO] " + message);
    }

    public void log(String message, Throwable cause) {
        System.err.println("[ERROR] " + message + ": " + cause.getMessage());
    }

    public void log(String format, Object... args) {
        System.out.printf(format + "%n", args);
    }
}

Inheritance Pitfalls & Composition

Inheritance creates a strong coupling relationship between parent and child classes. Modern software engineering prioritizes object composition over class inheritance.

The Fragile Base Class Problem: Changes to a parent class's internal implementation can inadvertently break child class invariants. When behavior reuse is needed, prefer injecting dependencies via constructor composition rather than extends.

Diamond Problem & Interface Clashes: While Java forbids multiple class inheritance to avoid ambiguous member resolution, implementing two interfaces that provide conflicting default method signatures requires explicit override disambiguation (InterfaceA.super.doSomething()).


Designing Extensible Class Hierarchies

Follow this checklist when modeling domain hierarchies to satisfy the Liskov Substitution Principle (LSP):

Define Behavior with Narrow Interfaces

Adhere to the Interface Segregation Principle (ISP). Keep interfaces focused on a single responsibility rather than creating monolithic contracts.

Use Abstract Classes for Shared Invariants Only

Reserve abstract classes for template method patterns where common pre-flight validations, resource hooks, or logging must be enforced before child execution.

Mark Classes Final by Default

Design explicitly for inheritance or forbid it. Classes that are not designed with template hooks should be marked final or sealed (sealed ... permits).


Frequently Asked Questions


On this page