OOP Concepts

Classes, objects, and OOP principles

1. OOP Fundamentals

OOP is a programming paradigm that organizes code around objects rather than functions. It models real-world entities as objects that contain both data (attributes) and behavior (methods). This approach promotes code reusability, modularity, and easier maintenance.

Encapsulation

Data hiding and bundling

Inheritance

Code reuse and hierarchy

Polymorphism

Many forms, one interface

Abstraction

Hide complexity, show essentials

Basic Class Structure

// Class definition with fields and methods
public class Car {
    private String brand;    // Instance variable
    private int year;        // Instance variable

    // Constructor
    public Car(String brand, int year) {
        this.brand = brand;
        this.year = year;
    }

    // Method
    public void start() {
        System.out.println(brand + " started");
    }
}

// Object creation and usage
Car myCar = new Car("Toyota", 2023);
myCar.start(); // Output: Toyota started

2. Encapsulation

Encapsulation is the bundling of data (variables) and methods that operate on that data into a single unit (class). It also involves restricting direct access to some of an object’s components, which is known as data hiding.

Data Hiding

Make fields private to prevent direct access from outside the class

Controlled Access

Use public methods (getters/setters) to control how data is accessed

Validation

Add validation logic in setter methods to ensure data integrity

Encapsulation Guidelines

  • Always make instance variables private
  • Provide public getter/setter methods only when necessary
  • Use validation in setter methods to maintain data integrity
  • Consider immutable objects for better thread safety
  • Don’t expose internal collections directly - return copies instead

Encapsulation Example

// Proper encapsulation with data hiding
public class Student {
    private String name;        // Private field
    private int age;           // Private field
    private double gpa;        // Private field

    // Constructor
    public Student(String name, int age) {
        this.name = name;
        setAge(age); // Use setter for validation
    }

    // Getter with no validation needed
    public String getName() {
        return name;
    }

    // Setter with validation
    public void setAge(int age) {
        if (age >= 0 && age 150) {
            this.age = age;
        } else {
            throw new IllegalArgumentException("Invalid age");
        }
    }

    // Getter
    public int getAge() {
        return age;
    }

    // Controlled access to GPA
    public void setGPA(double gpa) {
        if (gpa >= 0.0 && gpa 4.0) {
            this.gpa = gpa;
        }
    }

    public double getGPA() {
        return gpa;
    }
}

// Usage
Student student = new Student("Alice", 20);
student.setGPA(3.8);
// student.age = -5;  // ✗ Compilation error - field is private
student.setAge(21);   // ✓ Uses validation

3. Inheritance

Inheritance allows a new class (subclass/child) to acquire properties and methods from an existing class (superclass/parent). It establishes an “is-a” relationship between classes and promotes code reuse.

Code Reuse

Child classes inherit all non-private members from parent classes

Method Overriding

Child classes can provide specific implementations of parent methods

Hierarchical Structure

Creates a tree-like structure of related classes

Inheritance Guidelines

  • Use inheritance for “is-a” relationships (Dog is-a Animal)
  • Keep inheritance hierarchies shallow (2-3 levels max)
  • Use protected for members that subclasses need access to
  • Always use @Override annotation when overriding methods
  • Call super() in constructors to initialize parent state
  • Favor composition over inheritance when relationship isn’t clearly “is-a”

Inheritance Example

// Parent class
class Animal {
    protected String name;    // Protected: accessible by subclasses
    protected int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void eat() {
        System.out.println(name + " is eating");
    }

    public void sleep() {
        System.out.println(name + " is sleeping");
    }
}

// Child class inheriting from Animal
class Dog extends Animal {
    private String breed;

    public Dog(String name, int age, String breed) {
        super(name, age);    // Call parent constructor
        this.breed = breed;
    }

    // Method overriding
    @Override
    public void eat() {
        System.out.println(name + " the dog is munching kibble");
    }

    // New method specific to Dog
    public void bark() {
        System.out.println(name + " says Woof!");
    }
}

// Usage
Dog myDog = new Dog("Buddy", 3, "Golden Retriever");
myDog.eat();    // Calls overridden method
myDog.sleep();  // Inherited method
myDog.bark();   // Dog-specific method

4. Polymorphism

Polymorphism allows objects of different types to be treated as objects of a common base type. The word “polymorphism” comes from Greek, meaning “many forms.” It enables a single interface to represent different underlying forms (data types).

Runtime Polymorphism

Method overriding - determined at runtime through dynamic method dispatch

Compile-time Polymorphism

Method overloading - resolved at compile time based on method signatures

Interface Implementation

Multiple classes can implement the same interface with different behaviors

Polymorphism Best Practices

  • Program to interfaces, not implementations
  • Use polymorphism to reduce code duplication
  • Leverage polymorphism in collections for flexible data handling
  • Override equals() and hashCode() when necessary
  • Use instanceof sparingly - prefer polymorphic methods

Runtime Polymorphism Example

// Runtime Polymorphism Example
abstract class Shape {
    public abstract double getArea();
    public abstract void draw();
}

class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

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

    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

class Rectangle extends Shape {
    private double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double getArea() {
        return width * height;
    }

    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

// Polymorphism in action
Shape[] shapes = {new Circle(5), new Rectangle(4, 6)};
for (Shape shape : shapes) {
    shape.draw();    // Calls appropriate draw() method
    System.out.println("Area: " + shape.getArea());
}

Method Overloading Example

// Method Overloading Example
class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public double add(double a, double b) {
        return a + b;
    }

    public int add(int a, int b, int c) {
        return a + b + c;
    }
}

Calculator calc = new Calculator();
System.out.println(calc.add(5, 3));        // Calls int version
System.out.println(calc.add(5.5, 3.2));    // Calls double version
System.out.println(calc.add(1, 2, 3));     // Calls three-parameter version

5. Abstraction

Abstraction is the process of hiding implementation details while showing only essential features to the user. It focuses on what an object does rather than how it does it. Abstraction is achieved through abstract classes and interfaces.

Abstract Classes

Classes that cannot be instantiated and may contain abstract methods

Interfaces

Contracts that define what methods a class must implement

Implementation Hiding

Hide complex logic behind simple, easy-to-use interfaces

Abstraction Guidelines

  • Use abstract classes when you have common code to share among subclasses
  • Use interfaces when you want to specify a contract without implementation
  • Keep interfaces focused and cohesive (Interface Segregation Principle)
  • Provide default methods in interfaces when it makes sense (Java 8+)
  • Abstract away complex operations behind simple method calls

Abstraction Examples

// Interface - Pure abstraction
interface Drawable {
    void draw();                    // Abstract method
    void setColor(String color);    // Abstract method

    // Default method (Java 8+)
    default void display() {
        System.out.println("Displaying drawable object");
    }
}

// Abstract class - Partial abstraction
abstract class Vehicle {
    protected String brand;

    // Concrete method
    public void start() {
        System.out.println(brand + " starting...");
    }

    // Abstract method - must be implemented by subclasses
    public abstract void accelerate();
    public abstract int getMaxSpeed();
}

// Concrete implementation
class Car extends Vehicle implements Drawable {
    private String color;

    public Car(String brand) {
        this.brand = brand;
    }

    @Override
    public void accelerate() {
        System.out.println(brand + " car accelerating smoothly");
    }

    @Override
    public int getMaxSpeed() {
        return 200; // km/h
    }

    @Override
    public void draw() {
        System.out.println("Drawing a " + color + " " + brand + " car");
    }

    @Override
    public void setColor(String color) {
        this.color = color;
    }
}

// Usage
Vehicle myCar = new Car("Toyota");  // Programming to abstraction
myCar.start();
myCar.accelerate();

Drawable drawable = (Car) myCar;    // Interface reference
drawable.setColor("Red");
drawable.draw();

6. Composition vs Inheritance

One of the most important decisions in OOP design is whether to use inheritance (“is-a”) or composition (“has-a”). This choice affects flexibility, maintainability, and the overall design of your application.

Use Inheritance When:

  • There’s a clear “is-a” relationship (Dog is-a Animal)
  • You want to provide polymorphic behavior
  • You need to override specific methods
  • The relationship is stable and unlikely to change

Use Composition When:

  • There’s a “has-a” relationship (Car has-a Engine)
  • You need flexibility to change behavior at runtime
  • You want to avoid deep inheritance hierarchies
  • You need multiple inheritance-like behavior

Composition vs Inheritance Example

// Composition Example - "Has-a" relationship
class Engine {
    private String type;
    private int horsepower;

    public Engine(String type, int horsepower) {
        this.type = type;
        this.horsepower = horsepower;
    }

    public void start() {
        System.out.println(type + " engine starting");
    }

    public void stop() {
        System.out.println(type + " engine stopping");
    }
}

class Car {
    private Engine engine;  // Composition - Car HAS-A Engine
    private String model;

    public Car(String model, Engine engine) {
        this.model = model;
        this.engine = engine;  // Injected dependency
    }

    public void start() {
        engine.start();  // Delegating to composed object
        System.out.println(model + " is ready");
    }

    public void stop() {
        engine.stop();
        System.out.println(model + " stopped");
    }

    // Can change engine at runtime
    public void replaceEngine(Engine newEngine) {
        this.engine = newEngine;
    }
}

// Usage demonstrating both approaches
Engine v6Engine = new Engine("V6", 300);
Car myCar = new Car("Sedan", v6Engine);
myCar.start();

// Can replace engine (flexibility of composition)
Engine electricEngine = new Engine("Electric", 400);
myCar.replaceEngine(electricEngine);

7. SOLID Principles

SOLID is an acronym for five design principles intended to make software designs more understandable, flexible, and maintainable. These principles form the foundation of clean, robust object-oriented code.

S

Single Responsibility

A class should have only one reason to change

O

Open/Closed

Open for extension, closed for modification

L

Liskov Substitution

Subclasses should be substitutable for base classes

I

Interface Segregation

Many specific interfaces over one general interface

D

Dependency Inversion

Depend on abstractions, not concretions

SOLID Principles in Action

// Single Responsibility - Each class has one job
class User {
    private String name;
    private String email;
    // Only user data, no validation or persistence logic
}

class UserValidator {
    public boolean isValidEmail(String email) {
        return email.contains("@") && email.contains(".");
    }
}

class UserRepository {
    public void save(User user) {
        // Database saving logic only
    }
}

// Open/Closed - Strategy pattern for extensibility
interface PaymentStrategy {
    void pay(double amount);
}

class CreditCardPayment implements PaymentStrategy {
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " with credit card");
    }
}

class PayPalPayment implements PaymentStrategy {
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " with PayPal");
    }
}

// Interface Segregation - Specific interfaces
interface Flyable {
    void fly();
}

interface Swimmable {
    void swim();
}

// Duck can fly and swim
class Duck implements Flyable, Swimmable {
    public void fly() { System.out.println("Duck flying"); }
    public void swim() { System.out.println("Duck swimming"); }
}

// Fish only swims
class Fish implements Swimmable {
    public void swim() { System.out.println("Fish swimming"); }
}

8. Common Design Patterns

Design patterns are reusable solutions to common problems in object-oriented design. They represent best practices and provide a shared vocabulary for developers.

Singleton

Ensures only one instance of a class exists

Factory

Creates objects without specifying exact classes

Observer

Notifies multiple objects of state changes

Strategy

Encapsulates algorithms for runtime selection

Design Pattern Examples

// Singleton Pattern
class DatabaseConnection {
    private static DatabaseConnection instance;

    private DatabaseConnection() {} // Private constructor

    public static DatabaseConnection getInstance() {
        if (instance == null) {
            instance = new DatabaseConnection();
        }
        return instance;
    }
}

// Factory Pattern
interface Animal {
    void makeSound();
}

class Dog implements Animal {
    public void makeSound() { System.out.println("Woof!"); }
}

class Cat implements Animal {
    public void makeSound() { System.out.println("Meow!"); }
}

class AnimalFactory {
    public static Animal createAnimal(String type) {
        switch (type.toLowerCase()) {
            case "dog": return new Dog();
            case "cat": return new Cat();
            default: throw new IllegalArgumentException("Unknown animal type");
        }
    }
}

// Observer Pattern
interface Observer {
    void update(String message);
}

class NewsAgency {
    private List<Observer> observers = new ArrayList<>();

    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    public void notifyObservers(String news) {
        for (Observer observer : observers) {
            observer.update(news);
        }
    }
}

9. OOP Best Practices

Following established best practices ensures your OOP code is readable, maintainable, and follows industry standards.

Clean OOP Code Example

// Good: Single Responsibility Principle
class EmailValidator {
    public boolean isValid(String email) {
        return email != null && email.contains("@") && email.contains(".");
    }
}

class UserRepository {
    public void save(User user) {
        // Database operations only
    }

    public User findById(Long id) {
        // Database query only
        return null; // Simplified
    }
}

// Good: Immutable class
final class Money {
    private final BigDecimal amount;
    private final Currency currency;

    public Money(BigDecimal amount, Currency currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public BigDecimal getAmount() { return amount; }
    public Currency getCurrency() { return currency; }

    public Money add(Money other) {
        // Returns new instance instead of modifying
        return new Money(amount.add(other.amount), currency);
    }
}

// Good: Composition over inheritance
class Car {
    private final Engine engine;        // Composition
    private final Transmission transmission;  // Composition

    public Car(Engine engine, Transmission transmission) {
        this.engine = engine;
        this.transmission = transmission;
    }

    public void start() {
        engine.start();
    }
}

OOP Quick Reference

Class Elements:

  • private String field - Instance variable
  • public Class(params) - Constructor
  • public void method() - Instance method

Keywords:

  • extends - Class inheritance
  • implements - Interface implementation
  • abstract - Cannot be instantiated
  • final - Cannot be overridden