Classes and Objects in Java
Learn how classes serve as blueprints for creating objects in Java, and how instantiation with the new keyword works.
Learn how classes serve as blueprints for creating objects in Java, and how instantiation with the new keyword works.
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 defines the blueprint for creating objects — it specifies the fields (state) that each instance holds and the methods (behavior) that operate on that state. When you write new Robot("Wall-E"), the Robot class constructor is invoked and memory is allocated for a new instance with its own copy of all instance fields. Multiple objects created from the same class share the same method code but maintain independent state values.
Objects live on the heap; references to them live on the stack. A reference variable holds the memory address of an object, not the object itself. This distinction matters because multiple references can point to the same object, and if one reference modifies the object’s state, all other references see the change. Understanding this shared-mutability model is critical for avoiding subtle bugs in code that appears correct in isolation.
This guide covers the anatomy of a class, how objects are instantiated, how references and objects relate to each other in memory, and the design principles that separate good class design from problematic patterns. Understanding classes and objects is the prerequisite for all other object-oriented concepts in Java — inheritance, polymorphism, encapsulation, and abstraction all build on this foundation.
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
staticmethods in a utility class instead - Single-value data containers — consider a
recordin 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
finalfields 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
- Creating too many classes — each class should earn its place
- God objects — classes that do too much; split into focused units
- Tight coupling — classes that depend heavily on each other’s internals
- Mutable fields that shouldn’t be — default to
finalwhere possible - 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
finalfields and no setters
Interview Questions
Further Reading
- Constructors — initializing objects with constructors and constructor overloading
- Fields and Instance Variables — managing object state
- Encapsulation — hiding internal state with access modifiers
- Inheritance — subclassing and the extends keyword
- Polymorphism — runtime method dispatch through inheritance
- Oracle: Creating Classes — official documentation on class declaration syntax
- Effective Java: Item 1 — consider static factory methods instead of constructors
Conclusion
Classes and objects form the bedrock of Java’s object-oriented model. A class defines the blueprint — the fields that hold state and the methods that provide behavior — while objects are live instances created via the new keyword, each with their own memory for instance fields.
Understanding the distinction between references and objects is critical: a reference is a pointer to an object’s location in heap memory, and multiple references can point to the same object. This is why defensive copying matters for mutable types.
The class anatomy diagram shows how fields (state) and methods (behavior) work together. When designing classes, favor encapsulation by keeping fields private and exposing controlled access points. This prevents external code from putting objects into invalid states.
For one-off behavior, anonymous classes and lambda expressions (for functional interfaces) let you create objects without formal class definitions. Static factory methods offer an alternative to constructors for cases where you need to return cached instances, subclasses, or want more expressive creation syntax.
Classes integrate closely with other OOP concepts: constructors (covered in Java Constructors) initialize new instances, fields (detailed in Fields and Instance Variables) hold the per-object state, and methods define behavior that subclasses can override to achieve polymorphism (explored in Polymorphism in Java).
Category
Related Posts
Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.