TechStackTutor Logo
HOMEBLOGKIDSABOUT USCONTACT USBOOK DEMO
Java

Java OOP: Classes, Interfaces & Design Patterns

SOLID principles, encapsulation, inheritance, and the most-used design patterns

July 28, 2025

9 min read

Object-Oriented Programming is not just syntax — it's a way of thinking about systems. Mastering OOP means knowing when to use inheritance vs composition, when an interface beats an abstract class, and which design patterns solve which recurring problems. This guide covers it all with practical Java code.

1. The Four Pillars of OOP

java
// ENCAPSULATION — hide internal state, expose controlled interface public class BankAccount { private double balance; // private — can't be touched directly public void deposit(double amount) { if (amount <= 0) throw new IllegalArgumentException("Amount must be positive"); this.balance += amount; } public double getBalance() { return balance; } } // INHERITANCE — child class extends parent's behaviour public class SavingsAccount extends BankAccount { private double interestRate; public void applyInterest() { deposit(getBalance() * interestRate); } } // POLYMORPHISM — one interface, many implementations List<BankAccount> accounts = List.of(new BankAccount(), new SavingsAccount()); accounts.forEach(acc -> System.out.println(acc.getBalance())); // works for both // ABSTRACTION — expose only what callers need public abstract class Shape { public abstract double area(); // implementation details hidden public void printArea() { System.out.println("Area: " + area()); } }

2. Interfaces vs Abstract Classes

java
// Interface — defines a CONTRACT (what, not how) // Use when: unrelated classes share the same behaviour public interface Printable { void print(); // abstract by default default void printTwice() { // default implementation print(); print(); } } public interface Serializable { String serialize(); } // A class can implement many interfaces public class Report implements Printable, Serializable { public void print() { System.out.println("Report content"); } public String serialize() { return "{"type":"report"}"; } } // Abstract class — partial implementation + shared state // Use when: related classes share code AND state public abstract class Vehicle { protected String brand; // shared state public Vehicle(String brand) { this.brand = brand; } public abstract void start(); // subclass must implement public void displayBrand() { // shared implementation System.out.println("Brand: " + brand); } }

Rule of thumb: prefer interfaces for capability contracts (Comparable, Iterable, Runnable). Use abstract classes when subclasses need shared state or a partial implementation. A class can implement multiple interfaces but only extend one abstract class.

3. SOLID Principles

java
// S — Single Responsibility: one class, one job // BAD: UserService does auth + email + database // GOOD: class UserRepository { /* only data access */ } class EmailService { /* only sending emails */ } class AuthService { /* only authentication */ } // O — Open/Closed: open for extension, closed for modification interface DiscountStrategy { double apply(double price); } class SeasonalDiscount implements DiscountStrategy { public double apply(double price) { return price * 0.9; } } class LoyaltyDiscount implements DiscountStrategy { public double apply(double price) { return price * 0.85; } } // Adding a new discount = new class, no changes to existing code // L — Liskov Substitution: subclass must be usable wherever parent is used // BAD: Square extends Rectangle but breaks setWidth/setHeight assumptions // GOOD: separate Shape hierarchy with only shared contracts // I — Interface Segregation: prefer small specific interfaces // BAD: one fat interface with 10 methods // GOOD: interface Readable { String read(); } interface Writable { void write(String data); } interface ReadWrite extends Readable, Writable {} // D — Dependency Inversion: depend on abstractions, not concrete classes // BAD: class directly instantiates EmailService // GOOD: inject it class OrderService { private final NotificationService notifier; // interface, not concrete class public OrderService(NotificationService notifier) { this.notifier = notifier; // injected — easy to swap, easy to test } }

4. Design Pattern: Singleton

Guarantees a class has exactly one instance — useful for configuration, connection pools, or caches:

java
public class DatabaseConnection { private static volatile DatabaseConnection instance; private final Connection connection; private DatabaseConnection() { // expensive initialization this.connection = DriverManager.getConnection("jdbc:postgresql://..."); } public static DatabaseConnection getInstance() { if (instance == null) { synchronized (DatabaseConnection.class) { if (instance == null) { // double-checked locking instance = new DatabaseConnection(); } } } return instance; } public Connection getConnection() { return connection; } } // Usage: DatabaseConnection.getInstance().getConnection()

5. Design Pattern: Builder

Constructs complex objects step by step — eliminates telescoping constructors:

java
public class User { private final String name; // required private final String email; // required private final String phone; // optional private final String address; // optional private User(Builder builder) { this.name = builder.name; this.email = builder.email; this.phone = builder.phone; this.address = builder.address; } public static class Builder { private final String name; private final String email; private String phone; private String address; public Builder(String name, String email) { this.name = name; this.email = email; } public Builder phone(String phone) { this.phone = phone; return this; } public Builder address(String address) { this.address = address; return this; } public User build() { return new User(this); } } } // Usage — readable and flexible User user = new User.Builder("Alice", "alice@example.com") .phone("+1-555-1234") .address("123 Main St") .build(); // Note: Lombok's @Builder generates all this automatically

6. Design Pattern: Strategy

Defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime:

java
// Strategy interface public interface SortStrategy { void sort(int[] data); } // Concrete strategies public class BubbleSort implements SortStrategy { public void sort(int[] data) { /* bubble sort */ } } public class QuickSort implements SortStrategy { public void sort(int[] data) { /* quick sort */ } } // Context — uses whichever strategy is injected public class DataProcessor { private SortStrategy strategy; public void setStrategy(SortStrategy strategy) { this.strategy = strategy; } public void process(int[] data) { strategy.sort(data); // delegate to strategy } } // Runtime switching DataProcessor processor = new DataProcessor(); processor.setStrategy(new QuickSort()); processor.process(data); processor.setStrategy(new BubbleSort()); processor.process(data);

7. Design Pattern: Observer

One-to-many dependency — when one object changes state, all dependents are notified automatically:

java
// Observer interface public interface EventListener { void onEvent(String event); } // Subject (publisher) public class EventPublisher { private final List<EventListener> listeners = new ArrayList<>(); public void subscribe(EventListener listener) { listeners.add(listener); } public void unsubscribe(EventListener listener) { listeners.remove(listener); } public void publish(String event) { listeners.forEach(l -> l.onEvent(event)); } } // Concrete observers public class EmailNotifier implements EventListener { public void onEvent(String event) { System.out.println("Email sent for: " + event); } } public class AuditLogger implements EventListener { public void onEvent(String event) { System.out.println("Audit log: " + event); } } // Usage EventPublisher publisher = new EventPublisher(); publisher.subscribe(new EmailNotifier()); publisher.subscribe(new AuditLogger()); publisher.publish("ORDER_PLACED"); // both notifiers fire

Spring's ApplicationEventPublisher and @EventListener are a production-grade Observer implementation. Prefer them over hand-rolled observers in Spring applications.


What's Next?

  • Factory and Abstract Factory patterns
  • Decorator and Proxy patterns (used heavily in Spring AOP)
  • Java generics and bounded type parameters
  • Functional interfaces and lambdas as Strategy implementations
Back to Blog