Java OOP Introduction

Object-oriented programming (OOP) is a way of structuring code around objects — self-contained units that bundle data (fields) together with the behavior (methods) that operates on that data. Instead of writing a long list of instructions that manipulate loose variables, you model your program as a collection of interacting objects, each responsible for its own state. Java was built from the ground up as an object-oriented language: almost everything you write, other than a few primitive types, lives inside a class. Understanding OOP is therefore not optional in Java — it is the foundation every other feature is built on.

Overview: What Is Object-Oriented Programming?

In procedural programming, you write functions that take data and transform it, and the data itself is usually kept separate from the logic that changes it. OOP flips this around: data and the functions that act on that data are packaged together into a single unit called an object. An object is created from a class, which acts as a blueprint or template describing what fields (data) and methods (behavior) every object of that type will have.

For example, a Car class might define that every car has a brand and a speed, and a method accelerate() that changes the speed. The class itself is not a car — it is a description of what a car looks like. Each time you use the new keyword, you create an instance of that class: an actual object living in memory, with its own independent copy of the fields.

Java’s OOP model rests on four core ideas, often called the four pillars:

  • Encapsulation — bundling data and the methods that operate on it inside a class, and hiding the internal details from outside code (typically using private fields with public getter/setter methods).
  • Inheritance — letting one class reuse and extend the fields and methods of another class, forming an “is-a” relationship (covered in a later lesson).
  • Polymorphism — allowing different classes to be treated through a common interface, so the same method call can behave differently depending on the actual object (also covered later).
  • Abstraction — exposing only the essential features of an object while hiding the complicated implementation details.

This lesson focuses on the foundation of all four: classes, objects, fields, methods, and constructors, plus your first taste of encapsulation.

How the JVM Handles Classes and Objects

Understanding what actually happens when you write object-oriented Java code makes the syntax feel far less arbitrary. When your program starts, the JVM’s class loader reads each compiled .class file and stores the class’s structure — its method bytecode, field definitions, and metadata — in an area of memory called the method area (part of what is often called Metaspace in modern JVMs). This happens once per class, no matter how many objects you create from it.

Every time you call new SomeClass(), the JVM allocates a fresh block of memory on the heap large enough to hold that object’s fields, initializes those fields to their default values (0, false, or null depending on type), and then runs the constructor to set up the object’s real starting state. The new expression returns a reference — essentially a memory address — which you typically store in a variable on the stack. That variable does not contain the object itself; it points to it. This is why assigning one object variable to another copies the reference, not the object, and why Java’s automatic garbage collector can safely reclaim heap memory once no reference anywhere in the program still points to it.

Syntax

The general shape of a class definition looks like this:

class ClassName {
    // fields (state)
    dataType fieldName;

    // constructor
    ClassName(parameters) {
        // initialize fields
    }

    // methods (behavior)
    returnType methodName(parameters) {
        // logic
    }
}

// creating and using an object
ClassName variableName = new ClassName(arguments);
variableName.fieldName;
variableName.methodName(arguments);
Part Purpose
class ClassName Declares the blueprint. By convention, class names start with an uppercase letter.
Fields Variables declared inside the class body (outside any method) that hold each object’s state.
Constructor A special method with the same name as the class and no return type, run automatically when new is used, used to set up initial field values.
Methods Functions defined inside the class that describe the object’s behavior; they can read and change the object’s fields.
new The keyword that allocates a new object on the heap and returns a reference to it.
. (dot operator) Used to access a field or call a method on a specific object reference.

Examples

Example 1: A Simple Class with Fields and a Method

public class Main {
    static class Car {
        String brand;
        int speed;

        void accelerate(int amount) {
            speed += amount;
            System.out.println(brand + " is now going " + speed + " mph.");
        }
    }

    public static void main(String[] args) {
        Car myCar = new Car();
        myCar.brand = "Toyota";
        myCar.speed = 0;
        myCar.accelerate(30);
        myCar.accelerate(25);
    }
}

Output:

Toyota is now going 30 mph.
Toyota is now going 55 mph.

Here Car is a blueprint with two fields, brand and speed. myCar is one object created from that blueprint. Calling accelerate() modifies that specific object’s speed field — if you created a second Car, it would have its own independent speed, completely unaffected by the first.

Example 2: Using a Constructor to Initialize Objects

Setting fields one by one after new is error-prone — you might forget one. A constructor lets you require the caller to supply the initial state up front.

public class Main {
    static class Dog {
        String name;
        String breed;

        Dog(String name, String breed) {
            this.name = name;
            this.breed = breed;
        }

        void bark() {
            System.out.println(name + " the " + breed + " says Woof!");
        }
    }

    public static void main(String[] args) {
        Dog d1 = new Dog("Rex", "German Shepherd");
        Dog d2 = new Dog("Bella", "Poodle");
        d1.bark();
        d2.bark();
    }
}

Output:

Rex the German Shepherd says Woof!
Bella the Poodle says Woof!

The constructor Dog(String name, String breed) runs automatically the moment new Dog(...) executes. Inside it, this.name = name; assigns the constructor’s parameter to the object’s field — this refers to “the object currently being constructed”, which is essential when a parameter has the same name as a field.

Example 3: Encapsulation with Private Fields

So far, any code that has a reference to an object can directly change its fields, even to invalid values. Encapsulation fixes this by making fields private (accessible only inside the class) and exposing controlled access through methods.

public class Main {
    static class BankAccount {
        private String owner;
        private double balance;

        BankAccount(String owner, double balance) {
            this.owner = owner;
            this.balance = balance;
        }

        void deposit(double amount) {
            if (amount > 0) {
                balance += amount;
            }
        }

        void withdraw(double amount) {
            if (amount > 0 && amount <= balance) {
                balance -= amount;
            } else {
                System.out.println("Withdrawal denied: insufficient funds.");
            }
        }

        double getBalance() {
            return balance;
        }

        String getOwner() {
            return owner;
        }
    }

    public static void main(String[] args) {
        BankAccount account = new BankAccount("Alice", 100.0);
        account.deposit(50.0);
        account.withdraw(30.0);
        account.withdraw(1000.0);
        System.out.println(account.getOwner() + "'s balance: $" + account.getBalance());
    }
}

Output:

Withdrawal denied: insufficient funds.
Alice's balance: $120.0

Because balance is private, no code outside BankAccount can set it directly — it can only be changed through deposit() and withdraw(), both of which validate the request first. The final withdrawal of 1000 is rejected because it exceeds the balance, so the account correctly ends at 120.0 (100 + 50 − 30).

How It Works Step by Step (Under the Hood)

  • 1. Class loading. The first time a class is used, the JVM loads its bytecode into the method area and verifies it.
  • 2. Allocation. When new BankAccount(...) runs, the JVM reserves a block of heap memory sized for the object’s fields (owner and balance).
  • 3. Default initialization. Before the constructor runs, fields are zeroed out: owner starts as null, balance starts as 0.0.
  • 4. Constructor execution. The constructor body runs, assigning the real starting values passed in by the caller.
  • 5. Reference returned. The address of the new object on the heap is returned and stored in the account variable, which lives on the stack of the main method.
  • 6. Method dispatch. Each call like account.deposit(50.0) uses the reference to locate the object on the heap, then executes the deposit bytecode from the method area against that object’s fields.
  • 7. Garbage collection. Once no variable anywhere references the object (for example, after main returns), it becomes eligible for the garbage collector to reclaim its heap memory.

Common Mistakes

Mistake 1: Leaving Fields Public

If fields are declared public, any code with a reference to the object can set them to invalid values, bypassing any validation logic you write:

public class Main {
    static class Account {
        public double balance;
    }

    public static void main(String[] args) {
        Account acc = new Account();
        acc.balance = -500;
        System.out.println("Balance: " + acc.balance);
    }
}

Output:

Balance: -500.0

Nothing stops balance from becoming negative, because there is no method guarding it. The fix is exactly what Example 3 showed: make the field private and only allow changes through methods like deposit()/withdraw() that can reject bad input.

Mistake 2: Forgetting this in a Constructor

When a constructor parameter has the same name as a field, omitting this causes the parameter to refer to itself instead of the field — the field is silently never set:

public class Main {
    static class Point {
        int x;
        int y;

        Point(int x, int y) {
            x = x;
            y = y;
        }
    }

    public static void main(String[] args) {
        Point p = new Point(5, 10);
        System.out.println(p.x + ", " + p.y);
    }
}

Output:

0, 0

This compiles without any error or warning, which makes it especially dangerous — the fields silently keep their default values of 0 instead of 5 and 10. Using this.x = x; and this.y = y; tells the compiler explicitly “assign to the object’s field”, producing the correct output of 5, 10.

Best Practices

  • Make fields private by default, and only expose what callers actually need through methods — this is the essence of encapsulation.
  • Give every class a clear, single responsibility; if a class is doing too many unrelated things, split it up.
  • Prefer constructors that require all mandatory fields as parameters, so an object can never exist in a half-initialized state.
  • Use this explicitly whenever a constructor or method parameter shares a name with a field, to avoid silent shadowing bugs.
  • Name classes with nouns (Car, BankAccount) and methods with verbs (accelerate, deposit) so code reads naturally.
  • Validate input inside setter-style methods rather than trusting the caller to pass sensible values.

Practice Exercises

  • Exercise 1: Write a Rectangle class with private width and height fields, a constructor, and a method getArea() that returns width * height. Create two rectangles and print both of their areas.
  • Exercise 2: Write a Student class with fields name and an int[] of test scores, plus a method averageScore() that returns the average as a double. Create one student with at least three scores and print the average.
  • Exercise 3: Take the BankAccount class from Example 3 and add a transfer(BankAccount other, double amount) method that withdraws from the current account and deposits into other, only if the withdrawal succeeds. Test it with two accounts.

Summary

  • OOP organizes programs as interacting objects, each combining data (fields) and behavior (methods).
  • A class is a blueprint; an object is a specific instance created with new.
  • Constructors initialize an object’s starting state automatically when it is created.
  • The JVM stores class definitions in the method area and allocates each object on the heap, accessed through references held in variables.
  • Encapsulation — using private fields with controlled public methods — protects an object’s internal state from invalid changes.
  • Forgetting this when a parameter shadows a field is a common, silent bug — always double-check constructors that assign fields.
  • The remaining OOP pillars, inheritance and polymorphism, build directly on the class and object fundamentals covered here.