Java Classes and Objects: A Complete Beginner's Guide

Master the building blocks of Object-Oriented Programming in Java by learning how to define custom classes, create objects, and implement encapsulation.

Introduction to Classes and Objects

Everything in Java is built around the concept of objects. An object is a self-contained entity that holds both state (data) and behavior (actions). When you write a Java application, you are essentially defining what objects your program needs, what data they hold, and what actions they can perform.

If objects are the actual "things" in your program, a class is the blueprint used to create them. You can think of a class like an architectural blueprint for a house. The blueprint itself is not a house—you cannot live in it. However, you can use that single blueprint to build many actual houses (objects), each with its own specific address, paint color, and furniture, but all sharing the same fundamental structure.

Why Object-Oriented Programming?

Modeling software as objects makes complex programs easier to understand, manage, and scale. Instead of having disconnected variables and methods scattered around, OOP groups related data and logic together. A Student object holds the student's name and grades, and also contains the methods to calculate their GPA. This organization mirrors how we naturally think about the real world.

Prerequisites

Before diving into classes and objects, make sure you are comfortable with:

  • Declaring and using variables (primitive and reference types)
  • Writing and calling basic static methods
  • Understanding the difference between the Stack and the Heap (introduced in the Variables tutorial)

What You Will Learn

  • How to define a custom Java class with fields and methods
  • The difference between instance variables and local variables
  • How to create objects using the new keyword
  • The role of constructors in object initialization
  • How to use the this keyword to resolve naming conflicts
  • The principle of encapsulation and how to implement it using access modifiers

State and Behavior

Every class you design should clearly define two things:

  • State (Attributes/Fields): What does an object of this class know about itself? For a Car class, the state might include color , make , model , and speed . In code, state is represented by instance variables.
  • Behavior (Methods/Operations): What can an object of this class do? A Car can accelerate() , brake() , and honk() . Behavior is represented by methods.

When you create an object from a class, that specific object gets its own unique copy of the instance variables (its own state), but it shares the method definitions (the behavior) with all other objects of that class.

Class Definition Syntax

Here is the general structure for defining a class in Java:

Java Syntax
public class ClassName {
    
    // 1. Fields (Instance Variables) - Define the state
    private DataType fieldName;

    // 2. Constructors - Initialize the state
    public ClassName(DataType parameterName) {
        this.fieldName = parameterName;
    }

    // 3. Methods - Define the behavior
    public ReturnType methodName() {
        // Method logic
    }
}

By convention, class names in Java use PascalCase (the first letter of every word is capitalized, e.g., BankAccount , UserService ).

Simple Example: Creating a Car Class

Let's create a simple Car class to see how state, behavior, and object creation work together.

Java
public class Car {
    
    // State (Instance Variables)
    private String make;
    private String model;
    private int year;

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

    // Behavior (Method)
    public void displayInfo() {
        System.out.println(this.year + " " + this.make + " " + this.model);
    }
}

Now, let's create a Main class to instantiate (create) some Car objects:

Java
public class Main {
    public static void main(String[] args) {
        // Creating objects using the 'new' keyword
        Car car1 = new Car("Toyota", "Corolla", 2022);
        Car car2 = new Car("Ford", "Mustang", 2023);

        // Calling behavior on the objects
        car1.displayInfo();
        car2.displayInfo();
    }
}
Output
2022 Toyota Corolla 2023 Ford Mustang

How It Works: Memory and References

When the line Car car1 = new Car(...); executes, two distinct things happen in memory:

  1. The new keyword: It asks the JVM to allocate enough memory on the Heap to hold a Car object (space for a String reference, another String reference, and an integer). It then initializes those fields using the constructor.
  2. The = operator: It stores the memory address of that new Heap object into the reference variable car1 , which lives on the Stack .

car1 is not the object itself; it is a remote control pointing to the object. If you write Car car3 = car1; , you do not create a second car. You simply create a second remote control ( car3 ) that points to the exact same Car object on the Heap that car1 points to.

Understanding Constructors

A constructor is a special block of code used to initialize a newly created object. It looks like a method but has two strict rules:

  1. It must have the exact same name as the class.
  2. It has no return type (not even void ).

The Default Constructor

If you do not write any constructor in your class, Java automatically provides a "no-argument" default constructor that does nothing. However, the moment you write any constructor (like the one in the Car class above), Java removes the default one.

Constructor Overloading

Just like methods , constructors can be overloaded. You can provide multiple ways to create an object.

Java
public class Book {
    private String title;
    private String author;
    private int pages;

    // Constructor 1: Full details
    public Book(String title, String author, int pages) {
        this.title = title;
        this.author = author;
        this.pages = pages;
    }

    // Constructor 2: Unknown page count
    public Book(String title, String author) {
        this.title = title;
        this.author = author;
        this.pages = 0; // Default value
    }

    // Constructor 3: No-argument constructor
    public Book() {
        this.title = "Unknown Title";
        this.author = "Unknown Author";
        this.pages = 0;
    }
}

The this() Constructor Call

You can call one constructor from another using this() as the first line of a constructor. For example, public Book() { this("Unknown", "Unknown", 0); } . This prevents you from duplicating initialization code across multiple constructors.

Real-World Example: Bank Account with Encapsulation

A core principle of OOP is Encapsulation : hiding the internal state of an object and requiring all interaction to be performed through an object's methods. This protects the data from being put into an invalid state (like a bank account balance dropping below zero).

Java
public class BankAccount {
    
    // State is hidden (private)
    private String accountNumber;
    private double balance;

    // Constructor to initialize the account
    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        
        // Enforce business rule: balance cannot start negative
        if (initialBalance >= 0) {
            this.balance = initialBalance;
        } else {
            System.out.println("Error: Initial balance cannot be negative. Set to 0.");
            this.balance = 0;
        }
    }

    // Behavior: Safe way to modify state
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.printf("Deposited: $%.2f. New Balance: $%.2f%n", amount, balance);
        } else {
            System.out.println("Error: Deposit amount must be positive.");
        }
    }

    public void withdraw(double amount) {
        if (amount <= 0) {
            System.out.println("Error: Withdrawal amount must be positive.");
        } else if (amount > balance) {
            System.out.println("Error: Insufficient funds.");
        } else {
            balance -= amount;
            System.out.printf("Withdrew: $%.2f. New Balance: $%.2f%n", amount, balance);
        }
    }

    // Read-only access to private data (Getters)
    public String getAccountNumber() { return accountNumber; }
    public double getBalance() { return balance; }
}

And here is how it is used safely:

Java
public class Main {
    public static void main(String[] args) {
        BankAccount myAccount = new BankAccount("123456789", 500.00);
        
        myAccount.deposit(150.50);
        myAccount.withdraw(100.00);
        myAccount.withdraw(600.00); // Tests insufficient funds
        
        // myAccount.balance = -1000; // COMPILE ERROR! balance is private.
    }
}
Output
Deposited: $150.50. New Balance: $650.50 Withdrew: $100.00. New Balance: $550.50 Error: Insufficient funds.

Common Mistakes to Avoid

Mistake 1: Forgetting the new Keyword

Beginners often try to create an object like they declare a primitive variable.

Java — Incorrect
Car myCar; // This only creates a reference, NOT an object!
myCar.displayInfo(); // NullPointerException at runtime
Java — Correct
Car myCar = new Car("Honda", "Civic", 2021); // Object is created on the heap

Mistake 2: Static Context Errors

You cannot directly access instance variables (like make ) or call instance methods from a static method (like main ) without first creating an object.

Java — Incorrect
public static void main(String[] args) {
    System.out.println(make); // Compile error: non-static variable make cannot be referenced from a static context
}

Mistake 3: Creating Multiple Classes in One File Improperly

In Java, a file can only have one public class, and the file name must exactly match that public class name (e.g., Car.java for public class Car ). You can have other non-public classes in the same file, but for beginners, it is highly recommended to put every class in its own separate .java file to avoid confusion.

Best Practices for Classes and Objects

  1. Enforce Encapsulation: Always make your instance variables private . Expose them only through public "getter" and "setter" methods if external code needs to read or modify them.
  2. Design for Single Responsibility: A class should have one, and only one, reason to change. A Student class should manage student data. It should not contain logic for saving to a database—that belongs in a StudentRepository class.
  3. Use Constructors for Mandatory Data: If an object cannot logically exist without certain data (like a BankAccount without an account number), require that data in the constructor. Do not rely on setters to set mandatory data later.
  4. Prefer Immutability When Possible: If an object's state shouldn't change after creation (like a Month or Color object), make all fields private final and don't provide setters. In Java 14+, you can use the record keyword for this.

Performance Considerations

While modern JVMs are incredibly efficient at creating and destroying objects, keeping memory behavior in mind helps write scalable applications:

  • Object Creation Overhead: Allocating memory on the heap takes longer than allocating primitives on the stack. Avoid creating unnecessary objects inside tight loops.
  • Memory Footprint: Every object in Java has a header (usually 12-16 bytes) overhead, plus padding. A simple object holding one boolean might actually take up 16 bytes of memory. If you need to process millions of data points, consider using primitive arrays instead of collections of objects.
Java — Avoid this in tight loops
for (int i = 0; i < 1000000; i++) {
    // Creating a new DecimalFormat object 1 million times is wasteful
    DecimalFormat df = new DecimalFormat("#.##");
    System.out.println(df.format(someValue));
}

// BETTER: Create it once outside the loop
DecimalFormat df = new DecimalFormat("#.##");
for (int i = 0; i < 1000000; i++) {
    System.out.println(df.format(someValue));
}

Exercises

Exercise 1: The Rectangle Class

Create a Rectangle class with width and height as private double fields. Include a constructor, a method named getArea() that returns the area, and a method named getPerimeter() that returns the perimeter. Instantiate a rectangle in main and print its area and perimeter.

Exercise 2: Enhanced Student Class

Create a Student class with name and grades (an array or ArrayList of integers). Provide a method addGrade(int grade) that adds a grade to the list, and a method getAverageGrade() that calculates and returns the average as a double. Ensure grades cannot be added if they are outside the 0-100 range.

Solutions

Solution to Exercise 1: The Rectangle Class

Java
public class Rectangle {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        // Validate inputs to prevent negative dimensions
        if (width > 0 && height > 0) {
            this.width = width;
            this.height = height;
        } else {
            throw new IllegalArgumentException("Dimensions must be positive.");
        }
    }

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

    public double getPerimeter() {
        return 2 * (width + height);
    }

    public static void main(String[] args) {
        Rectangle room = new Rectangle(5.5, 3.2);
        System.out.printf("Area: %.2f%n", room.getArea());
        System.out.printf("Perimeter: %.2f%n", room.getPerimeter());
    }
}
Output
Area: 17.60 Perimeter: 17.40

Explanation: Notice how the constructor actively protects the object's state by throwing an exception if invalid data is passed. This is much better than allowing a negative width and letting the program calculate nonsensical results later.

Solution to Exercise 2: Enhanced Student Class

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

public class Student {
    private String name;
    private List<Integer> grades;

    public Student(String name) {
        this.name = name;
        this.grades = new ArrayList<>();
    }

    public void addGrade(int grade) {
        if (grade >= 0 && grade <= 100) {
            grades.add(grade);
        } else {
            System.out.println("Invalid grade: " + grade + ". Must be 0-100.");
        }
    }

    public double getAverageGrade() {
        if (grades.isEmpty()) {
            return 0.0;
        }
        int sum = 0;
        for (int grade : grades) {
            sum += grade;
        }
        return (double) sum / grades.size();
    }

    public String getName() { return name; }

    public static void main(String[] args) {
        Student alice = new Student("Alice");
        alice.addGrade(85);
        alice.addGrade(105); // Will be rejected
        alice.addGrade(92);
        alice.addGrade(78);

        System.out.println(alice.getName() + "'s average: " + 
                           String.format("%.2f", alice.getAverageGrade()));
    }
}
Output
Invalid grade: 105. Must be 0-100. Alice's average: 85.00

Explanation: The addGrade method encapsulates the business rule that grades must be between 0 and 100. The getAverageGrade method safely handles the edge case where no grades have been added yet to prevent a division-by-zero error.

Summary

  • A class is a blueprint defining state (fields) and behavior (methods).
  • An object is a specific instance of a class allocated in the Heap memory.
  • The new keyword allocates memory and invokes a constructor.
  • Constructors initialize the object's state and can be overloaded to provide multiple ways to create an object.
  • The this keyword refers to the current object and is used to differentiate between instance variables and parameters.
  • Encapsulation (hiding fields with private and exposing them via methods) protects the integrity of an object's data.

Frequently Asked Questions

A class is a blueprint or template that defines the structure (fields) and capabilities (methods) of something. An object is a concrete, living instance created from that blueprint in memory. For example, String is a class, but "Hello World" is an object of the String class.

The new keyword tells the Java Virtual Machine to allocate memory on the Heap for a new object. It invokes the class's constructor to set up the initial state of that object, and finally, it returns a reference (memory address) to that object so you can assign it to a variable.

The this keyword is a reference variable that points to the current object—the specific instance whose method or constructor is currently executing. It is most commonly used to resolve shadowing, such as when a constructor parameter has the same name as an instance field (e.g., this.name = name; ).

Objects themselves are stored in the Heap , which is a large pool of memory shared across all threads in your application. However, the variables that "point" to those objects (the references) are stored on the Stack , alongside local primitive variables and method call data.

Yes, syntactically, Java allows an empty class (e.g., class Empty {} ). However, an object created from an empty class has no state and no behavior, making it practically useless. In modern Java, if you just need to pass a few pieces of immutable data around, you should use a record instead of a manually written empty class.