Classes, Objects & Instances
In simple words
A class is a blueprint — a description of what something has and what it can do, like an architect's drawing of a house. An object is a real thing built from that blueprint, like an actual house standing on a street. One blueprint can build as many objects as you like; they all have the same shape, but each one has its own values. Instance is just another word for an object — account1 is an instance of the class BankAccount.
What is a class?
A class is a blueprint that defines the structure and behaviour that all objects of that type will have. It defines what data (fields) and what actions (methods) the object will have.
A class itself does nothing — it's just a definition. No memory is allocated for a class until an object is created from it.
// This is a CLASS — just a blueprint, nothing is created yet
class BankAccount {
// Fields — what data every BankAccount object will hold
private String accountId;
private String ownerId;
private double balance;
// Constructor — how to create a BankAccount object
public BankAccount(String accountId, String ownerId, double initialBalance) {
this.accountId = accountId;
this.ownerId = ownerId;
this.balance = initialBalance;
}
// Methods — what actions every BankAccount object can do
public void credit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
this.balance += amount;
}
public void debit(double amount) {
if (amount > this.balance) throw new IllegalStateException("Insufficient balance");
this.balance -= amount;
}
public double getBalance() {
return this.balance;
}
}
What is an object?
An object is a concrete instance of a class — created in memory using the new keyword. Each object has its own copy of the fields defined in the class.
// Creating OBJECTS from the BankAccount class
// Each object is independent — its own data, its own memory
BankAccount account1 = new BankAccount("ACC001", "user_alice", 5000.00);
BankAccount account2 = new BankAccount("ACC002", "user_bob", 2000.00);
BankAccount account3 = new BankAccount("ACC003", "user_carol", 10000.00);
// Each object has its own state
account1.debit(1000);
System.out.println(account1.getBalance()); // 4000.00 — only account1 changed
System.out.println(account2.getBalance()); // 2000.00 — account2 unaffected
Class (blueprint): Objects (created from blueprint):
+-----------------+ +----------------------------+
| BankAccount | ---> | account1: ACC001, 5000.00 |
| - accountId | +----------------------------+
| - ownerId | ---> | account2: ACC002, 2000.00 |
| - balance | +----------------------------+
| + credit() | ---> | account3: ACC003, 10000.00 |
| + debit() | +----------------------------+
+-----------------+ Each has its own field values.
All share the same methods.
Try it — build objects from one class and see that they don't affect each other:
What is an instance?
Instance and object mean the same thing.
account1 is an instance of BankAccount
account2 is an instance of BankAccount
account3 is an instance of BankAccount
"creating an object" = "instantiating a class"
The only subtle difference in usage:
- "Object" emphasises the thing in memory
- "Instance" emphasises the relationship to the class it came from
In interviews and real conversations, they are used interchangeably.
Instance variables vs class (static) variables
| Type | Belongs to | One copy per | Access |
|---|---|---|---|
| Instance variable (field) | Each object | Each object | object.field |
| Static variable | The class | Entire class (per JVM) | ClassName.field |
class UserSession {
// INSTANCE variable — each session object has its own sessionId
private String sessionId;
private String userId;
private long expiresAt;
// STATIC variable — shared across ALL UserSession objects
private static int activeSessions = 0; // counts all active sessions
private static final int MAX_SESSIONS = 10000; // constant
public UserSession(String userId) {
this.sessionId = UUID.randomUUID().toString();
this.userId = userId;
this.expiresAt = System.currentTimeMillis() + 3600000;
activeSessions++; // increment shared counter
}
public void invalidate() {
activeSessions--; // decrement when session ends
}
public static int getActiveSessions() {
return activeSessions;
}
}
UserSession s1 = new UserSession("user_alice");
UserSession s2 = new UserSession("user_bob");
System.out.println(UserSession.getActiveSessions()); // 2 — accessed via class name
System.out.println(s1.sessionId); // unique to s1
System.out.println(s2.sessionId); // different value, unique to s2
Instance methods vs static methods
class MathUtils {
// STATIC method — no object needed, operates on parameters only
public static int add(int a, int b) {
return a + b;
}
}
int result = MathUtils.add(3, 5); // called on the CLASS, no object created
class Counter {
private int count = 0; // instance variable
// INSTANCE method — operates on this object's state
public void increment() { count++; }
public int getCount() { return count; }
}
Counter pageCounter = new Counter();
Counter loginCounter = new Counter();
pageCounter.increment();
pageCounter.increment(); // pageCounter.count = 2
loginCounter.increment(); // loginCounter.count = 1 (independent)
Object lifecycle
1. Declaration -> BankAccount account; (reference variable, no object yet)
2. Instantiation -> account = new BankAccount(...) (object created in heap memory)
3. Initialisation -> constructor runs, fields set
4. Usage -> account.credit(500)
5. Garbage collected -> when no references point to it (the JVM handles this)
Memory — where objects live
Stack Memory Heap Memory
------------ -----------
account1 ---------------------> { accountId:"ACC001", balance: 5000 }
account2 ---------------------> { accountId:"ACC002", balance: 2000 }
account3 ---------------------> { accountId:"ACC003", balance: 10000 }
- Stack: holds reference variables (the pointers)
- Heap: holds the actual object data
- Multiple references can point to the same object:
BankAccount acc = new BankAccount("ACC001", "alice", 5000);
BankAccount alias = acc; // both point to the SAME object in the heap
alias.debit(1000);
System.out.println(acc.getBalance()); // 4000 — same object changed
Where this shows up in system design
Every service you draw is a class. When you say "Order Service" in an interview, this is literally what you mean:
class OrderService {
private OrderRepository orderRepo; // dependencies (other classes)
private InventoryService inventoryService;
private NotificationService notificationService;
public Order placeOrder(OrderRequest request) { ... } // core behaviour
public Order getOrder(String orderId) { ... }
public void cancelOrder(String orderId) { ... }
}
Entities / domain models are classes — UserService manages User objects, PaymentService manages Payment and Transaction objects.
Spring beans are singleton objects — Spring creates one instance of a @Service class and shares it across every request. That one instance must be stateless or thread-safe (which is why we inject repositories and don't store mutable state in service fields).
Senior-level nuances (these are what actually get tested)
Nobody asks "what is a class" at 10 years' experience. They ask things downstream of it:
1. Identity vs equality. == compares references — is it literally the same object on the heap. .equals() compares meaning. Override equals() and hashCode() together, on the same fields, ideally an immutable business key (orderId) — never a mutable field. If an object goes into a HashSet or is used as a Map key and you skip one of the two, you get silent bugs (duplicates stored, contains() misses).
2. Value vs reference semantics. Passing an object passes the reference. A method can mutate your object. Defend with a defensive copy on the way in/out, or make the object immutable.
3. Immutability. An immutable object (all fields final, set in the constructor, no setters) is automatically thread-safe and safe to share and cache. Default domain models and DTOs to immutable; make things mutable only when you have a reason.
4. static is not a free global. activeSessions++ is read-modify-write and not thread-safe — needs AtomicInteger. And static is per-JVM, so with 4 server instances there are 4 separate counters — it is not a global count. A real global counter needs a shared store (Redis) or a metrics system. Static mutable state also leaks between unit tests.
In the interview — the one-liner
"A class is the blueprint — fields and methods. An object is a concrete instance on the heap, created with
new; each object has its own instance fields but sharesstaticfields across the whole class per JVM. In practice the thing that bites you is a singleton service holding mutable instance state across request threads, or a domain object used as a map key withoutequals/hashCode— so I keep services stateless and domain models immutable."
Quick revision — 6 lines
- Class = blueprint. Object / instance = a thing built from the blueprint.
- Each object has its own instance variables;
staticvariables are shared across all objects (per JVM). - Instance methods need an object; static methods belong to the class.
- Objects live on the heap; references live on the stack. Two references can point to one object.
- In Spring, service beans are singleton objects — must be stateless or thread-safe.
- What gets tested: identity vs equality (
==vsequals/hashCode), reference semantics, immutability,staticnot being thread-safe or global.
Optional. Everything above is enough for the interview. These are curated free articles for the full mechanism behind points the note only touches — read them when you want the why, not just the what.
equals() and hashCode() — where they come from and why they must agree
Every class silently inherits equals() and hashCode() from Object. The inherited equals() compares identity — "is this the exact same object on the heap?" — which is what == already does for references. Primitives compare by value with ==; objects do not. So the moment you want new Money(100, "INR").equals(new Money(100, "INR")) to be true, or you want to drop your object into a HashSet or use it as a HashMap key, you must override equals() to compare the fields that define equality — and override hashCode() to match, because hash-based collections locate a bucket via hashCode() first and only then call equals(). The contract: equal objects must return equal hash codes; unequal objects may collide; and the value must not change while the object sits in a collection — so base both on an immutable key like orderId.
Where the JVM stores static vs non-static members
The JVM keeps runtime data in three areas that matter here:
HEAP (one, shared by every thread, garbage-collected)
- every object made with `new`
- each object's INSTANCE fields sit inside that object
- one Class object per loaded class -> holds that class's STATIC fields
- the String pool
METASPACE ("method area" — native memory, one per JVM)
- class metadata: the description of each field and method
- the BYTECODE of every method — static AND instance
- the runtime constant pool
STACK (ONE PER THREAD)
- one "frame" is pushed each time a method is called
- the frame holds that call's LOCAL variables, parameters, and `this`
- the frame is popped (its locals vanish) when the method returns
Where each member ends up:
| Member | Stored where | How many copies |
|---|---|---|
static field (class variable) | In the class's single Class object, which lives on the heap (Java 8+; older material says "Method Area / PermGen"). Created when the class is loaded, set in the class initialiser. | one per class — shared by every object |
| instance field (non-static) | Inside each object, on the heap. Born when you new the object, gone when it is garbage-collected. | one per object |
static method — its code | Metaspace — method bytecode is stored once per class. | one per class |
| instance method — its code | Metaspace, exactly the same. Objects do not carry method code. | one per class |
| local variables & parameters | On the running thread's stack, inside that method's frame. A primitive local holds the value; an object local holds a reference to a heap object. | one per method call |
this | A hidden parameter, so also on the stack frame — a reference pointing at the heap object. | one per call |
Three things to remember:
static= one copy, tied to the class. It exists before any object is created and lives as long as the class stays loaded.- Non-static (instance) = one copy per object, on the heap.
newjust makes another set of instance fields. - Method code is never copied per object — it is stored once per class in Metaspace. What is created per call is a stack frame holding that call's local variables.
Common misconceptions:
-
"Instance methods live inside each object." No — only the data (instance fields) is per-object; the code is shared by the whole class.
-
"Static variables are on the stack." No — the stack only holds per-call locals. Statics are class-level, on the heap in the
Classobject. -
"A local object is on the stack." The reference is on the stack; the object itself is always on the heap. This is why two references can point to one object.
How HashMap and HashSet work internally
A HashMap is an array of buckets. On put(key, value) it calls key.hashCode(), spreads the bits, and takes that modulo the array length to choose a bucket. Empty bucket → the entry goes straight in. Occupied bucket (a collision) → the entry is added to that bucket, which is a linked list — and since Java 8, once one bucket holds 8+ entries (and the table has ≥ 64 buckets) it converts to a balanced red-black tree so lookups in a hot bucket stay O(log n) instead of O(n). get(key) repeats the hash to find the bucket, then walks it calling equals() to pick the exact key. When the map passes its load factor (0.75 — i.e. 75% full) it doubles the array and rehashes every entry. HashSet is literally a HashMap where every key maps to one shared dummy value — which is why a HashSet element needs equals() and hashCode() exactly like a map key.
Immutability — beyond a single final
final on a field only stops reassignment of that field. If the field points at a mutable object (an array, a List, a Date), callers can still mutate what it points to. A properly immutable class: mark the class final (so nobody subclasses it to add mutable state), make every field private final, set them only in the constructor, expose no setters, and defensively copy any mutable field both on the way in (constructor) and on the way out (getter) so no outside reference can reach the internal state. Immutable objects are automatically thread-safe and safe to share and cache — which is exactly why String is immutable.