Java Classes and Objects

A class is a blueprint that describes what data an object holds and what it can do, while an object is a concrete instance of that blueprint created while your program runs. Almost everything in Java is organized around classes and objects — understanding them well is the foundation for everything else in object-oriented programming, from encapsulation to inheritance to polymorphism.

Overview: How Classes and Objects Work

A class does not itself hold any data. It is a template that defines fields (the data an object will carry) and methods (the behavior an object can perform). Nothing actually exists in memory until you use the new keyword to create an object from that class — this process is called instantiation.

When you write Car myCar = new Car("Toyota", "Corolla", 2022);, several things happen. The Java Virtual Machine (JVM) first makes sure the Car class has been loaded into memory (class loading happens once per class, not once per object). It then allocates a block of memory on the heap large enough to hold all of the object’s instance fields. The constructor runs and initializes those fields, and finally the new expression evaluates to a reference — essentially an address — pointing at that block of memory. That reference is what gets stored in the variable myCar.

This distinction between the object (on the heap) and the variable (a reference to it, usually stored on the stack for local variables) matters a lot in practice. When you assign one object variable to another, like Car anotherCar = myCar;, you are not copying the car’s data — you are copying the reference. Both variables now point at the exact same object, so a change made through one variable is visible through the other. Objects are only truly duplicated if you write code that explicitly copies their field values into a new object.

Each object gets its own independent copy of the class’s instance fields (also called instance variables). If you create three Car objects, each one has its own make, model, and odometer stored separately in memory. Methods, on the other hand, are not duplicated per object — the JVM stores one copy of the method’s bytecode and simply runs it with a hidden reference to whichever object it was called on (accessible inside the method as this).

A well-designed class also practices encapsulation: it keeps its fields private so outside code cannot reach in and corrupt its data directly, and it exposes controlled access through public methods (often called getters and setters, or more meaningful behavior methods like deposit() and withdraw()). This lets the class enforce its own rules — for example, refusing to let a bank balance go negative.

Syntax

class ClassName {
    // fields (instance variables)
    accessModifier type fieldName;

    // constructor(s)
    ClassName(parameters) {
        // initialize fields, usually with 'this'
    }

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

// creating an object from the class
ClassName variableName = new ClassName(arguments);
Part Meaning
class ClassName Declares a new blueprint. By convention, class names start with an uppercase letter.
Fields Variables declared directly inside the class body (outside any method) that store an object’s state.
Constructor A special method with the same name as the class and no return type, used to initialize a new object’s fields.
this A reference to the current object, most often used to distinguish a field from a parameter that shares its name.
Methods Functions defined inside the class that describe the object’s behavior; they can read and modify its fields.
new The operator that allocates memory for a new object and calls its constructor, returning a reference to the object.
Access modifiers private, public, or (no modifier) package-private, controlling who can see a field or method.

Examples

Example 1: A basic class with fields, a constructor, and methods

public class Main {
    public static void main(String[] args) {
        Car car1 = new Car("Toyota", "Corolla", 2022);
        Car car2 = new Car("Honda", "Civic", 2023);
        car1.displayInfo();
        car2.displayInfo();
        car1.drive(150);
        car1.displayInfo();
    }
}

class Car {
    String make;
    String model;
    int year;
    int odometer;

    Car(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
        this.odometer = 0;
    }

    void drive(int miles) {
        odometer += miles;
    }

    void displayInfo() {
        System.out.println(year + " " + make + " " + model + " - Odometer: " + odometer + " miles");
    }
}

Output:

2022 Toyota Corolla - Odometer: 0 miles
2023 Honda Civic - Odometer: 0 miles
2022 Toyota Corolla - Odometer: 150 miles

Two independent Car objects are created, each with its own make, model, and odometer. Calling car1.drive(150) changes only car1‘s odometer — car2 is completely unaffected, which demonstrates that each object owns a separate copy of the instance fields.

Example 2: Encapsulation and constructor overloading

public class Main {
    public static void main(String[] args) {
        BankAccount acc1 = new BankAccount("Alice", 500.0);
        BankAccount acc2 = new BankAccount("Bob");

        acc1.deposit(150.0);
        acc1.withdraw(200.0);
        acc2.deposit(50.0);

        System.out.println(acc1);
        System.out.println(acc2);

        acc1.withdraw(10000.0);
    }
}

class BankAccount {
    private String owner;
    private double balance;

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

    BankAccount(String owner) {
        this(owner, 0.0);
    }

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

    void withdraw(double amount) {
        if (amount > balance) {
            System.out.println("Insufficient funds for " + owner);
        } else {
            balance -= amount;
        }
    }

    public String toString() {
        return owner + "'s balance: $" + balance;
    }
}

Output:

Alice's balance: $450.0
Bob's balance: $50.0
Insufficient funds for Alice

The balance field is private, so outside code cannot set it directly — it can only be changed through deposit() and withdraw(), which enforce rules like rejecting negative deposits and overdrafts. The second constructor uses this(owner, 0.0) to reuse the first constructor, avoiding duplicated initialization logic. Overriding toString() lets System.out.println(acc1) print a readable summary instead of a cryptic memory address.

Example 3: Working with an array of objects

public class Main {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student("Maria", 92.5);
        students[1] = new Student("James", 78.0);
        students[2] = new Student("Wei", 88.25);

        double total = 0;
        for (Student s : students) {
            s.printReport();
            total += s.getGrade();
        }

        double average = total / students.length;
        System.out.println("Class average: " + average);
    }
}

class Student {
    private String name;
    private double grade;

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

    double getGrade() {
        return grade;
    }

    void printReport() {
        String status = grade >= 60 ? "PASS" : "FAIL";
        System.out.println(name + ": " + grade + " (" + status + ")");
    }
}

Output:

Maria: 92.5 (PASS)
James: 78.0 (PASS)
Wei: 88.25 (PASS)
Class average: 86.25

This example shows objects stored in an array, just like any other type. Each Student object is independent, and the loop calls getGrade() on each one to accumulate a total, showing how objects are commonly grouped and processed in real programs.

Under the Hood: What Happens When You Create an Object

When the JVM executes a new expression, it performs roughly these steps:

  • Class loading: if the class has not been used yet, the JVM’s class loader reads the compiled .class file, verifies its bytecode, and prepares any static fields (this happens only once per class, no matter how many objects you create).
  • Memory allocation: the JVM reserves a contiguous block of memory on the heap sized to hold all of the object’s instance fields, plus some internal bookkeeping (like a pointer to the object’s class metadata, used for things such as instanceof checks).
  • Default initialization: before your constructor runs, every field is set to its default value — 0 for numeric types, false for boolean, and null for object references.
  • Constructor execution: the matching constructor runs, typically overwriting the defaults with the values you passed in.
  • Reference returned: the new expression evaluates to a reference to the freshly built object, which you usually store in a variable.

Objects live on the heap until the JVM’s garbage collector determines that nothing references them anymore, at which point their memory is reclaimed automatically — you never manually free memory in Java the way you would in C.

Common Mistakes

Mistake 1: Calling an instance method as if it were static

Instance methods belong to an object, not to the class itself. You cannot call one without first creating an object.

class Dog {
    String name;

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

public class Main {
    public static void main(String[] args) {
        bark(); // ERROR: non-static method bark() cannot be referenced from a static context
    }
}

The main method is static, meaning it runs without any object existing yet. Calling bark() directly fails to compile because the compiler has no object to run it on. The fix is to create a Dog object first, then call the method on that object:

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.name = "Rex";
        myDog.bark();
    }
}

class Dog {
    String name;

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

Mistake 2: Using an object reference that was never assigned

Declaring a field of an object type does not create an object — it only creates a reference, which defaults to null until you assign it with new.

class Garage {
    Car car; // just a reference, currently null - no Car object exists yet

    void start() {
        car.drive(10); // NullPointerException: car has never been assigned an object
    }
}

Because car was declared but never set to a new Car(...), it still holds null. Calling a method on it throws a NullPointerException at runtime. The fix is to instantiate the field, either in a constructor or before it’s used:

class Garage {
    Car car;

    Garage() {
        car = new Car("Ford", "Focus", 2020);
    }

    void start() {
        car.drive(10);
    }
}

Best Practices

  • Keep fields private and expose behavior through methods, so the class can validate and control how its state changes.
  • Give constructors everything they need to leave the object in a valid, ready-to-use state — avoid objects that require several setup calls before they work correctly.
  • Use this to disambiguate a field from a parameter of the same name inside a constructor or setter.
  • Name classes as singular nouns (Car, not Cars) since each instance represents one thing.
  • Override toString() on classes you plan to print or log, so debugging output is meaningful instead of a memory address like Car@1b6d3586.
  • Remember that assigning one object variable to another copies the reference, not the object — write an explicit copy method if you actually need a duplicate.
  • Favor small, focused classes with a clear single responsibility over large classes that try to do everything.

Practice Exercises

  • Exercise 1: Write a Rectangle class with private width and height fields, a constructor, and a method area() that returns width * height. Create two rectangles in main and print both of their areas.
  • Exercise 2: Write a Book class with fields for title, author, and a boolean checkedOut. Add methods checkOut() and returnBook() that update checkedOut and print a message; have checkOut() print a warning instead if the book is already checked out.
  • Exercise 3: Create a Temperature class that stores a value in Celsius and has a method toFahrenheit() returning the Fahrenheit equivalent (celsius * 9 / 5 + 32). Build an array of three Temperature objects with different values and print each one’s Fahrenheit conversion.

Summary

  • A class is a blueprint defining fields (data) and methods (behavior); an object is a concrete instance of that blueprint created with new.
  • Objects live on the heap; variables hold references to them, so assigning one object variable to another copies the reference, not the data.
  • Each object has its own independent copy of instance fields, but shares the same method code defined in the class.
  • Constructors initialize new objects and can be overloaded to offer multiple ways to create an object.
  • Encapsulation — keeping fields private and exposing controlled methods — protects an object’s internal consistency.
  • Calling instance methods requires an object; uninitialized object references default to null and cause a NullPointerException if used before assignment.