Classes and Objects in Java

Learn how classes serve as blueprints for creating objects in Java, and how instantiation with the new keyword works.

published: reading time: 18 min read author: Geek Workbench
Quick Summary

Learn how classes serve as blueprints for creating objects in Java, and how instantiation with the new keyword works. The guide uses practical examples to explain when to use, when not to use and shows how to apply the ideas in a Spring Boot project. It closes with common pitfalls and production checks so you can apply the pattern with fewer surprises.

Classes and Objects in Java

Every Java application is built on classes and objects. They are the foundation of object-oriented programming — the blueprint versus the building.

Introduction

A class is a blueprint — a template that defines what an object of that type looks like: what data it holds (fields), what operations it supports (methods), and how it gets created (constructors). An object is a live instance built from that blueprint, allocated on the heap with its own copy of the class’s fields.

Think of it like architectural plans and the buildings constructed from them. The plans never house anyone — they are just specifications. The buildings are what people actually live in. In Java, class is the plan; new ClassName() is the construction crew producing a habitable object.

Classes and objects are the primary units of organization in Java. Everything executable lives inside a class or is itself an object. The String class defines the blueprint for string objects. The Robot class defines the blueprint for robot objects. When you write new Robot(“R2-D2”), you are instantiating — creating a real robot object from the Robot blueprint.

Java is fundamentally an object-oriented language. Even primitive types (int, boolean, double) are not objects — they are raw values. Everything else in Java revolves around objects and the classes that define them. Understanding this class-to-object relationship is the single most important foundation for writing Java code.

This post covers how classes work, when to use them versus alternatives, their internal anatomy, common failure patterns, and practical patterns like static factory methods and nested classes.

When to Use

Use a class when you need to:

  • Model real-world entities with state (fields) and behavior (methods)
  • Encapsulate related data and logic that belong together
  • Create multiple instances sharing the same structure but different state
  • Organize code into logical units that can be tested and maintained independently
// Defining a class — the blueprint
public class Robot {
    // Fields — state
    private String name;
    private int batteryLevel;

    // Constructor — initializes new instances
    public Robot(String name) {
        this.name = name;
        this.batteryLevel = 100;
    }

    // Method — behavior
    public void charge() {
        this.batteryLevel = Math.min(100, batteryLevel + 20);
    }
}

// Instantiating objects — the buildings
Robot r2d2 = new Robot("R2-D2");
Robot c3po = new Robot("C-3PO");
// Anonymous class for one-off behavior
Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("Executing task");
    }
};

When Not to Use

Avoid classes for:

  • Pure utility functions — use static methods in a utility class instead
  • Single-value data containers — consider a record in Java 16+
  • Data that only wraps primitives — use primitives directly or record
  • One-off scripts — top-level code in a main method may suffice for simple programs
// Don't do this — unnecessary class for a simple operation
class StringUtils {
    public static String capitalize(String s) {
        return s.isEmpty() ? s : Character.toUpperCase(s.charAt(0)) + s.substring(1);
    }
}
// Better: use a simple static utility class is fine, but for simple capitalize, consider if it's truly needed

Class Anatomy — Mermaid Diagram

classDiagram
    class Robot {
        -String name
        -int batteryLevel
        +Robot(String name)
        +charge() void
        +getName() String
    }
    Robot : +new Robot("R2-D2")

Failure Scenarios

1. Uninitialized Reference

When you declare a reference variable without assigning it a value, the variable holds a null reference. Calling a method on a null reference triggers a NullPointerException at runtime. The JVM has no object to dispatch the method call to, since there is no actual instance behind the variable.

Uninitialized references commonly arise from early returns in constructors, conditional initialization paths that the compiler cannot fully trace, or developers simply forgetting to wire up an assignment. The compiler cannot catch this because object references are default-initialized to null, which is a valid type value, not an error.

Robot robot;           // Declared but not assigned
robot.charge();       // NullPointerException — robot is null

// Fix: always initialize before use
Robot robot = new Robot("Wall-E");

2. Mutable Shared State via References

Aliasing happens when two reference variables point to the same object in heap memory. Because objects are reference types, assigning one reference to another does not copy the object. Both variables now share the same underlying instance. Modifying state through one reference is immediately visible through the other, which can silently corrupt data in code that appears correct in isolation.

This is especially dangerous in multi-threaded programs where one thread might modify a shared collection while another iterates over it, or in single-threaded code where a method stores a reference to an internal object and the caller later mutates it. Defensive copying (creating a new instance before returning or storing) prevents external code from holding references into your object’s internals.

List<String> list1 = new ArrayList<>();
List<String> list2 = list1;    // Both reference the SAME list
list2.add("item");             // Modifies list1 too

// Fix: create independent copies
List<String> list2 = new ArrayList<>(list1);

3. Forgetting the new Keyword

Java requires the new keyword to instantiate a class because object creation involves two distinct steps: allocating memory on the heap and then invoking a constructor to initialize that memory. Writing Robot(“Test”) without new looks like a method call to the compiler, not a constructor invocation, so the compiler raises a “cannot find symbol” error pointing at Robot. This is a compile-time error, not a runtime one, which means the fix is always immediate and obvious once you try to build.

Factory methods like String.valueOf(42) are different. They are static methods that happen to return new objects, but they are called like any other static method. Contrast that with constructors, which are only callable through new. Knowing the difference matters when you are reading code: if you see ClassName(args) without new, you are looking at a static factory method, not a constructor call.

String s = String.valueOf(42);  // Factory method — correct
Robot r = Robot("Test");        // Compile error — forgot new
Robot r = new Robot("Test");     // Correct

Trade-off Table

Approach Use Case Drawback
Concrete class Full control over behavior and state More boilerplate
Abstract class Shared base with partial implementation Single inheritance limit
Interface Multiple contracts without implementation No state
Record (Java 16+) Immutable data carrier Cannot hold mutable state
Enum Fixed set of constants Not extendable at runtime

Code Snippets

Static Factory Method Pattern

Static factory methods are named methods that return new instances of the class, replacing or supplementing constructors. Unlike constructors, they have names. List.of(), Path.of(), and Optional.of() are all factory methods you already use. The name makes the intent clear: createVacuumBot(“Roomba”) tells you exactly what kind of robot you are getting, whereas a constructor call new Robot(“Roomba”) gives you no semantic hint.

Factory methods also let you return existing instances instead of creating new ones every time. Integer.valueOf(42) returns a cached instance for values between -128 and 127, avoiding the allocation overhead of new Integer(42). They can also return subclasses, which is useful when the class hierarchy has private constructors and you want to control which concrete type gets instantiated. Unlike constructors, which must always produce a fresh object, factory methods can return null or hand back a cached instance instead.

The tradeoff is that subclasses cannot use factory methods from their parent class, and the methods are not distinguished by signature alone. createVacuumBot(String) and createSecurityBot(String) are separate methods, not overloads of a single constructor.

public class Robot {
    private final String name;

    private Robot(String name) {  // Private constructor
        this.name = name;
    }

    // Static factory method instead of public constructor
    public static Robot createVacuumBot(String name) {
        return new Robot(name);
    }

    public static Robot createSecurityBot(String name) {
        Robot bot = new Robot(name);
        // Security bot specific setup
        return bot;
    }
}

// Usage
Robot vac = Robot.createVacuumBot("Roomba");
Robot sec = Robot.createSecurityBot("Guard");

Nested Class

Java lets you declare a class inside another class — these are called nested classes. The enclosing class is the outer class; the declared one is the nested class. Nested classes are useful when a class only makes sense in the context of its outer class, or when you want to group closely related code without exposing it to the rest of the package.

Java has four kinds of nested classes:

Kind Keyword Access to Outer Instance Instantiation
Static nested class static No new Outer.Inner()
Member inner class (none) Yes — implicit Outer.this outer.new Inner()
Local inner class (none, inside method) Yes — from enclosing method Inside that method only
Anonymous class (no name) Inherits or implements one type Inline in expression

Static nested classes are the simplest — they behave like top-level static methods. They cannot access outer class instance fields directly because they have no implicit reference to an outer instance. Use them for logically grouping a class that doesn’t need to reach into the outer class’s state.

Non-static member inner classes are different. Because they are implicitly tied to an outer instance, they can read and modify outer class fields directly — no getter needed. The outer.new Inner() syntax reflects this: you need an outer instance before you can create the inner instance. This tight coupling is useful for helper classes that collaborate closely with their outer class, but be careful — it also makes the inner class harder to test in isolation and can create memory leaks if the inner instance outlives the outer instance.

Local inner classes are declared inside a method body. They can access effectively final local variables from the enclosing method. Anonymous classes are a special case — they are local inner classes with no name, defined and instantiated in a single expression. Both are less common in modern Java; lambda expressions have replaced many anonymous class use cases, especially for functional interfaces.

public class Outer {
    private String outerField = "outer";

    public class Inner {
        private String innerField = "inner";
        public void accessOuter() {
            System.out.println(outerField);  // Can access outer class field
        }
    }
}

// Instantiate inner class via outer instance
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();

Observability Checklist

  • Fields are private with controlled access via getters/setters
  • Constructor validates required parameters
  • Immutable classes use final fields and no setters
  • Static fields documented with their purpose
  • Thread-safe design for shared instances

Security Notes

  • Encapsulate fields — never expose internal state directly
  • Defensive copies — when returning collections from getters, return copies not references
  • Immutable objects — prefer immutability to avoid race conditions
  • Input validation — validate all constructor and setter parameters
public class User {
    private final List<String> roles;  // Mutable field

    public List<String> getRoles() {
        return List.copyOf(roles);  // Return defensive copy — prevents external modification
    }
}

Pitfalls

  1. Creating too many classes — each class should earn its place
  2. God objects — classes that do too much; split into focused units
  3. Tight coupling — classes that depend heavily on each other’s internals
  4. Mutable fields that shouldn’t be — default to final where possible
  5. Reassigning parameters — don’t modify parameter values inside methods
// Bad: modifying parameter
public void process(User user) {
    user = new User();  // This only changes local copy, not the caller's reference
}

// Good: operate on the object, don't reassign
public void process(User user) {
    user.updateStatus("active");  // Call methods on the object
}

Quick Recap

  • Class = blueprint defining state (fields) and behavior (methods)
  • Object = instance created from a class via new
  • Reference = variable holding object’s memory address
  • Encapsulation = keep fields private, expose via methods
  • Immutability = use final fields and no setters

Interview Questions

1. What is the difference between a class and an object?
A class is a blueprint or template that defines the structure (fields) and behavior (methods) that objects of that type will have. An object is a concrete instance created from that blueprint, occupying its own memory with actual values for the fields.
// Class — the blueprint
public class Robot {
    private String name;
    public Robot(String name) { this.name = name; }
    public String getName() { return name; }
}

// Object — a real instance created from the blueprint
Robot bot = new Robot("R2-D2");  // new triggers constructor
String name = bot.getName();      // "R2-D2"
2. Can a class have multiple constructors?
Yes. Java supports constructor overloading — multiple constructors with different parameter lists. This allows flexible object creation with different initial states.
3. What is the default value of an object reference field?
The default value is null. Unlike primitive types which have specific defaults (0, false, etc.), object references default to null until explicitly initialized.
4. What is the difference between new and factory methods?
new always creates a fresh instance via a constructor. Factory methods (like List.of() or Path.of()) can return cached instances, existing objects, or subclasses — the caller receives an instance without knowing or caring about the concrete type.
Robot r = new Robot("Wall-E");       // new — always creates a new object
List<String> list = List.of("a","b"); // factory — may return cached instance
5. When should you use a record instead of a class?
Use a record for immutable data carriers where the main purpose is to hold values (like DTOs, transfer objects, results). Records automatically get equals(), hashCode(), toString(), and constructor parameter accessors — reducing boilerplate significantly.
6. What happens when you try to instantiate an interface directly in Java?
Compilation error — interfaces cannot be instantiated with new. Interfaces are abstract contracts and require a concrete implementation class.
Runnable r = new Runnable();  // Compile error: Runnable is abstract; cannot be instantiated
Runnable r = new Runnable() { // Correct: implement it with an anonymous class
    public void run() { System.out.println("task"); }
};
7. Can a class have both instance methods and static methods? Explain the difference.
Yes — instance methods require an object (via new) and operate on instance state. Static methods belong to the class itself, not instances — called via ClassName.method(). Static methods cannot access instance fields directly without an object reference.
8. What is the difference between a nested class and an inner class in Java?
A static nested class is independent of any outer instance — instantiate it with new OuterClass.InnerClass(). A non-static inner class holds an implicit reference to its outer instance and can access outer class members directly. Inner classes require an outer instance to create; static nested classes do not.
// Static nested — no outer instance needed
Outer.StaticNested n = new Outer.StaticNested();

// Non-static inner — requires an outer instance
Outer outer = new Outer();
Outer.Inner i = outer.new Inner();  // note the outer.new syntax
9. Why should mutable objects stored as fields be defensive-copied in constructors and getters?
Prevents external code from modifying internal state — maintains encapsulation. If you store a direct reference, caller can change the object and affect your invariants. Defensive copy (new ArrayList<>(list) or List.copyOf()) gives you independent ownership.
10. What is method signature and does it include the return type?
Method signature includes method name and parameter types (e.g., doSomething(String, int)). Return type is NOT part of the method signature in Java. Two methods with same name and params but different return types cause compilation error.
11. What is the purpose of the static keyword on a nested class?
Makes the nested class independent of any outer class instance. Static nested class cannot access outer class instance fields or methods directly. Useful when nested class doesn't need to reference outer class instances.
12. What is an anonymous class and when would you use one?
An anonymous class is a local class with no name, defined and instantiated in a single expression. Useful for one-off interface implementations or extending classes on the fly.
Runnable task = new Runnable() {  // anonymous class implementing Runnable
    public void run() { System.out.println("running"); }
};
// Lambda would be shorter: () -> System.out.println("running")
13. Can a class be declared as both final and abstract?
No — these are mutually exclusive modifiers. abstract means the class must be subclassed; final means the class cannot be subclassed. Compilation error results from this combination.
14. What is the difference between member nested class and local inner class?
Member inner class is declared at class level (as a member of the outer class). Local inner class is declared inside a method body. Local inner class can access effectively final local variables of the enclosing method.
15. What happens when a class has no explicit constructor definition?
Java compiler provides a default no-arg constructor automatically. This default constructor initializes all instance fields to default values (0, false, null). Once you define any constructor, the default is no longer provided.
16. What is the relationship between class and interface for multiple inheritance?
Java class supports single inheritance only (one extends clause). Class can implement multiple interfaces (implements clause accepts comma-separated list). Interface can extend multiple interfaces (allows contract composition without state inheritance).
17. When is it appropriate to use var (local variable type inference) in Java 10+?
Useful for long generic type names or complex generic chains. Cannot use var for fields, method parameters, or constructor parameters. Must initialize var on declaration — compiler infers exact type from initializer.
18. What is the purpose of a private constructor in a class?
Prevents direct instantiation from outside the class. Common in the Singleton pattern — the class itself controls how many instances exist. Also used in the Factory pattern to force callers through a factory method instead of calling new directly.
public class DatabaseConnection {
    private static DatabaseConnection instance;
    private DatabaseConnection() {}  // private — no external new

    public static DatabaseConnection getInstance() {
        if (instance == null) instance = new DatabaseConnection();
        return instance;
    }
}
DatabaseConnection db = DatabaseConnection.getInstance();  // correct
DatabaseConnection db2 = new DatabaseConnection();           // compile error
19. What is the difference between an object reference and a primitive variable?
Primitives store actual values (int, boolean, double) directly on the stack. Object references store memory addresses pointing to heap-allocated objects. Reference default is null; primitives have type-specific defaults (0, false, 0.0).
20. What is the diamond problem and how does Java handle multiple inheritance?
Diamond problem occurs when a class inherits from two classes that have a common ancestor. Java does not support multiple class inheritance, avoiding this issue. Interfaces can extend multiple interfaces, providing contract composition without state inheritance.

Further Reading

Conclusion

Classes and objects are the bedrock of Java’s object-oriented model. A class is the blueprint; an object is the thing built from that blueprint, created via the new keyword, with its own chunk of heap memory for instance fields.

The reference-object distinction matters more than it first appears. A reference is just a pointer to where the object lives. Multiple references can point to the same object — aliasing — which silently bites you when one reference modifies state and another reference sees the change. Defensive copying, final fields, and private fields with controlled access points are the tools for managing this.

Static factory methods deserve more attention than they usually get. List.of(), Path.of(), Optional.of() — you use these every day. They return cached instances, subclasses, or fresh ones depending on what makes sense. Constructors always produce new objects; factory methods give you flexibility constructors can’t match.

Anonymous classes and lambdas handle one-off behavior without formal class definitions. The four kinds of nested classes cover most other cases, from static nested classes (plain old class-like constructs) to anonymous classes (inline one-offs).

Category

Related Posts

Abstract Classes in Java

Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals