☕ Java

Java Classes and Objects — Syntax, Types, Examples & Best Practices

Everything you need to know about Java Classes and Objects — class declaration, object creation, constructors, fields, methods, access modifiers, static vs instance members, 'this' keyword, encapsulation, Java 21 records, and real-world production code examples.

📅

Last Updated

March 2026

âąī¸

Read Time

25 min

đŸŽ¯

Level

Beginner

đŸˇī¸

Chapter

13 of 35

What are Classes and Objects in Java?

Java is a class-based, object-oriented programming (OOP) language — meaning everything in Java revolves around classes and objects. A class is a blueprint or template that defines the structure (data/fields) and behavior (methods) that its objects will have. An object is a specific, concrete instance created from that class blueprint, occupying memory and holding actual data values.

Real-world analogy: Think of a class as the architectural blueprint of a house — it describes how many rooms there are, where the doors go, and what the plumbing layout looks like. An object is an actual house built from that blueprint. You can build many different houses (objects) from the same blueprint (class), and each house can have different paint colors and furniture (field values), but they all share the same structural design (class definition).

In Java, every program has at least one class. Even the Hello World program is wrapped in a class. Objects are created at runtime using the new keyword, which allocates memory in the heap and calls a constructor to initialize the object's state. Without classes and objects, there is no Java program.

Class Declaration — Syntax & Structure

A class declaration defines a new reference type in Java. It specifies the class name, optional modifiers, the fields (data) the class holds, the constructors used to create objects, and the methods (behavior) objects can perform. The class body is enclosed in curly braces {}. By convention, class names use PascalCase (e.g., BankAccount, StudentRecord).

📌
Class Syntax

[access_modifier] class ClassName { // 1. Fields (state / attributes) // 2. Constructor(s) // 3. Methods (behavior) // 4. Nested classes (optional) }

📋
Class Rules

1. Class name must match the filename exactly (for public classes). 2. One public class per .java file. 3. Class names use PascalCase — BankAccount, not bankaccount. 4. Fields should be private (encapsulation); methods can be public. 5. Every class implicitly extends java.lang.Object if no parent is specified.

🔁
Class Components

Fields: store object data (int age, String name). Constructors: initialize objects when 'new' is used. Methods: define what objects can do. Access modifiers: control who can see each member. Static members: belong to the class itself, not individual objects.

☕ JavaBankAccount.java
// ✅ Well-structured Java class — BankAccount example
public class BankAccount {

    // ── 1. Fields (instance variables — private for encapsulation) ──
    private String accountNumber;
    private String holderName;
    private double balance;
    private String accountType;

    // ── Static field — shared across ALL BankAccount objects ──
    private static int totalAccounts = 0;
    private static final double MIN_BALANCE = 500.0;

    // ── 2. Constructor ──
    public BankAccount(String accountNumber, String holderName,
                       double initialBalance, String accountType) {
        this.accountNumber = accountNumber;
        this.holderName    = holderName;
        this.balance       = initialBalance;
        this.accountType   = accountType;
        totalAccounts++;   // increment shared counter on each new object
    }

    // ── 3. Methods ──
    public void deposit(double amount) {
        if (amount <= 0) {
            System.out.println("Deposit amount must be positive.");
            return;
        }
        balance += amount;
        System.out.printf("Deposited ₹%.2f | New Balance: ₹%.2f%n", amount, balance);
    }

    public boolean withdraw(double amount) {
        if (amount <= 0 || (balance - amount) < MIN_BALANCE) {
            System.out.println("Withdrawal denied — insufficient funds or minimum balance breach.");
            return false;
        }
        balance -= amount;
        System.out.printf("Withdrawn ₹%.2f | Remaining: ₹%.2f%n", amount, balance);
        return true;
    }

    public void printStatement() {
        System.out.println("Account: " + accountNumber);
        System.out.println("Holder : " + holderName);
        System.out.printf( "Balance: ₹%.2f%n", balance);
    }

    // ── Static method — belongs to class, not any object ──
    public static int getTotalAccounts() {
        return totalAccounts;
    }

    // ── Getters (access private fields from outside) ──
    public String getAccountNumber() { return accountNumber; }
    public String getHolderName()    { return holderName;    }
    public double getBalance()        { return balance;       }
}

Object Creation — the 'new' Keyword

Objects are created at runtime using the new keyword. The new keyword does three things in sequence: (1) Allocates memory in the heap for the new object, (2) Calls the constructor to initialize the object's fields, and (3) Returns a reference (memory address) to the newly created object, which is stored in a reference variable. The reference variable itself lives on the stack; the object lives on the heap.

☕ JavaObjectCreation.java
public class ObjectCreation {
    public static void main(String[] args) {

        // ✅ Basic object creation
        // Syntax: ClassName referenceVariable = new ClassName(arguments);
        BankAccount account1 = new BankAccount("ACC001", "Aarav Shah", 10000.0, "Savings");
        BankAccount account2 = new BankAccount("ACC002", "Priya Mehta", 25000.0, "Current");

        // ✅ Each object has its OWN copy of instance fields
        account1.deposit(5000.0);   // only account1's balance changes
        account2.withdraw(3000.0);  // only account2's balance changes

        account1.printStatement();
        account2.printStatement();

        // ✅ Static field — shared across all objects
        System.out.println("Total accounts created: " + BankAccount.getTotalAccounts()); // 2

        // ✅ Reference variable stores ADDRESS of object, not the object itself
        BankAccount ref1 = account1;  // ref1 and account1 point to SAME object
        ref1.deposit(1000.0);         // modifies the same account1 object
        System.out.println(account1.getBalance()); // reflects ref1's deposit

        // ✅ null reference — variable exists but points to no object
        BankAccount emptyRef = null;
        // emptyRef.deposit(100);  // ❌ NullPointerException — no object to call on

        // ✅ Anonymous object — created without storing reference (used once)
        new BankAccount("ACC003", "Dev Patel", 5000.0, "Savings").printStatement();
        // Object is immediately eligible for garbage collection after this line
    }
}

Constructors — Types & Overloading

A constructor is a special block of code that is automatically invoked when an object is created with new. It has the same name as the class and no return type — not even void. Constructors initialize the new object's fields to meaningful starting values. Java allows constructor overloading — multiple constructors with different parameter lists — so objects can be created in different ways depending on available information.

☕ JavaConstructorTypes.java
public class Student {

    private String name;
    private int    age;
    private String course;
    private double gpa;

    // ── 1. No-arg Constructor (default-like) ──
    public Student() {
        this.name   = "Unknown";
        this.age    = 0;
        this.course = "Undeclared";
        this.gpa    = 0.0;
        System.out.println("No-arg constructor called");
    }

    // ── 2. Parameterized Constructor (name + age only) ──
    public Student(String name, int age) {
        this.name   = name;
        this.age    = age;
        this.course = "Undeclared";
        this.gpa    = 0.0;
        System.out.println("Partial constructor called for: " + name);
    }

    // ── 3. Fully Parameterized Constructor ──
    public Student(String name, int age, String course, double gpa) {
        this.name   = name;
        this.age    = age;
        this.course = course;
        this.gpa    = gpa;
        System.out.println("Full constructor called for: " + name);
    }

    // ── 4. Copy Constructor ──
    public Student(Student other) {
        this.name   = other.name;
        this.age    = other.age;
        this.course = other.course;
        this.gpa    = other.gpa;
        System.out.println("Copy constructor called — cloned: " + other.name);
    }

    // ── 5. Constructor chaining with this() ──
    public Student(String name) {
        this(name, 18); // calls Student(String, int) constructor
        System.out.println("Single-arg constructor — chained to partial constructor");
    }

    public void display() {
        System.out.printf("Name: %-15s | Age: %2d | Course: %-15s | GPA: %.1f%n",
                          name, age, course, gpa);
    }

    public static void main(String[] args) {
        Student s1 = new Student();                               // No-arg
        Student s2 = new Student("Ananya", 20);                  // Partial
        Student s3 = new Student("Rohan", 21, "Computer Sci", 8.7); // Full
        Student s4 = new Student(s3);                             // Copy
        Student s5 = new Student("Meera");                       // Chained

        s1.display(); s2.display(); s3.display(); s4.display(); s5.display();
    }
}

Fields and Methods — State and Behavior

Fields (also called instance variables or attributes) define the state of an object — the data it holds. Methods define the behavior of an object — the actions it can perform. Together, fields and methods form the complete definition of what a class represents and can do. Good class design keeps fields private and exposes behavior through well-named public methods.

đŸ—ƒī¸
Instance Fields

Declared inside the class but outside any method. Each object gets its own independent copy. Example: 'private String name;' — every Student object has its own name. Default values: numeric fields default to 0, boolean to false, object references to null. Always declare fields private to enforce encapsulation.

đŸ“Ļ
Static Fields (Class Variables)

Declared with 'static' keyword. ONE copy shared across ALL objects of the class. Example: 'private static int totalStudents = 0;' — all Student objects share this counter. Used for: counters, constants (static final), shared configuration. Access via class name: 'Student.totalStudents', not via object reference.

âš™ī¸
Instance Methods

Called on a specific object — 'account.deposit(500)'. Can access and modify instance fields of the object. Have implicit access to 'this'. Most regular class methods are instance methods. Named with camelCase verbs: deposit(), withdraw(), calculateInterest(), getBalance().

đŸ›ī¸
Static Methods

Declared with 'static'. Belong to the class — called as 'ClassName.method()'. Cannot access instance fields or 'this' — they have no object context. Used for: utility functions (Math.sqrt()), factory methods, helper operations that don't need object state. main() is static because JVM calls it before any object exists.

â†Šī¸
Return Types

Methods return data using 'return statement'. Return type declared before method name: 'public double getBalance()' returns a double. 'public void deposit()' returns nothing. A method can return any type: primitives (int, double), objects (String, List), or void. The returned value can be used by the caller.

đŸ“Ĩ
Parameters & Arguments

Parameters are variables in the method signature: 'public void deposit(double amount)'. Arguments are the actual values passed when calling: 'account.deposit(5000.0)'. Java is PASS BY VALUE — primitives pass a copy of the value; objects pass a copy of the reference (not the object itself). Modifying a primitive parameter inside a method doesn't affect the caller.

☕ JavaFieldsAndMethods.java
public class Circle {

    // ── Instance fields ──
    private double radius;
    private String color;

    // ── Static field — shared by all circles ──
    private static int totalCirclesCreated = 0;
    public  static final double PI = 3.141592653589793; // constant

    // ── Constructor ──
    public Circle(double radius, String color) {
        this.radius = radius;
        this.color  = color;
        totalCirclesCreated++;
    }

    // ── Instance methods ──
    public double calculateArea() {
        return PI * radius * radius;
    }

    public double calculateCircumference() {
        return 2 * PI * radius;
    }

    public boolean isLargerThan(Circle other) {
        return this.radius > other.radius;
    }

    // ── Static method ──
    public static int getTotalCirclesCreated() {
        return totalCirclesCreated;
        // Cannot use 'this.radius' here — no object context in static method
    }

    public void describe() {
        System.out.printf("Circle [color=%s, radius=%.2f, area=%.2f]%n",
                          color, radius, calculateArea());
    }

    public static void main(String[] args) {
        Circle c1 = new Circle(5.0, "Red");
        Circle c2 = new Circle(8.0, "Blue");

        c1.describe(); // Circle [color=Red, radius=5.00, area=78.54]
        c2.describe(); // Circle [color=Blue, radius=8.00, area=201.06]

        System.out.println("c1 larger than c2? " + c1.isLargerThan(c2)); // false
        System.out.println("Total circles: " + Circle.getTotalCirclesCreated()); // 2
    }
}

Access Modifiers — Controlling Visibility

Access modifiers in Java control the visibility and accessibility of classes, fields, constructors, and methods. They are a core mechanism of encapsulation — one of the four pillars of OOP. Java has four access levels: public, protected, default (package-private), and private. Choosing the right modifier prevents unauthorized access to internal data and implementation details.

ModifierSame ClassSame PackageSubclass (other package)Other Package
public✅ Yes✅ Yes✅ Yes✅ Yes
protected✅ Yes✅ Yes✅ Yes❌ No
default✅ Yes✅ Yes❌ No❌ No
private✅ Yes❌ No❌ No❌ No
☕ JavaAccessModifiers.java
public class Person {

    public    String publicName;    // Accessible from anywhere
    protected int    protectedAge;  // Accessible in same package + subclasses
              String defaultEmail;  // Accessible only within same package (no keyword)
    private   double privateSalary; // Accessible only within this class

    public Person(String name, int age, String email, double salary) {
        this.publicName    = name;
        this.protectedAge  = age;
        this.defaultEmail  = email;
        this.privateSalary = salary;
    }

    // ✅ Public method — part of the class's public API
    public void introduce() {
        System.out.println("Hi, I am " + publicName + ", age " + protectedAge);
    }

    // ✅ Private helper — internal implementation detail, hidden from outside
    private double calculateTax() {
        return privateSalary * 0.10;
    }

    // ✅ Public method that internally uses private helper
    public double getNetSalary() {
        return privateSalary - calculateTax(); // internal use is fine
    }
}

class Main {
    public static void main(String[] args) {
        Person p = new Person("Aditya", 30, "adi@example.com", 80000);

        System.out.println(p.publicName);    // ✅ OK — public
        System.out.println(p.protectedAge);  // ✅ OK — same package
        System.out.println(p.defaultEmail);  // ✅ OK — same package
        // System.out.println(p.privateSalary); // ❌ Compile error — private

        p.introduce();                       // ✅ OK — public method
        System.out.println(p.getNetSalary()); // ✅ OK — public method
        // p.calculateTax();                 // ❌ Compile error — private method
    }
}

The 'this' Keyword — Current Object Reference

The this keyword in Java is a reference to the current object — the specific object on which the current method or constructor is being executed. It is used to disambiguate between instance fields and local variables with the same name, to call other constructors of the same class, to pass the current object as an argument, and to return the current object (method chaining). this is only available in non-static contexts.

☕ JavaThisKeyword.java
public class Employee {

    private String name;
    private String department;
    private double salary;

    // ── Use 1: Disambiguate field vs parameter (most common use) ──
    public Employee(String name, String department, double salary) {
        this.name       = name;       // 'this.name' = field; 'name' = parameter
        this.department = department;
        this.salary     = salary;
    }

    // ── Use 2: Constructor chaining — this() calls another constructor ──
    public Employee(String name) {
        this(name, "General", 30000.0); // Must be FIRST statement
        System.out.println("Single-arg constructor — chained");
    }

    // ── Use 3: Pass current object as argument ──
    public void register(EmployeeRegistry registry) {
        registry.add(this); // passing current Employee object
    }

    // ── Use 4: Method chaining (Builder/Fluent pattern) ──
    public Employee setDepartment(String department) {
        this.department = department;
        return this; // returns current object for chaining
    }

    public Employee setSalary(double salary) {
        this.salary = salary;
        return this;
    }

    public void display() {
        System.out.printf("Employee: %-15s | Dept: %-12s | Salary: ₹%.2f%n",
                          name, department, salary);
    }

    public static void main(String[] args) {
        // ✅ Normal construction
        Employee e1 = new Employee("Kiran Rao", "Engineering", 75000.0);
        e1.display();

        // ✅ Method chaining using return this
        Employee e2 = new Employee("Neha Joshi");
        e2.setDepartment("Marketing").setSalary(55000.0); // chained calls
        e2.display();

        // ❌ this cannot be used in static method
        // In a static context: System.out.println(this); // COMPILE ERROR
    }
}

Static vs Instance Members — Class-Level vs Object-Level

One of the most important distinctions in Java OOP is between static members (class-level) and instance members (object-level). Static members are declared with the static keyword and belong to the class — they exist independently of any object. Instance members belong to each individual object — they are created fresh for every new object. Confusing the two is a very common source of bugs.

AspectInstance MemberStatic Member
Belongs toEach individual objectThe class itself
MemorySeparate copy per object (in heap)Single shared copy (in method area)
Access viaObject reference: obj.field / obj.method()Class name: ClassName.field / ClassName.method()
Can use 'this'?✅ Yes — 'this' refers to current object❌ No — no object context in static
Access instance?✅ Yes — full access to instance fields❌ No — cannot access instance fields directly
Created whenEach time 'new' is usedWhen the class is first loaded by JVM
Destroyed whenObject is garbage collectedWhen the class is unloaded (program end)
Typical useObject state: name, age, balanceShared data: counters, constants, utility methods
Keyword(no keyword needed)static
☕ JavaStaticVsInstance.java
public class Counter {

    // ── Instance variable — each object has its own count ──
    private int instanceCount = 0;

    // ── Static variable — ONE shared count for ALL objects ──
    private static int staticCount = 0;

    public void increment() {
        instanceCount++;  // only this object's count goes up
        staticCount++;    // the shared class-wide count goes up
    }

    public void displayCounts() {
        System.out.println("Instance count: " + instanceCount
                         + " | Static count: " + staticCount);
    }

    // ✅ Static utility method — no object needed
    public static int getStaticCount() {
        return staticCount;
        // return instanceCount; // ❌ COMPILE ERROR — no instance context
    }

    public static void main(String[] args) {
        Counter c1 = new Counter();
        Counter c2 = new Counter();

        c1.increment(); c1.increment(); c1.increment(); // c1 increments 3x
        c2.increment();                                 // c2 increments 1x

        c1.displayCounts(); // Instance count: 3 | Static count: 4
        c2.displayCounts(); // Instance count: 1 | Static count: 4 (shared!)

        System.out.println("Total: " + Counter.getStaticCount()); // 4
    }
}

Encapsulation — Getters, Setters & Data Hiding

Encapsulation is one of the four pillars of object-oriented programming. It means bundling data (fields) and behavior (methods) together in a class, while hiding the internal state from outside access. In Java, encapsulation is achieved by declaring fields private and providing controlled access through public getter and setter methods. This gives the class full control over how its data is read or modified — allowing validation, transformation, and protection of internal state.

☕ JavaEncapsulation.java
public class Product {

    // ✅ Private fields — hidden from outside; only accessible via methods
    private String name;
    private double price;
    private int    stockQuantity;
    private String category;

    public Product(String name, double price, int stockQuantity, String category) {
        setName(name);              // use setter for validation even in constructor
        setPrice(price);
        setStockQuantity(stockQuantity);
        this.category = category;
    }

    // ── Getter: read-only access to name ──
    public String getName() { return name; }

    // ── Setter with validation: cannot set blank name ──
    public void setName(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Product name cannot be blank");
        }
        this.name = name.trim();
    }

    // ── Getter ──
    public double getPrice() { return price; }

    // ── Setter with validation: price must be positive ──
    public void setPrice(double price) {
        if (price < 0) {
            throw new IllegalArgumentException("Price cannot be negative");
        }
        this.price = price;
    }

    public int getStockQuantity() { return stockQuantity; }

    public void setStockQuantity(int qty) {
        if (qty < 0) throw new IllegalArgumentException("Stock cannot be negative");
        this.stockQuantity = qty;
    }

    // ── Read-only field — no setter for category (set once at construction) ──
    public String getCategory() { return category; }

    // ── Business method using encapsulated data ──
    public boolean isAvailable() { return stockQuantity > 0; }

    public void sell(int quantity) {
        if (quantity > stockQuantity) {
            throw new IllegalStateException("Insufficient stock");
        }
        stockQuantity -= quantity;
        System.out.printf("Sold %d units of '%s'. Remaining: %d%n",
                          quantity, name, stockQuantity);
    }

    @Override
    public String toString() {
        return String.format("Product{name='%s', price=₹%.2f, stock=%d, category='%s'}",
                            name, price, stockQuantity, category);
    }

    public static void main(String[] args) {
        Product p = new Product("Laptop", 75000.0, 10, "Electronics");
        System.out.println(p);

        p.sell(3);
        System.out.println("Available: " + p.isAvailable()); // true

        p.setPrice(72000.0); // price reduced — setter validates
        // p.setPrice(-100); // ❌ throws IllegalArgumentException
    }
}

Object Memory — Stack & Heap

Understanding how Java manages memory for objects is critical for writing correct code and avoiding NullPointerException. Java memory is divided into two main areas: the Stack stores method call frames and local variables (including reference variables), and the Heap stores all object instances. The Garbage Collector (GC) automatically reclaims heap memory from objects that are no longer referenced.

📚
Stack Memory

Stores: method call frames, local variables, primitive values, and object REFERENCES (addresses). LIFO structure — last in, first out. Fast access. Size is limited. When a method returns, its stack frame is popped. Reference variables live on the stack; the objects they point to live on the heap. Thread-safe — each thread has its own stack.

đŸ”ī¸
Heap Memory

Where ALL objects are stored — created with 'new'. Shared across all threads. Much larger than stack. Objects stay alive as long as at least one reference points to them. Managed by Garbage Collector — GC periodically scans for unreferenced objects and frees their memory. Setting a reference to null removes that reference; if no other references exist, the object becomes eligible for GC.

đŸ—‘ī¸
Garbage Collection

Java's automatic memory management. GC runs in the background, finding objects with no live references and reclaiming their heap memory. Developers don't need to manually free memory (unlike C/C++). An object becomes GC-eligible when: all reference variables pointing to it are set to null, go out of scope, or are reassigned. GC call is not guaranteed immediately: 'System.gc()' is a hint, not a command.

âš ī¸
NullPointerException

Thrown when you call a method or access a field on a null reference — a reference variable that doesn't point to any object. Example: 'Student s = null; s.getName();' — NPE! Prevention: always initialize references before use, check for null before calling methods, use Optional<T> for values that might be absent. Java 14+ NPEs have helpful messages showing exactly which reference was null.

☕ JavaObjectMemory.java
public class ObjectMemory {
    public static void main(String[] args) {

        // ── Stack: reference variable 'a' | Heap: Student object ──
        Student a = new Student("Arjun", 22);  // 'a' is on stack, object on heap

        // ── Stack: reference variable 'b' | SAME heap object as 'a' ──
        Student b = a;  // b copies the REFERENCE (address), not the object

        b.setName("Bhavna");   // modifies the SAME object that 'a' points to
        System.out.println(a.getName()); // prints 'Bhavna' — same object!

        // ── Make 'a' point to a NEW object ──
        a = new Student("Chetan", 25); // new object in heap; old object now has only 'b' pointing to it
        System.out.println(a.getName()); // Chetan
        System.out.println(b.getName()); // Bhavna (still the original object)

        // ── Set b to null — original object now has NO references ──
        b = null; // Original 'Bhavna' object is now GC-eligible

        // b.getName(); // ❌ NullPointerException — b is null!

        // ── Anonymous object — no reference stored, GC-eligible immediately ──
        new Student("Temp", 18).display(); // used and immediately eligible for GC

        System.gc(); // Hint to JVM to run GC — not guaranteed to run immediately
    }
}

Common Mistakes & Pitfalls — Bugs That Fool Everyone

These mistakes appear repeatedly in beginner Java code related to classes and objects. Each one can cause subtle bugs that are hard to trace. Study them carefully to recognize them in your own code.

☕ JavaCommonMistakes.java
// ❌ MISTAKE 1: Accessing instance member from static method
public class Car {
    private String model;

    public static void showModel() {
        // System.out.println(model); // ❌ COMPILE ERROR
        // Cannot make a static reference to the non-static field 'model'
        // Fix: either make method non-static, or accept a Car parameter
    }
}

// ❌ MISTAKE 2: Forgetting 'this' — field shadowed by parameter
public class Box {
    private int width;
    public Box(int width) {
        width = width; // ❌ Assigns parameter to itself — field never set!
        // Fix: this.width = width;
    }
}

// ❌ MISTAKE 3: Assuming reference copy = object copy
Student s1 = new Student("Raj", 20);
Student s2 = s1;      // ❌ s2 is NOT a copy — both point to SAME object
s2.setName("Ria");    // Changes s1 too!
System.out.println(s1.getName()); // Prints 'Ria' — unexpected!
// Fix: implement a copy constructor: Student s2 = new Student(s1);

// ❌ MISTAKE 4: Calling method on uninitialized reference
Student s = null;
// s.getName(); // ❌ NullPointerException at runtime
// Fix: always initialize before use, or add null check
if (s != null) { s.getName(); }

// ❌ MISTAKE 5: Adding return type to constructor
// public void MyClass() { }  // ❌ This is a METHOD named 'MyClass', not a constructor!
// Java silently treats it as a regular method — the real constructor is never called
// Fix: constructors have NO return type: public MyClass() { }

// ❌ MISTAKE 6: Using == to compare objects instead of .equals()
String s1 = new String("Java");
String s2 = new String("Java");
if (s1 == s2) {        // ❌ FALSE — different objects in heap
    System.out.println("Equal");
}
if (s1.equals(s2)) {   // ✅ TRUE — compares content
    System.out.println("Equal"); // prints
}

// ❌ MISTAKE 7: Accessing static member via object reference (misleading)
Counter c1 = new Counter();
c1.staticCount; // ❌ Misleading — looks like instance access
// Fix: Counter.staticCount — use class name for static members

Java 21 Records — Compact, Immutable Data Classes

Java 16 introduced Records (JEP 395) as a permanent feature — a special, compact class form designed for immutable data carriers. A record automatically generates: a canonical constructor, private final fields, public getter methods (named after the field, without 'get' prefix), toString(), equals(), and hashCode(). Records eliminate the boilerplate of traditional POJO (Plain Old Java Object) classes when all you need is to store and pass data.

☕ JavaJavaRecords.java
// ❌ BEFORE Records: Traditional POJO — 40+ lines of boilerplate
public class PointOld {
    private final double x;
    private final double y;
    public PointOld(double x, double y) { this.x = x; this.y = y; }
    public double x() { return x; }
    public double y() { return y; }
    @Override public boolean equals(Object o) { /* ... 10 lines ... */ }
    @Override public int hashCode() { return Objects.hash(x, y); }
    @Override public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}

// ✅ AFTER Records: Same thing in 1 line — Java 16+
record Point(double x, double y) { }
// Automatically generated: constructor, x(), y(), equals(), hashCode(), toString()

// ✅ Record with compact constructor (add validation)
record Product(String name, double price, int stock) {
    // Compact constructor — no parameter list needed here
    Product {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required");
        if (price < 0)  throw new IllegalArgumentException("Price cannot be negative");
        if (stock < 0)  throw new IllegalArgumentException("Stock cannot be negative");
        name = name.trim(); // can normalize fields in compact constructor
    }

    // ✅ Records can have custom methods
    public boolean isAvailable() { return stock > 0; }
    public double discountedPrice(double pct) { return price * (1 - pct / 100); }
}

// ✅ Using records
public class RecordDemo {
    public static void main(String[] args) {
        Point p1 = new Point(3.0, 4.0);
        Point p2 = new Point(3.0, 4.0);

        System.out.println(p1);          // Point[x=3.0, y=4.0]
        System.out.println(p1.x());      // 3.0 — accessor, no 'get' prefix
        System.out.println(p1.equals(p2)); // true — value equality

        Product laptop = new Product("Laptop", 75000.0, 5);
        System.out.println(laptop);
        System.out.println("Available: " + laptop.isAvailable());   // true
        System.out.println("10% off: ₹" + laptop.discountedPrice(10)); // 67500.0

        // Records are IMMUTABLE — no setters; fields are final
        // laptop.name = "Desktop"; // ❌ COMPILE ERROR — final field
    }
}

Bad Practices & Anti-Patterns — What Senior Developers Reject

These class and object anti-patterns are the most common reasons for failed code reviews in professional Java teams. Each reduces maintainability, testability, or correctness.

đŸšĢ
God Class (Doing Too Much)

A class with hundreds of fields and methods that handles everything — database access, business logic, UI rendering, email sending — in one giant class. Violates the Single Responsibility Principle (SRP). Fix: decompose into focused classes: CustomerService, CustomerRepository, EmailNotifier. Each class should have ONE reason to change.

đŸšĢ
Public Fields (Breaking Encapsulation)

Declaring fields as public: 'public int balance;' — any code anywhere can set 'account.balance = -1000000;' — an impossible valid state. Always make fields private and provide validated getters/setters. Public fields make refactoring impossible — you can never change the type or add validation later without breaking all callers.

đŸšĢ
Mutable Static Fields (Global State)

Static mutable fields behave like global variables — any code can modify them, causing unpredictable behavior, especially in multithreaded environments. Every test that changes a static field can affect all subsequent tests. Use static only for constants (static final) or counters that are intentionally shared. Prefer dependency injection over static state.

đŸšĢ
Anemic Domain Model

Classes with only getters and setters and no real behavior — just data bags. All business logic lives in separate 'Service' classes that pull data out, manipulate it, and push it back. This defeats the purpose of OOP. Move behavior that naturally belongs to the class INTO the class: instead of 'accountService.isOverdrawn(account)', write 'account.isOverdrawn()' directly in the Account class.

đŸšĢ
Missing toString() and equals()/hashCode()

Not overriding toString() means debugging shows 'com.example.Student@1b6d3586' — useless. Not overriding equals() means object comparison uses reference equality — two Student objects with the same ID won't be equal in collections. Always override toString() for debuggability and equals()+hashCode() together for objects used in collections (List, Map, Set). Records do this automatically.

đŸšĢ
Excessive Constructor Parameters

A constructor with 8-10 parameters is a sign the class is doing too much OR needs a Builder pattern. 'new User(name, email, phone, address, age, role, status, createdDate, lastLogin, preferredLanguage)' — callers can't remember the order. Fix: use the Builder pattern for complex object construction, or split into smaller focused classes.

☕ JavaAntiPatterns.java
// ❌ ANTI-PATTERN 1: Public fields — no encapsulation
public class BadAccount {
    public double balance;  // ❌ Anyone can set account.balance = -999999
    public String owner;
}

// ✅ BETTER: Private fields with validated access
public class GoodAccount {
    private double balance;
    public void setBalance(double balance) {
        if (balance < 0) throw new IllegalArgumentException("Balance cannot be negative");
        this.balance = balance;
    }
    public double getBalance() { return balance; }
}

// ❌ ANTI-PATTERN 2: Missing toString()
Student s = new Student("Raj", 21);
System.out.println(s); // ❌ Prints: Student@7852e922 — useless for debugging

// ✅ BETTER: Always override toString()
@Override
public String toString() {
    return String.format("Student{name='%s', age=%d}", name, age);
}
System.out.println(s); // ✅ Prints: Student{name='Raj', age=21}

// ❌ ANTI-PATTERN 3: Too many constructor parameters
User u = new User("Raj", "raj@email.com", "9999", "Delhi", 25, "ADMIN", "ACTIVE");
// ✅ BETTER: Builder pattern
User u = new User.Builder("Raj", "raj@email.com")
              .phone("9999")
              .address("Delhi")
              .role("ADMIN")
              .build();

// ❌ ANTI-PATTERN 4: Anemic class — no behavior, just data
class OrderAnemic {
    public double totalAmount;
    public String status;
    // All logic in OrderService — order knows nothing about itself
}
// ✅ BETTER: Rich domain class with behavior
class Order {
    private double totalAmount;
    private String status;
    public boolean isEligibleForDiscount() { return totalAmount > 5000; }
    public void cancel() {
        if (!"PENDING".equals(status)) throw new IllegalStateException("Cannot cancel");
        this.status = "CANCELLED";
    }
}

Real-World Production Code Examples — Classes & Objects in Context

The following examples model class and object patterns found in real enterprise Java codebases — a Spring Boot service layer with proper encapsulation, validation, and clean class design.

☕ JavaOrder.java — Rich Domain Class
package com.techsustainify.ecommerce.domain;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Order {

    // ── Immutable identity fields ──
    private final String orderId;
    private final String customerId;
    private final LocalDateTime createdAt;

    // ── Mutable state fields ──
    private OrderStatus status;
    private final List<OrderItem> items;
    private String shippingAddress;
    private LocalDateTime lastUpdatedAt;

    // ── Static counter ──
    private static int totalOrdersPlaced = 0;

    // ── Constructor ──
    public Order(String orderId, String customerId, String shippingAddress) {
        if (orderId == null || orderId.isBlank())
            throw new IllegalArgumentException("Order ID cannot be blank");
        if (customerId == null || customerId.isBlank())
            throw new IllegalArgumentException("Customer ID cannot be blank");
        if (shippingAddress == null || shippingAddress.isBlank())
            throw new IllegalArgumentException("Shipping address required");

        this.orderId         = orderId;
        this.customerId      = customerId;
        this.shippingAddress = shippingAddress;
        this.status          = OrderStatus.PENDING;
        this.items           = new ArrayList<>();
        this.createdAt       = LocalDateTime.now();
        this.lastUpdatedAt   = this.createdAt;
        totalOrdersPlaced++;
    }

    // ── Business methods (behavior, not just getters) ──
    public void addItem(OrderItem item) {
        if (status != OrderStatus.PENDING)
            throw new IllegalStateException("Cannot add items to a " + status + " order");
        items.add(item);
        lastUpdatedAt = LocalDateTime.now();
    }

    public void confirm() {
        if (items.isEmpty())
            throw new IllegalStateException("Cannot confirm an empty order");
        if (status != OrderStatus.PENDING)
            throw new IllegalStateException("Only PENDING orders can be confirmed");
        this.status       = OrderStatus.CONFIRMED;
        this.lastUpdatedAt = LocalDateTime.now();
    }

    public void ship() {
        if (status != OrderStatus.CONFIRMED)
            throw new IllegalStateException("Only CONFIRMED orders can be shipped");
        this.status       = OrderStatus.SHIPPED;
        this.lastUpdatedAt = LocalDateTime.now();
    }

    public void cancel() {
        if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED)
            throw new IllegalStateException("Cannot cancel a " + status + " order");
        this.status       = OrderStatus.CANCELLED;
        this.lastUpdatedAt = LocalDateTime.now();
    }

    public double calculateTotal() {
        return items.stream().mapToDouble(OrderItem::getSubtotal).sum();
    }

    public boolean isEligibleForDiscount() {
        return calculateTotal() > 5000 && status == OrderStatus.PENDING;
    }

    // ── Defensive getter — returns unmodifiable view, list cannot be mutated ──
    public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }

    // ── Read-only getters ──
    public String getOrderId()      { return orderId;      }
    public String getCustomerId()   { return customerId;   }
    public OrderStatus getStatus()  { return status;       }
    public LocalDateTime getCreatedAt() { return createdAt; }

    // ── Static method ──
    public static int getTotalOrdersPlaced() { return totalOrdersPlaced; }

    @Override
    public String toString() {
        return String.format("Order{id='%s', customer='%s', status=%s, total=₹%.2f, items=%d}",
                            orderId, customerId, status, calculateTotal(), items.size());
    }
}
☕ JavaUserRecord.java — Java Record for DTO
package com.techsustainify.user.dto;

import java.time.LocalDate;

// ✅ Java Record — perfect for API response DTOs (immutable, no boilerplate)
public record UserSummaryDTO(
    String userId,
    String fullName,
    String email,
    String role,
    LocalDate joinedDate
) {
    // ✅ Compact constructor — validate data coming from DB or API
    UserSummaryDTO {
        if (userId == null || userId.isBlank())
            throw new IllegalArgumentException("userId cannot be blank");
        if (email == null || !email.contains("@"))
            throw new IllegalArgumentException("Invalid email");
        fullName = fullName != null ? fullName.trim() : "Unknown";
        role     = role     != null ? role.toUpperCase() : "USER";
    }

    // ✅ Custom methods are allowed in records
    public boolean isAdmin() {
        return "ADMIN".equals(role);
    }

    public String maskedEmail() {
        int atIndex = email.indexOf('@');
        return email.substring(0, 2) + "****" + email.substring(atIndex);
    }
}

// ── Usage ──
class RecordUsage {
    public static void main(String[] args) {
        UserSummaryDTO user = new UserSummaryDTO(
            "USR001", "Rahul Verma", "rahul@example.com", "admin",
            LocalDate.of(2023, 6, 15)
        );

        System.out.println(user);                     // auto toString()
        System.out.println(user.fullName());          // accessor (no 'get')
        System.out.println(user.isAdmin());           // true — 'admin' → 'ADMIN'
        System.out.println(user.maskedEmail());       // ra****@example.com
    }
}

Class-to-Object Flowchart — Visual Creation Flow

This flowchart illustrates how a Java class is transformed into objects at runtime, from class loading through object creation, usage, and garbage collection.

📄 .java Source FileClass definition written by developer
compile
âš™ī¸ javac CompilerCompiles to .class bytecode
load
🔄 JVM Class LoaderLoads .class file into memory
static members
💾 Method AreaStatic fields, methods, class metadata
ready for objects
🔑 new ClassName(args)Developer calls 'new' keyword
allocate
đŸ”ī¸ Heap AllocationMemory allocated for object fields
initialize
🔨 Constructor ExecutesFields initialized with starting values
return ref
📮 Reference ReturnedMemory address stored in variable
use object
✅ Object in UseMethods called, fields accessed
ref count = 0
đŸ—‘ī¸ Garbage CollectedNo references remain — memory freed

Code Execution Flow — from source to output

Java Classes & Objects Interview Questions — Beginner to Advanced

These questions are consistently asked in Java fresher interviews, OCPJP certification exams, and campus placement tests for OOP topics.

Practice Questions — Test Your Classes & Objects Knowledge

Attempt each question independently before reading the answer — active recall is proven to be 2–3x more effective than passive reading.

1. What is the output? class Dog { String name; Dog(String name) { this.name = name; } void bark() { System.out.println(name + " says: Woof!"); } } public class Main { public static void main(String[] args) { Dog d1 = new Dog("Bruno"); Dog d2 = d1; d2.name = "Max"; d1.bark(); d2.bark(); } }

Easy

2. What is wrong with this constructor and how do you fix it? public class Rectangle { private int width; private int height; public void Rectangle(int width, int height) { width = width; height = height; } }

Easy

3. Design a 'Circle' class with: private field 'radius' (double), constructor that validates radius > 0 (throw IllegalArgumentException if not), methods calculateArea() and calculateCircumference(), and a proper toString().

Easy

4. A 'Counter' class has: 'private int count = 0;' and 'public static int totalCounters = 0;'. If you create Counter c1, c2, c3 and call c1.increment() twice, c2.increment() once, what is c1.count, c2.count, c3.count, and Counter.totalCounters?

Easy

5. Refactor this class using encapsulation: public class Student { public String name; public int age; public double gpa; }

Medium

6. What is the output? public class Test { static int x = 10; int y = 20; public static void main(String[] args) { Test t1 = new Test(); Test t2 = new Test(); t1.y = 100; Test.x = 999; System.out.println(t2.y); System.out.println(t2.x); } }

Medium

7. Implement a simple 'Builder' pattern for a User class with fields: String name (required), String email (required), String phone (optional), int age (optional, default 0), String role (optional, default 'USER').

Medium

8. Rewrite this traditional POJO as a Java Record with validation: public class Coordinate { private final double lat; private final double lon; public Coordinate(double lat, double lon) { if (lat < -90 || lat > 90) throw new IllegalArgumentException("Invalid latitude"); if (lon < -180 || lon > 180) throw new IllegalArgumentException("Invalid longitude"); this.lat = lat; this.lon = lon; } public double lat() { return lat; } public double lon() { return lon; } // equals, hashCode, toString auto-generated... }

Medium

Conclusion — Classes & Objects: The Foundation of Every Java Program

Classes and objects are the foundation of Java. Every Java program is built from classes — even the simplest Hello World lives inside one. Understanding how classes define structure and behavior, how objects are created and live in heap memory, and how encapsulation protects data integrity is not just academic theory — it directly determines the quality of every line of Java code you write.

The difference between beginner and professional Java code is often visible in class design: beginners use public fields, forget encapsulation, create classes that do too much, and confuse reference copies for object copies. Professional Java code has private fields, validated setters, clearly named methods, single-responsibility classes, and meaningful toString()/equals()/hashCode() overrides.

ConceptPurposeKey Rule
ClassBlueprint defining fields + methodsOne public class per .java file; PascalCase name
ObjectRuntime instance created with 'new'Lives in heap; reference stored on stack
ConstructorInitializes object fields when createdSame name as class; NO return type
Instance field/methodPer-object state and behaviorEach object gets its own copy
Static field/methodClass-level shared data and utilitiesONE copy; access via ClassName.member
Access modifierControls visibility of membersFields: private; API methods: public
this keywordReference to current objectOnly in non-static context
EncapsulationData hiding + controlled accessprivate fields + validated getters/setters
Record (Java 16+)Compact immutable data classUse for DTOs, value objects — no setters
Garbage CollectionAutomatic heap memory managementObject eligible when ref count = 0

Your next step: Java Inheritance — where you'll learn how classes can extend other classes, inherit and override their behavior, and how Java's single-inheritance model combined with interfaces enables powerful, flexible object hierarchies. ☕

Frequently Asked Questions — Java Classes and Objects