Skip to content

OOP Notes

Classes, objects, the four pillars, abstract vs interface

Stage 1intro11 min read

The core object-oriented concepts, in one place.

How this file relates to ../JavaConcepts/: this file explains the idea and when to use it. JavaConcepts explains the Java syntax and detail for each one, with examples from this repo. Where a topic has a fuller write-up there, the section here links to it instead of repeating it.


Table of Contents

  1. Class and Object
  2. Access Modifiers
  3. Constructors
  4. this and super Keywords
  5. Static vs Instance Members
  6. The Four Pillars
  7. Encapsulation
  8. Inheritance
  9. Polymorphism
  10. Abstraction
  11. Abstract Classes vs Interfaces
  12. Multiple Inheritance with Interfaces
  13. Relationships Between Classes
  14. Inner Classes
  15. Final Keyword
  16. Object Class and Methods
  17. Exception Handling in OOP
  18. Best Practices
  19. Summary

1. Class and Object

Class

A class is a definition that lists the data and actions its objects can have.

It is a reusable plan. It says what each object of this kind knows and can do. It has:

  • Fields (variables): store data and state
  • Methods (functions): define behaviour
  • Constructors: set up new objects

Object

An object is one real thing built from a class while the program runs. It has its own identity and its own data.

The class is the plan and the object is one result built from that plan. One class can produce many objects.

class Car {
    String brand;
    int year;
}

Car a = new Car();   // one object
Car b = new Car();   // a different object, its own state

Think of the class as the blueprint for a house and each object as a house built from it. Change the blueprint and future houses change. Change one house and only that house changes.


2. Access Modifiers

Access modifiers decide who is allowed to use a class, method or field. Start with the strictest one that works, usually private. Open it up only when code outside really needs it.

ModifierSame classSame packageSubclassEverywhere
publicyesyesyesyes
protectedyesyesyesno
defaultyesyesnono
privateyesnonono

"default" means you wrote no modifier at all. It is also called package private.

class Example {
    public int pub = 1;      // anywhere
    protected int prot = 2;  // package plus subclasses
    int def = 3;             // package only
    private int priv = 4;    // this class only
}

3. Constructors

A constructor runs once, when an object is created. It has the same name as the class and no return type. Its job is to make the object ready to use, so anything the object cannot work without belongs in the constructor.

The three kinds you will see: default (no parameters), parameterised (takes values to start with), and copy (builds a new object from another object of the same class).

Full detail and repo examples: ../JavaConcepts/05-Constructors.md.


4. this and super Keywords

this

  • Refers to the current object
  • Tells a field apart from a parameter with the same name
  • this(...) calls another constructor in the same class

super

  • Refers to the parent class
  • super(...) calls the parent constructor, and must be the first statement
  • super.method() calls the parent's version of a method that the child has replaced (overridden)
class Parent {
    String name = "Parent";
}

class Child extends Parent {
    String name = "Child";

    void show() {
        System.out.println(this.name);   // Child
        System.out.println(super.name);  // Parent
    }
}

5. Static vs Instance Members

Static members belong to the class itself. There is one copy, shared by all objects. It is created when the class loads, and you can use it without creating an object.

Instance members belong to one object. Every object has its own copy.

class Example {
    static int count = 0;   // one shared counter
    int id;                 // one per object

    Example() {
        count++;
        id = count;
    }
}

A static method cannot use instance fields. It runs without any object, so there is no object whose field it could read.

Full detail, plus final: ../JavaConcepts/06-StaticAndFinal.md.


6. The Four Pillars

Everything below is built on four ideas. Try to say each one in a single sentence.

PillarOne sentenceMain tool
EncapsulationKeep data private and expose only what is needed.private plus getters
InheritanceA class can reuse and specialise another class.extends
PolymorphismOne type, many behaviours, picked at runtime.overriding, interfaces
AbstractionShow what something does, hide how it does it.interfaces, abstract classes

7. Encapsulation

Encapsulation means keeping data and the methods that use it together in one class, and protecting the data from outside code. In practice: fields are private, and other code reaches them only through methods.

The problem it solves: if any code can change a field, any code can break it. A bank balance set to -500 by a stray line is hard to track down.

Why it matters:

  • Data hiding: outside code cannot touch the internal state
  • Controlled access: you decide what can be read and what can be written
  • Validation: a setter can reject bad values, a public field cannot
  • Freedom to change: you can rewrite the inside without breaking the code that uses the class
class Account {
    private double balance;         // nobody can set this directly

    public double getBalance() {
        return balance;
    }

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

Note there is no setBalance. Encapsulation does not mean "write a getter and setter for every field". It means "offer only the actions that make sense". Here the action is deposit, not "overwrite the balance".

A getter that returns an internal collection breaks encapsulation without warning, because the caller can then change that collection. Return a copy instead. See ClassRelationships.md.

Full detail: ../JavaConcepts/04-Encapsulation.md.


8. Inheritance

Inheritance lets one class take the fields and methods of another class, then add or change what it needs. This saves you from copying the same code into many classes. It is the "is-a" relationship: a Dog is an Animal.

Types:

  1. Single: one class extends another
  2. Multilevel: a class extends a class that extends another
  3. Hierarchical: several classes extend the same base
class Animal {
    void eat() {}
}

class Dog extends Animal {   // single
    void bark() {}
}

class Puppy extends Dog {    // multilevel
    void play() {}
}

Java has no multiple inheritance of classes. A class extends at most one class, but can implement many interfaces.

Use it carefully. Inheritance ties classes together more tightly than anything else in OOP. The child depends on the parent's inner details, so a change to the parent can break every child. Try composition (building a class from smaller parts) first. See CompositionVsInheritance.md.

Full detail: ../JavaConcepts/03-AbstractClassesAndInheritance.md.


9. Polymorphism

Polymorphism means "many forms". You write one call, and what it does depends on which object is behind it.

KindAlso calledDecided atHow
Overloadingstaticcompile timesame method name, different parameters
Overridingdynamicrun timechild redefines a parent method
// Overloading: the compiler picks by argument types
class Calculator {
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }
}

// Overriding: the JVM picks by the real object
Animal animal = new Dog();
animal.sound();     // Dog's version runs, even though the variable is an Animal

The second one is why the design patterns in this repo work. PaymentService looks up a PaymentMethod and calls pay(). The right version (card, UPI, wallet) runs, and the service never needs to know which one it got.

Full detail: ../JavaConcepts/07-OverridingAndPolymorphism.md.


10. Abstraction

Abstraction hides the complicated inside and shows only what the caller needs. You drive a car with a steering wheel and pedals without knowing how the engine works.

Use it when:

  • Several things should be used through one common type
  • Callers should not depend on how the work is done inside
  • Some behaviour is shared, but part of it must differ for each version
abstract class Shape {
    abstract double getArea();     // every shape has an area, each computes it differently
}

class Circle extends Shape {
    double radius;

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

Encapsulation hides data. Abstraction hides how the work is done. They often appear together, but they are not the same thing.


11. Abstract Classes vs Interfaces

A classic interview question. Both let you write code against a general type instead of one specific class.

Abstract classInterface
Methodsabstract and concreteabstract, plus default and static since Java 8
Fieldsany instance variablesconstants only (public static final)
Constructoryesno
Inheritancea class extends onea class implements many
Relationship"is a kind of""can do this"
Use whenimplementations share state and codeyou are defining a contract
// Abstract class: shared state and shared code
abstract class Appliance {
    private final String model;         // state lives here

    Appliance(String model) {
        this.model = model;
    }

    abstract void turnOn();             // each appliance does this differently
    void display() {                    // shared for everyone
        System.out.println(model);
    }
}

// Interface: a contract, no state
interface RemoteControl {
    void changeChannel(int channel);
    default void info() {               // optional shared behaviour
        System.out.println("Remote controlled");
    }
}

class TV extends Appliance implements RemoteControl {
    TV(String model) { super(model); }

    @Override void turnOn() {}
    @Override public void changeChannel(int channel) {}
}

Rule of thumb: start with an interface. Use an abstract class only when the subclasses really share data or real code.

Full detail: ../JavaConcepts/01-Interfaces.md and ../JavaConcepts/03-AbstractClassesAndInheritance.md.


12. Multiple Inheritance with Interfaces

Java does not let a class extend two classes. If both parents had the same method, Java could not tell which one to use. Interfaces avoid most of this, because they hold no data and their methods are normally just contracts. A class can implement as many as it wants. If two interfaces give the same default method, the class must override it and pick one, or the code will not compile.

interface Swimmer {
    void swim();
}

interface Flyer {
    void fly();
}

class Duck implements Swimmer, Flyer {
    @Override public void swim() {}
    @Override public void fly() {}
}

This also keeps interfaces small. A class takes only the contracts it can really fulfil. That idea is the Interface Segregation Principle. See ../../Principles/SOLID/ISP/.


13. Relationships Between Classes

Inheritance is only one way classes connect. The others are association, aggregation, composition and dependency. Choosing the right one is most of class design.

PhraseRelationshipJava shapeExample
is-ainheritanceextends / implementsDog is an Animal
has-aassociation, aggregation, compositiona fieldCar has an Engine
uses-adependencya method parameterService uses a DTO

Full write-up with UML symbols, multiplicity and database tables: ClassRelationships.md.


14. Inner Classes

A class written inside another class. Use it when the inner class only makes sense next to the outer one, so nothing else needs to see it.

Four kinds: member inner (non static), static nested, local (inside a method), and anonymous (no name, defined and used on the spot).

The one you will use most is static nested. It suits a helper that belongs to the outer class but does not need an outer object to work. Both the Builder and the Singleton holder in this repo use it.

Full detail and repo examples: ../JavaConcepts/11-InnerAndNestedClasses.md.


15. Final Keyword

final means "this cannot change". What it locks depends on where you put it:

  • Variable: cannot be given a new value after it is set
  • Method: cannot be overridden
  • Class: cannot be extended

Make fields final by default. It shows what is fixed, stops accidental reassignment, and helps make an object safe to share between threads.

Careful: final on a reference stops you pointing it at a different object. It does not freeze the object itself. A final List can still have items added to it.

Full detail: ../JavaConcepts/06-StaticAndFinal.md.


16. Object Class and Methods

Every Java class inherits from Object, so every object already has these:

MethodDefault behaviourOverride it when
toString()class name plus a hashyou want readable output in logs
equals()compares referencestwo objects with the same data are "equal"
hashCode()based on memory addressalways, whenever you override equals
getClass()the runtime classnever, it is final
class Person {
    private final String id;

    @Override
    public String toString() {
        return "Person(" + id + ")";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        return Objects.equals(id, ((Person) o).id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

Overriding equals without hashCode is a very common bug in Java. A HashMap uses hashCode to pick where to store a key, so an equal object can hash to a different spot and is never found again. Full explanation: ../JavaConcepts/15-EqualsAndHashCode.md.


17. Exception Handling in OOP

Exceptions keep error handling apart from normal logic, so the main flow stays easy to read. They are also part of a class's contract: what a method throws tells callers how it can fail.

class InvalidAgeException extends Exception {
    InvalidAgeException(String message) {
        super(message);
    }
}

class Person {
    private int age;

    void setAge(int age) throws InvalidAgeException {
        if (age < 0 || age > 150) {
            throw new InvalidAgeException("Invalid age: " + age);
        }
        this.age = age;
    }
}

Write a custom exception only when the caller needs to catch that one specific failure. Otherwise use the standard ones: IllegalArgumentException for a bad input, IllegalStateException for a call made at the wrong time.

Full detail: ../JavaConcepts/09-Exceptions.md.


18. Best Practices

SOLID

  • Single Responsibility: a class should have one reason to change
  • Open Closed: add new behaviour by adding new code, not by editing working code
  • Liskov Substitution: a subtype must work anywhere its base type does
  • Interface Segregation: no class should be forced to depend on methods it does not use
  • Dependency Inversion: depend on interfaces, not on concrete classes

Full examples with bad and good code: ../../Principles/SOLID/.

Class design

  • Give classes, methods and variables names that say what they are
  • Keep a class focused on one job
  • Prefer composition over inheritance
  • Use the strictest access modifier that works
  • Make fields final unless they need to change
  • Never return an internal collection from a getter, return a copy

Exceptions

  • Checked exceptions (the compiler forces you to handle them) for failures a caller can recover from
  • Unchecked exceptions for programming errors
  • Put useful context in the message, not just "error"
  • Never swallow an exception silently
  • Use try with resources for anything that must be closed

Performance

  • Give collections a starting size when you know roughly how many items they will hold
  • Use StringBuilder to build strings in a loop
  • Avoid creating objects you throw away straight after
  • Measure before you optimise, because guesses are usually wrong

A small example putting it together

// Depends on interfaces (DIP), one job (SRP), dependencies injected (testable)
class UserService {
    private final UserRepository repository;
    private final EmailService emailService;

    UserService(UserRepository repository, EmailService emailService) {
        this.repository = Objects.requireNonNull(repository);
        this.emailService = Objects.requireNonNull(emailService);
    }

    public Optional<User> findUserById(String id) {
        return repository.findById(id);
    }

    public void createUser(User user) {
        repository.save(user);
        emailService.sendWelcomeEmail(user.getEmail());
    }
}

interface UserRepository {
    Optional<User> findById(String id);
    void save(User user);
}

interface EmailService {
    void sendWelcomeEmail(String email);
}

// Immutable value object: final class, final fields, equals and hashCode on the id
final class User {
    private final String id;
    private final String name;
    private final String email;

    User(String id, String name, String email) {
        this.id = Objects.requireNonNull(id, "id cannot be null");
        this.name = Objects.requireNonNull(name, "name cannot be null");
        this.email = Objects.requireNonNull(email, "email cannot be null");
    }

    public String getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        return Objects.equals(id, ((User) o).id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public String toString() {
        return "User(" + id + ", " + name + ")";
    }
}

Note what UserService does not do. It does not catch and hide exceptions, and it does not create its own repository. It receives its repository and email service through the constructor, so the code that builds it chooses them (for example, a fake one in a test).

Since Java 16 a record gives you the whole User class in one line. Java writes equals, hashCode and toString for you:

record User(String id, String name, String email) { }

19. Summary

ConceptPurposeMain benefit
Classblueprint for objectsorganisation, reuse
Objectinstance of a classmodels a real thing
Encapsulationhide datasafety, freedom to change
Inheritancereuse and specialisehierarchy, polymorphism
Polymorphismone type, many behavioursflexibility, extensibility
Abstractionhide implementationsimplicity, loose coupling
Compositionbuild from partsflexible, swappable at runtime

Where to go next

In this topic

Done with this topic?

How well do you know it?