Home Java Java Classes and Objects Explained — Build Real OOP Programs From Scratch

Java Classes and Objects Explained — Build Real OOP Programs From Scratch

In Plain English 🔥
A class is a blueprint — like the architectural plan for a house. The plan itself isn't a house you can live in, but you can use it to build as many houses as you want. Each house you build from that plan is an object. Every house has the same structure (rooms, doors, windows) defined by the plan, but each one has its own address, its own paint colour, its own residents. That's exactly how Java classes and objects work.
⚡ Quick Answer
A class is a blueprint — like the architectural plan for a house. The plan itself isn't a house you can live in, but you can use it to build as many houses as you want. Each house you build from that plan is an object. Every house has the same structure (rooms, doors, windows) defined by the plan, but each one has its own address, its own paint colour, its own residents. That's exactly how Java classes and objects work.

Every real-world application you've ever used — a banking app, a game, a ride-sharing platform — is built around things that have properties and behaviours. A bank account has a balance and an owner; it can accept deposits and process withdrawals. A car has a make, model and speed; it can accelerate and brake. Java's class system exists precisely to let you model these real-world things in code, cleanly and predictably. This is the foundation of Object-Oriented Programming, and it's why Java powers everything from Android apps to enterprise banking systems.

Before OOP existed, large programs were written as one long sequence of instructions. The bigger the program got, the messier and harder to maintain it became — like writing a novel with no chapters, no characters, just one endless paragraph. Classes solve this by letting you group related data and behaviour together into a single, reusable unit. Instead of scattered variables and functions floating around your codebase, you have a tidy, self-contained object that knows what it is and what it can do.

By the end of this article you'll know how to define a class with fields, a constructor, and methods; how to create objects from that class and work with them; and you'll understand the difference between a class and an object at a level that will never confuse you again — including in job interviews.

What a Class Actually Is — The Blueprint Explained

A class is a template you write once that describes two things: the data a thing can hold (called fields or instance variables) and the actions it can perform (called methods). You're not creating anything real yet — just defining the shape of something that could exist.

Think of a cookie cutter. The cutter is the class. It defines the shape — a star, a circle, whatever. But it isn't a cookie. Every time you press the cutter into dough, you get a new cookie — a new object — that has the shape defined by the cutter.

In Java, you define a class using the class keyword. The class name should be a noun (because a class represents a thing) and it should start with a capital letter — that's a hard Java convention. Everything that belongs to the class lives inside its curly braces.

Fields hold the state of an object. Every object created from the same class gets its own private copy of those fields. Change the colour of one cookie — it doesn't affect any other cookie. That independence is what makes objects so powerful and predictable.

BankAccount.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
// A class is a blueprint — we're describing what a BankAccount looks like
// This file defines the class but doesn't create any account yet
public class BankAccount {

    // --- FIELDS (instance variables) ---
    // These describe the STATE of a BankAccount object
    // Every BankAccount object will have its OWN copy of these
    String accountHolderName;  // who owns this account
    String accountNumber;      // unique identifier for the account
    double balance;            // how much money is currently in it

    // --- CONSTRUCTOR ---
    // A constructor is a special method that runs automatically when
    // you create a new object. Its job is to set the initial state.
    // Notice: same name as the class, no return type
    public BankAccount(String holderName, String number, double openingBalance) {
        accountHolderName = holderName;   // store the name we were given
        accountNumber     = number;        // store the account number
        balance           = openingBalance; // set the starting balance
    }

    // --- METHODS ---
    // These describe the BEHAVIOUR a BankAccount can perform

    // Deposit money into the account
    public void deposit(double amount) {
        if (amount <= 0) {
            System.out.println("Deposit amount must be positive.");
            return; // stop here — don't proceed with bad input
        }
        balance = balance + amount; // increase the balance
        System.out.println("Deposited $" + amount + " — New balance: $" + balance);
    }

    // Withdraw money from the account
    public void withdraw(double amount) {
        if (amount > balance) {
            System.out.println("Insufficient funds. Current balance: $" + balance);
            return; // stop — can't withdraw more than available
        }
        balance = balance - amount; // reduce the balance
        System.out.println("Withdrew $" + amount + " — New balance: $" + balance);
    }

    // Print a summary of this account's current state
    public void printStatement() {
        System.out.println("--- Account Statement ---");
        System.out.println("Holder : " + accountHolderName);
        System.out.println("Number : " + accountNumber);
        System.out.println("Balance: $" + balance);
        System.out.println("-------------------------");
    }

    // main method — the entry point Java uses to run the program
    public static void main(String[] args) {

        // Creating two OBJECTS (instances) from the same BankAccount class
        // Each object is completely independent — its own data, its own state
        BankAccount alicesAccount = new BankAccount("Alice Chen", "ACC-1001", 500.00);
        BankAccount bobsAccount   = new BankAccount("Bob Martins", "ACC-1002", 1200.00);

        // Working with Alice's account
        alicesAccount.deposit(250.00);   // Alice deposits $250
        alicesAccount.withdraw(100.00);  // Alice withdraws $100
        alicesAccount.printStatement();  // print Alice's current state

        System.out.println(); // blank line for readability

        // Working with Bob's account — completely separate from Alice's
        bobsAccount.withdraw(400.00);    // Bob withdraws $400
        bobsAccount.printStatement();    // print Bob's current state
    }
}
▶ Output
Deposited $250.0 — New balance: $750.0
Withdrew $100.0 — New balance: $650.0
--- Account Statement ---
Holder : Alice Chen
Number : ACC-1001
Balance: $650.0
-------------------------

Withdrew $400.0 — New balance: $800.0
--- Account Statement ---
Holder : Bob Martins
Number : ACC-1002
Balance: $800.0
-------------------------
⚠️
Pro Tip: One Class Per FileIn Java, the public class name must exactly match the filename — including capitalisation. If your class is named `BankAccount`, the file must be `BankAccount.java`. Get this wrong and the compiler will refuse to run it with a 'class BankAccount is public, should be declared in a file named BankAccount.java' error.

Objects, the `new` Keyword, and Why Each Instance Is Independent

A class sitting in a file does nothing on its own — it's just a plan on paper. To get something you can actually work with, you have to instantiate it. That's a fancy word for 'create an object from the class', and you do it with the new keyword.

When Java sees new BankAccount(...), it does three things in quick succession: it allocates a fresh chunk of memory to hold this object's data, it runs the constructor to fill in the initial values, and it hands you back a reference — think of it like a postal address — that points to where this object lives in memory.

That reference is stored in a variable. When you write BankAccount alicesAccount = new BankAccount(...), the variable alicesAccount doesn't contain the object itself — it contains the address of the object. This is a crucial distinction we'll revisit in the gotchas section.

Every object you create is completely independent. Changing Alice's balance has zero effect on Bob's balance because they each have their own copy of the balance field in their own region of memory. This is exactly why objects are so useful — you can have hundreds of bank accounts in one program, each managing its own state, without them tangling with each other.

CarShowroom.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// Let's model a Car to reinforce how multiple objects stay independent
public class CarShowroom {

    // --- The Car class blueprint ---
    // (In a real project this would be its own file — Car.java)
    static class Car {
        String make;    // e.g. "Toyota"
        String model;   // e.g. "Corolla"
        String colour;  // e.g. "Red"
        int    currentSpeedKph; // how fast it's going right now

        // Constructor — sets up a car's initial state when it's created
        public Car(String make, String model, String colour) {
            this.make   = make;   // 'this.make' = the field; 'make' = the parameter
            this.model  = model;
            this.colour = colour;
            this.currentSpeedKph = 0; // every new car starts stationary
        }

        // Accelerate the car by a given amount
        public void accelerate(int kph) {
            currentSpeedKph += kph; // += means 'add kph to current speed'
            System.out.println(colour + " " + make + " " + model
                + " accelerates to " + currentSpeedKph + " kph");
        }

        // Brake the car — slow it down
        public void brake(int kph) {
            currentSpeedKph -= kph; // reduce speed
            if (currentSpeedKph < 0) currentSpeedKph = 0; // can't go below zero
            System.out.println(colour + " " + make + " " + model
                + " slows to " + currentSpeedKph + " kph");
        }
    }

    public static void main(String[] args) {

        // 'new' creates a fresh Car object and returns a reference to it
        Car redToyota  = new Car("Toyota",  "Corolla", "Red");
        Car blueFord   = new Car("Ford",    "Focus",   "Blue");
        Car greenHonda = new Car("Honda",   "Civic",   "Green");

        // Each car manages its own speed independently
        redToyota.accelerate(60);  // red Toyota speeds up
        blueFord.accelerate(80);   // blue Ford speeds up — red Toyota unaffected
        redToyota.accelerate(20);  // red Toyota goes faster again
        greenHonda.accelerate(50); // green Honda joins in
        blueFord.brake(30);        // blue Ford slows down — others unaffected

        // Let's confirm the final speeds of each object
        System.out.println("\n--- Final Speeds ---");
        System.out.println("Red Toyota  : " + redToyota.currentSpeedKph  + " kph");
        System.out.println("Blue Ford   : " + blueFord.currentSpeedKph   + " kph");
        System.out.println("Green Honda : " + greenHonda.currentSpeedKph + " kph");
    }
}
▶ Output
Red Toyota Corolla accelerates to 60 kph
Blue Ford Focus accelerates to 80 kph
Red Toyota Corolla accelerates to 80 kph
Green Honda Civic accelerates to 50 kph
Blue Ford Focus slows to 50 kph

--- Final Speeds ---
Red Toyota : 80 kph
Blue Ford : 50 kph
Green Honda : 50 kph
🔥
What Does 'this' Mean?Inside a constructor or method, `this` refers to the current object — the specific instance the method is running on. You need it when a parameter has the same name as a field: `this.make = make` means 'set MY field called make to the value of the parameter called make'. Without `this`, Java would think you're assigning the parameter to itself — a silent bug.

Constructors — Giving Your Objects a Proper Starting State

A constructor is the first code that runs when an object is born. Its entire job is to make sure the object starts life in a valid, usable state — not half-initialised with random default values.

Constructors look like methods but with two important differences: they have no return type (not even void), and they must have exactly the same name as the class. Java knows it's a constructor because of these two rules.

You can have multiple constructors in one class — each accepting different parameters. Java figures out which one to call based on what you pass to new. This is called constructor overloading, and it's incredibly practical. Sometimes you want to create a full account with all details upfront; other times you just know the holder's name. Both scenarios can be handled cleanly without duplicating code.

If you write no constructor at all, Java silently provides a no-argument default constructor that sets all fields to their default values (0 for numbers, null for objects, false for booleans). The moment you write even one constructor yourself, Java removes that free default — something that trips up a lot of beginners.

ProductCatalog.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
// Demonstrates constructor overloading — multiple constructors in one class
public class ProductCatalog {

    static class Product {
        String name;
        double price;
        int    stockQuantity;
        String category;

        // Constructor 1: Full details — use this when you have everything upfront
        public Product(String name, double price, int stockQuantity, String category) {
            this.name          = name;
            this.price         = price;
            this.stockQuantity = stockQuantity;
            this.category      = category;
        }

        // Constructor 2: Price and name only — stock defaults to 0, category to "Uncategorised"
        // Useful when you're adding new products that haven't arrived in stock yet
        public Product(String name, double price) {
            this.name          = name;
            this.price         = price;
            this.stockQuantity = 0;               // sensible default
            this.category      = "Uncategorised"; // sensible default
        }

        // Method to restock — adds more units to current stock
        public void restock(int unitsAdded) {
            stockQuantity += unitsAdded;
            System.out.println(name + " restocked. New quantity: " + stockQuantity);
        }

        // Method to display product details in a readable format
        public void displayDetails() {
            System.out.println("Product  : " + name);
            System.out.println("Category : " + category);
            System.out.println("Price    : $" + price);
            System.out.println("In Stock : " + stockQuantity + " units");
            System.out.println("---");
        }
    }

    public static void main(String[] args) {

        // Using Constructor 1 — all four arguments provided
        Product laptop = new Product("ThinkPad X1", 1299.99, 15, "Electronics");

        // Using Constructor 2 — only name and price; stock and category use defaults
        Product newArrival = new Product("Wireless Keyboard", 49.99);

        laptop.displayDetails();
        newArrival.displayDetails();

        // The new arrival just came in — let's update its stock
        newArrival.restock(50);
        System.out.println();
        newArrival.displayDetails(); // display again to see updated stock
    }
}
▶ Output
Product : ThinkPad X1
Category : Electronics
Price : $1299.99
In Stock : 15 units
---
Product : Wireless Keyboard
Category : Uncategorised
Price : $49.99
In Stock : 0 units
---
Wireless Keyboard restocked. New quantity: 50

Product : Wireless Keyboard
Category : Uncategorised
Price : $49.99
In Stock : 50 units
---
⚠️
Watch Out: Adding a Constructor Removes the Free DefaultThe moment you define any constructor, Java's automatic no-arg constructor disappears. If you later try `new Product()` with no arguments, you'll get a compile error: 'constructor Product() is not applicable'. Fix it by explicitly adding a no-arg constructor if you still need one.

Putting It All Together — A Complete OOP Mini-Program

You now have all the pieces: a class defines the blueprint, fields hold the state, a constructor sets the initial state, and methods define the behaviour. Let's pull all of this together into a program that feels like real software — a simple student grade tracker.

This example introduces one more important concept: access modifiers. Notice the fields are marked private and the methods are public. This is called encapsulation — hiding the internal data and only letting the outside world interact with it through controlled methods. It's why you can't just reach into a bank app and edit your own balance. We keep fields private and expose getters and setters where appropriate. This is the correct, professional way to write Java classes.

Even though encapsulation is technically a deeper topic, you'll see it in literally every real Java codebase, so it's important to see it from the start and not be confused when you encounter it. The rule of thumb: fields are almost always private. Methods that should be usable from outside the class are public.

StudentGradeTracker.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
public class StudentGradeTracker {

    // The Student class — a self-contained blueprint for a student record
    static class Student {

        // PRIVATE fields — only code inside this class can touch them directly
        // This is encapsulation: protect the data, control how it's accessed
        private String studentName;
        private int    studentId;
        private double[] examScores;  // array to hold multiple exam scores
        private int    scoresRecorded; // how many scores have been added so far

        // Constructor — sets up the student with a name, ID and exam capacity
        public Student(String name, int id, int maxExams) {
            this.studentName    = name;
            this.studentId      = id;
            this.examScores     = new double[maxExams]; // reserve space for scores
            this.scoresRecorded = 0; // no exams taken yet
        }

        // PUBLIC method — adds an exam score to this student's record
        public void addExamScore(double score) {
            if (scoresRecorded >= examScores.length) {
                System.out.println("Cannot add more scores — all exam slots are full.");
                return;
            }
            if (score < 0 || score > 100) {
                System.out.println("Invalid score. Must be between 0 and 100.");
                return;
            }
            examScores[scoresRecorded] = score; // store the score at the next open slot
            scoresRecorded++;                    // move the slot counter forward
            System.out.println("Score " + score + " recorded for " + studentName);
        }

        // Calculates and returns the average of all recorded scores
        public double calculateAverage() {
            if (scoresRecorded == 0) return 0.0; // avoid dividing by zero
            double total = 0;
            for (int i = 0; i < scoresRecorded; i++) {
                total += examScores[i]; // add each score to the running total
            }
            return total / scoresRecorded; // average = total divided by count
        }

        // Converts a numeric average into a letter grade
        public String getLetterGrade() {
            double average = calculateAverage();
            if (average >= 90) return "A";
            if (average >= 80) return "B";
            if (average >= 70) return "C";
            if (average >= 60) return "D";
            return "F";
        }

        // Getter — the only approved way for outside code to read the student's name
        public String getStudentName() {
            return studentName;
        }

        // Prints a full, formatted report for this student
        public void printReport() {
            System.out.println("=============================");
            System.out.println("Student  : " + studentName);
            System.out.println("ID       : " + studentId);
            System.out.printf ("Average  : %.1f%%\n", calculateAverage()); // 1 decimal place
            System.out.println("Grade    : " + getLetterGrade());
            System.out.println("Exams    : " + scoresRecorded + " recorded");
            System.out.println("=============================");
        }
    }

    // Main method — where execution starts
    public static void main(String[] args) {

        // Create two student objects — completely independent
        Student priya = new Student("Priya Sharma",  1001, 4);
        Student james = new Student("James Okafor",  1002, 4);

        // Add exam scores for Priya
        priya.addExamScore(88.5);
        priya.addExamScore(92.0);
        priya.addExamScore(79.5);
        priya.addExamScore(95.0);

        // Add exam scores for James
        james.addExamScore(74.0);
        james.addExamScore(68.5);
        james.addExamScore(71.0);

        // Attempt to add a 5th score to Priya's record (max is 4 — should warn us)
        priya.addExamScore(85.0);

        System.out.println(); // blank line before the reports

        // Print both student reports
        priya.printReport();
        james.printReport();
    }
}
▶ Output
Score 88.5 recorded for Priya Sharma
Score 92.0 recorded for Priya Sharma
Score 79.5 recorded for Priya Sharma
Score 95.0 recorded for Priya Sharma
Score 74.0 recorded for James Okafor
Score 68.5 recorded for James Okafor
Score 71.0 recorded for James Okafor
Cannot add more scores — all exam slots are full.

=============================
Student : Priya Sharma
ID : 1001
Average : 88.8%
Grade : B
Exams : 4 recorded
=============================
=============================
Student : James Okafor
ID : 1002
Average : 71.2%
Grade : C
Exams : 3 recorded
=============================
🔥
Interview Gold: Why Make Fields Private?Making fields private prevents outside code from putting objects into an impossible state — like setting a student's score to 9999 or a bank balance to a negative number with no withdrawal logic. Your methods become the controlled gateway. Interviewers love this answer because it shows you understand encapsulation, not just its syntax.
AspectClassObject
What it isA blueprint / template defined once in codeA real instance created from that blueprint at runtime
Exists inYour source code file (.java)Memory (heap) while the program is running
Created withThe `class` keywordThe `new` keyword
How many?Usually one definition per conceptUnlimited — create as many as you need
Holds data?Defines what data will exist (field declarations)Holds its own actual values for those fields
Memory usageNo memory allocated for data yetEach object occupies its own memory on the heap
AnalogyThe cookie-cutterEach individual cookie made with that cutter

🎯 Key Takeaways

  • A class is a blueprint — it defines structure and behaviour but creates nothing until new is called. The class is the plan; the object is the built house.
  • Every object created with new gets its own independent copy of all instance fields. Changing one object's data never automatically changes another object's data.
  • Constructors run once at object creation to guarantee a valid starting state — they have no return type and must match the class name exactly.
  • Make fields private and expose behaviour through public methods — this is encapsulation, and it's what separates professional Java code from beginner code.

⚠ Common Mistakes to Avoid

  • Mistake 1: Forgetting new and treating the reference variable as the object — Writing BankAccount myAccount; declares a variable but it's null — it points to nothing. Calling myAccount.deposit(100) throws a NullPointerException at runtime. Fix: always initialise with new: BankAccount myAccount = new BankAccount(...);
  • Mistake 2: Parameter name shadowing a field name without using this — Writing public Car(String colour) { colour = colour; } is a no-op: you're assigning the parameter to itself, and the field this.colour stays at its default value (null). Fix: always use this.fieldName = parameterName when the names match, e.g. this.colour = colour;
  • Mistake 3: Assuming two variables pointing to the same object are independent — Writing BankAccount accountA = new BankAccount(...); BankAccount accountB = accountA; does NOT create two objects. Both variables point to the same object in memory. Doing accountB.deposit(500) also changes what you see through accountA. Fix: to get a truly separate object, call new again: BankAccount accountB = new BankAccount(...);

Interview Questions on This Topic

  • QWhat is the difference between a class and an object in Java? Can you give a real-world example?
  • QWhat happens if you define a class with no constructor? What does Java do, and what changes when you add your own constructor?
  • QIf two variables reference the same object and you modify the object through one variable, what does the second variable see — and why?

Frequently Asked Questions

What is the difference between a class and an object in Java?

A class is the template — written once, it defines what fields and methods something will have. An object is a live instance of that class created at runtime with the new keyword. One class can produce hundreds of independent objects, each with their own data.

Can a Java class have multiple constructors?

Yes — this is called constructor overloading. You define multiple constructors with the same name but different parameter lists. Java picks the correct one based on what arguments you pass to new. It's useful when you want to support creating objects with varying levels of initial information.

Do I always need to write a constructor in my Java class?

Not always. If you write no constructor, Java automatically provides a no-argument default constructor that sets fields to their default values (0, null, false). However, as soon as you write any constructor yourself, that free default is removed. Write an explicit no-arg constructor if you still need one alongside your custom constructors.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousCopying Arrays in JavaNext →Constructors in Java
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged