Object-Oriented Programming

Reviewed & published by Brayan K

This is the most important lesson in the entire Java course. Java is fundamentally object-oriented — everything lives inside a class. Even your main() method is inside a class. Understanding OOP is what separates beginners who can write scripts from developers who can build real applications. After this lesson, you'll think about code completely differently.

Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy: Blueprints & Houses

A class is like a blueprint for a house. It describes the rooms, their sizes, and how the wiring works — but it's not a house itself. When you build a house from the blueprint, that's an object (also called an instance). You can build many houses from one blueprint, each with different paint colors (field values) and different furniture inside.

Before OOP: Programs were long lists of instructions (procedural code). As software grew to thousands of lines, this became unmanageable. OOP lets you break problems into objects that each handle their own data and behavior — like departments in a company, each responsible for their own work.

1️⃣ Your First Class — The Three Core Parts

Every class has three parts: fields (data it stores), a constructor (how to create it), and methods (what it can do). Let's build a Car class step by step:

public class Car {
    // ━━━ FIELDS (data each car stores) ━━━
    String brand;
    int year;
    double mileage;

    // ━━━ CONSTRUCTOR (how to create a car) ━━━
    public Car(String brand, int year) {
        this.brand = brand;    // "this.brand" = the field
        this.year = year;      // "brand" = the parameter
        this.mileage = 0;      // default starting mileage
    }

    // ━━━ METHODS (what a car can do) ━━━
    public void drive(double miles) {
        this.mileage += miles;
    }

    public String displayInfo() {
        return brand + " (" + year + ") — " + mileage + " miles";
    }
}

Now let's USE this class by creating objects:

// Creating objects from the Car blueprint
Car myCar = new Car("Toyota", 2023);
Car friendsCar = new Car("Honda", 2021);

// Each object has its OWN data
myCar.drive(150);
friendsCar.drive(3200);

System.out.println(myCar.displayInfo());
// → "Toyota (2023) — 150.0 miles"

System.out.println(friendsCar.displayInfo());
// → "Honda (2021) — 3200.0 miles"

🔑 Key insight: Each object is independent. Driving myCar doesn't affect friendsCar. They share the same blueprint (class) but have their own copies of the data (fields).

2️⃣ The this Keyword

Inside a method or constructor, this refers to the current object — the specific car, student, or account that the method is being called on. It's most important when parameter names match field names:

// ❌ WITHOUT this — the parameter "shadows" the field
public Car(String brand) {
    brand = brand;  // Sets the parameter to itself! Field unchanged!
}

// ✅ WITH this — clearly distinguishes field from parameter
public Car(String brand) {
    this.brand = brand;  // this.brand = the field, brand = the parameter
}

// this also lets you chain constructors:
public Car(String brand) {
    this(brand, 2024);  // Calls the other constructor
}

3️⃣ Encapsulation — Protecting Your Data

Encapsulation means hiding the internal data of an object and only allowing access through controlled methods. Think of a bank account: you can't reach into the vault directly. You go through the teller (methods) who validates your request.

public class BankAccount {
    private double balance;  // PRIVATE — can't be accessed from outside!

    public BankAccount(double initial) {
        if (initial < 0) {
            throw new IllegalArgumentException("Can't start negative!");
        }
        this.balance = initial;
    }

    // GETTER — read-only access
    public double getBalance() {
        return balance;
    }

    // SETTER with validation — controlled write access
    public void deposit(double amount) {
        if (amount <= 0) {
            System.out.println("Amount must be positive!");
            return;
        }
        balance += amount;
    }

    public boolean withdraw(double amount) {
        if (amount > balance) {
            System.out.println("Insufficient funds!");
            return false;
        }
        balance -= amount;
        return true;
    }
}

// Usage:
BankAccount acct = new BankAccount(1000);
acct.deposit(500);       // ✅ Goes through validation
acct.withdraw(200);      // ✅ Checks balance first
// acct.balance = -999;  // ❌ Won't compile — balance is private!

🔑 Why not just make everything public? Without encapsulation, anyone could write acct.balance = -999999. Encapsulation ensures your data stays valid by forcing all changes through methods that include validation.

public class Main {
    // ━━━ A Car class with fields, constructor, and methods ━━━
    static class Car {
        String brand, model;
        int year;
        double mileage = 0;

        Car(String brand, String model, int year) {
            this.brand = brand;
            this.model = model;
            this.year = year;
        }
        void drive(double miles) { this.mileage += miles; }
        String displayInfo() {
            return brand + " " + model + " (" + year + ") - " + mileage + " mi";
        }
        int age() { return 2025 - year; }
    }

    // ━━━ A BankAccount with encapsulated (private) balance ━━━
    static class BankAccount {
        private double balance;

        BankAccount(double initial) {
            if (initial < 0) throw new IllegalArgumentException("Can't start negative!");
            this.balance = initial;
        }
        double getBalance() { return balance; }
        void deposit(double amt) {
            if (amt <= 0) { System.out.println("   Amount must be positive!"); return; }
            balance += amt;
            System.out.println("   Deposited $" + amt + " -> Balance: $" + balance);
        }
        boolean withdraw(double amt) {
            if (amt > balance) { System.out.println("   Insufficient funds!"); return false; }
            balance -= amt;
            System.out.println("   Withdrew $" + amt + " -> Balance: $" + balance);
            return true;
        }
    }

    public static void main(String[] args) {
        System.out.println("1. CAR OBJECTS:");
        Car car1 = new Car("Toyota", "Camry", 2022);
        Car car2 = new Car("Honda", "Civic", 2023);
        car1.drive(5000);
        car2.drive(3200);
        System.out.println("   " + car1.displayInfo());
        System.out.println("   " + car2.displayInfo());
        System.out.println("   Car1 age: " + car1.age() + " years");

        System.out.println("\n2. BANK ACCOUNT (encapsulated):");
        BankAccount acct = new BankAccount(1000);
        System.out.println("   Starting balance: $" + acct.getBalance());
        acct.deposit(500);
        acct.withdraw(200);
        acct.withdraw(5000);  // Fails!
        acct.deposit(-100);   // Rejected!

        System.out.println("\n3. OBJECTS ARE INDEPENDENT:");
        BankAccount a1 = new BankAccount(100);
        BankAccount a2 = new BankAccount(500);
        a1.deposit(50);
        System.out.println("   Account 1: $" + a1.getBalance());
        System.out.println("   Account 2: $" + a2.getBalance() + " (unchanged!)");
    }
}

4️⃣ Constructor Overloading — Multiple Ways to Create Objects

Just like methods, constructors can be overloaded — you can have multiple constructors with different parameter lists, giving users different ways to create objects:

public class Student {
    private String name;
    private String grade;
    private double gpa;

    // Full constructor
    public Student(String name, String grade, double gpa) {
        this.name = name;
        this.grade = grade;
        this.gpa = gpa;
    }

    // Partial constructor (defaults)
    public Student(String name) {
        this(name, "Freshman", 0.0);  // Calls the full constructor
    }

    // Copy constructor
    public Student(Student other) {
        this(other.name, other.grade, other.gpa);
    }
}

// Usage:
Student s1 = new Student("Alice", "Junior", 3.8);
Student s2 = new Student("Bob");          // Freshman, GPA 0.0
Student s3 = new Student(s1);             // Copy of Alice

5️⃣ Static vs Instance — Class-Level vs Object-Level

Instance members belong to each individual object (each car has its own mileage). Static members belong to the class itself and are shared by all objects.

public class Car {
    // Instance field — each car has its own
    private String brand;
    private double mileage;

    // Static field — shared by ALL cars
    private static int totalCarsCreated = 0;

    public Car(String brand) {
        this.brand = brand;
        this.mileage = 0;
        totalCarsCreated++;  // Incremented for EVERY car
    }

    // Static method — called on the CLASS, not an object
    public static int getTotalCars() {
        return totalCarsCreated;
    }
}

Car a = new Car("Toyota");
Car b = new Car("Honda");
Car c = new Car("BMW");
System.out.println(Car.getTotalCars());  // 3 (shared counter)

💡 When to use static:

import java.util.ArrayList;
import java.util.List;

public class Main {
    static class Student {
        static int totalStudents = 0;  // shared by ALL students
        String name, grade;
        double gpa;

        // Full constructor
        Student(String name, String grade, double gpa) {
            this.name = name;
            this.grade = grade;
            this.gpa = gpa;
            totalStudents++;
        }
        // Overloaded constructor with defaults
        Student(String name) { this(name, "Freshman", 0.0); }

        @Override
        public String toString() {
            return String.format("%s (%s, GPA: %.2f)", name, grade, gpa);
        }
    }

    static class Book {
        String title, author;
        int pages;
        Book(String title, String author, int pages) {
            this.title = title; this.author = author; this.pages = pages;
        }
        String info() { return "\"" + title + "\" by " + author + " (" + pages + "p)"; }
    }

    public static void main(String[] args) {
        System.out.println("1. CONSTRUCTOR OVERLOADING:");
        Student s1 = new Student("Alice", "Junior", 3.85);
        Student s2 = new Student("Bob");                     // defaults!
        Student s3 = new Student("Charlie", "Senior", 3.42);
        System.out.println("   " + s1);
        System.out.println("   " + s2 + " <- defaults applied!");
        System.out.println("   " + s3);

        System.out.println("\n2. STATIC COUNTER:");
        System.out.println("   Total students created: " + Student.totalStudents);
        new Student("Diana");
        System.out.println("   After adding Diana: " + Student.totalStudents);

        System.out.println("\n3. ARRAY OF OBJECTS:");
        List<Book> library = new ArrayList<>(List.of(
            new Book("1984", "Orwell", 328),
            new Book("Dune", "Herbert", 412),
            new Book("Gatsby", "Fitzgerald", 180),
            new Book("Hobbit", "Tolkien", 310)));
        for (int i = 0; i < library.size(); i++)
            System.out.println("   " + (i + 1) + ". " + library.get(i).info());

        library.sort((a, b) -> a.pages - b.pages);
        System.out.println("\n   Sorted by pages:");
        for (int i = 0; i < library.size(); i++)
            System.out.println("   " + (i + 1) + ". " + library.get(i).info());

        Book longest = library.stream().max((a, b) -> a.pages - b.pages).get();
        System.out.println("\n   Longest: " + longest.info());
        int totalPages = library.stream().mapToInt(b -> b.pages).sum();
        System.out.println("   Total pages: " + totalPages);
    }
}

6️⃣ The Four Pillars of OOP

These four principles are the foundation of object-oriented design. You've already learned the first two — the others come in the next lessons:

1. Encapsulation ✅ (this lesson)

Hide internal details, control access via methods. Like a TV remote — you press buttons, you don't rewire circuits.

2. Abstraction

Show only essential features, hide complexity. Like driving a car — you use the steering wheel without understanding the engine.

3. Inheritance (next lesson)

Create new classes from existing ones. "Dog is an Animal" — dogs automatically get Animal behaviors.

4. Polymorphism (next lesson)

Objects can take many forms. A draw() method works differently for Circle, Square, and Triangle.

7️⃣ toString() — Making Objects Printable

By default, printing an object shows something useless like Car@1b6d3586. Override toString() to make it human-readable:

public class Car {
    private String brand;
    private int year;

    @Override
    public String toString() {
        return brand + " (" + year + ")";
    }
}

Car car = new Car("Toyota", 2023);
System.out.println(car);  // "Toyota (2023)" — automatically calls toString()

// Without @Override toString(), it would print: Car@1b6d3586
import java.util.ArrayList;
import java.util.List;

public class Main {
    static class Grade {
        String subject;
        int score;
        Grade(String subject, int score) { this.subject = subject; this.score = score; }
    }

    static class Student {
        private final List<Grade> grades = new ArrayList<>();  // encapsulated
        String name, id;

        Student(String name, String id) { this.name = name; this.id = id; }

        void addGrade(String subject, int score) {
            if (score < 0 || score > 100) {
                System.out.println("   Invalid score: " + score);
                return;
            }
            grades.add(new Grade(subject, score));
        }
        double getAverage() {
            if (grades.isEmpty()) return 0;
            return grades.stream().mapToInt(g -> g.score).average().orElse(0);
        }
        String getLetterGrade() {
            double avg = getAverage();
            if (avg >= 90) return "A";
            if (avg >= 80) return "B";
            if (avg >= 70) return "C";
            if (avg >= 60) return "D";
            return "F";
        }
        void report() {
            System.out.println("   --- " + name + " (ID: " + id + ") ---");
            for (Grade g : grades)
                System.out.printf("   %-12s%d%n", g.subject, g.score);
            System.out.println("   --------------------");
            System.out.printf("   Average:  %.1f (%s)%n", getAverage(), getLetterGrade());
        }
    }

    public static void main(String[] args) {
        System.out.println("=== Student Grade System ===\n");
        Student alice = new Student("Alice Johnson", "S001");
        alice.addGrade("Math", 95);
        alice.addGrade("English", 88);
        alice.addGrade("Science", 92);
        alice.addGrade("History", 85);

        Student bob = new Student("Bob Smith", "S002");
        bob.addGrade("Math", 72);
        bob.addGrade("English", 68);
        bob.addGrade("Science", 75);
        bob.addGrade("History", 80);

        alice.report();
        System.out.println();
        bob.report();

        System.out.println("\n--- CLASS SUMMARY ---");
        for (Student s : List.of(alice, bob))
            System.out.printf("   %-16s%s (%.1f)%n", s.name, s.getLetterGrade(), s.getAverage());

        System.out.println("\n--- ENCAPSULATION TEST ---");
        alice.addGrade("Art", 150);  // rejected
        alice.addGrade("Art", -5);   // rejected
        alice.addGrade("Art", 91);   // valid
        System.out.printf("   Alice's new average: %.1f%n", alice.getAverage());
    }
}
public class Main {
    static class Locker {
        // 1) Nobody outside the class should reach these directly.
        ___ String owner;                 // 👉 replace ___ with private
        private int capacity;
        private int used;

        // 2) The constructor has the class's name and no return type.
        ___(String owner, int capacity) { // 👉 replace ___ with Locker
            // 3) Tell the field apart from the parameter of the same name.
            ___.owner = owner;            // 👉 replace ___ with this
            this.capacity = capacity;
            this.used = 0;
        }

        boolean store(int items) {
            if (used + items > capacity) return false;
            used += items;
            return true;
        }

        // 4) A getter reads a private field without exposing it.
        int getSpaceLeft() {
            ___ capacity - used;          // 👉 replace ___ with return
        }

        // 5) Printing an object calls this method, if you write one.
        @Override
        public String ___() {             // 👉 replace ___ with toString
            return owner + "'s locker: " + used + "/" + capacity;
        }
    }

    public static void main(String[] args) {
        Locker a = new Locker("Ada", 10);
        System.out.println(a.store(4));   // fits
        System.out.println(a.store(9));   // would overflow, so refused
        System.out.println("Space left: " + a.getSpaceLeft());
        System.out.println(a);            // uses toString()
    }
}

Common Mistakes

public Car(String brand) { brand = brand; }  // ❌ Does nothing!
public Car(String brand) { this.brand = brand; } // ✅ Correct

Pro Tips

💡 Name classes with nouns: Car, Student, BankAccount — not CarManager or DoStuff.

💡 Keep classes focused: A class should represent ONE concept. If it's doing too many things, split it up.

💡 Always override toString(): Makes debugging 10x easier when you can print objects meaningfully.

💡 Start private, open later: Begin with everything private. Make things public only when external access is actually needed.

💡 Getters without setters = read-only fields: Provide a getter but no setter for values that shouldn't change after creation (like a student's ID).

📋 Quick Reference

ConceptSyntaxPurpose
Classclass Car { }Define a blueprint
Objectnew Car("Toyota")Create an instance
thisthis.brand = brandRefer to current object
privateprivate int x;Restrict field access
Getterpublic int getX()Read-only access
Setterpublic void setX(int x)Validated write access
staticstatic int count;Shared across all instances
toString()@Override toString()Printable representation

🎉 Lesson Complete!

You now understand classes, objects, encapsulation, constructors, the this keyword, and static members — the foundation of ALL Java programming. Every framework, library, and API you'll ever use in Java is built on these concepts.

Practice quiz

What is the relationship between a class and an object?

  • They are identical
  • An object is a blueprint for a class
  • A class is a blueprint; an object is an instance built from it
  • A class is created with new

Answer: A class is a blueprint; an object is an instance built from it. A class is the blueprint; an object (instance) is a concrete thing created from that blueprint with new.

What are the three core parts of a class?

  • Fields, constructor, methods
  • Imports, loops, returns
  • Public, private, static
  • Class, object, instance

Answer: Fields, constructor, methods. A class has fields (data), a constructor (how to create it), and methods (what it can do).

What does the 'this' keyword refer to inside a method?

  • The class itself
  • The parent class
  • A static field
  • The current object the method is called on

Answer: The current object the method is called on. this refers to the current object — useful when a parameter name matches a field name.

What does encapsulation mean?

  • Making all fields public
  • Hiding internal data and controlling access through methods
  • Copying objects
  • Running code faster

Answer: Hiding internal data and controlling access through methods. Encapsulation hides internal data (e.g. private fields) and exposes controlled access via methods.

Why declare a field like balance as private?

  • So outside code can't set it to an invalid value directly
  • To make it run faster
  • So it prints automatically
  • Because private fields are static

Answer: So outside code can't set it to an invalid value directly. private forces all changes through validated methods, preventing invalid states like a negative balance.

What is wrong with: public Car(String brand) { brand = brand; }

  • Nothing
  • It won't compile
  • It assigns the parameter to itself; the field stays unchanged — needs this.brand = brand
  • It creates two objects

Answer: It assigns the parameter to itself; the field stays unchanged — needs this.brand = brand. Without this., 'brand = brand' just reassigns the parameter; use this.brand = brand to set the field.

How is a static field different from an instance field?

  • Static fields are private
  • A static field is shared by all objects; an instance field belongs to each object
  • Instance fields are shared
  • There is no difference

Answer: A static field is shared by all objects; an instance field belongs to each object. Static members belong to the class and are shared by all instances; instance fields are per-object.

In the lesson, static int totalStudents is incremented in the constructor. After creating 3 students it is 3 — what is it after creating a 4th?

  • 1
  • 3
  • 0
  • 4

Answer: 4. The static counter is shared across all instances, so a 4th student makes it 4.

Why override the toString() method?

  • To make the object run faster
  • To give the object a human-readable printed form instead of Car@1b6d3586
  • To make fields private
  • To create a constructor

Answer: To give the object a human-readable printed form instead of Car@1b6d3586. Overriding toString lets println show meaningful text instead of a default Class@hashcode string.

How do you actually create an object from a Car class?

  • Car myCar;
  • Car myCar = Car();
  • Car myCar = new Car("Toyota", 2023);
  • new myCar.Car();

Answer: Car myCar = new Car("Toyota", 2023);. You must use new with a constructor: Car myCar = new Car(...); a bare declaration creates no object.

Continue this course