Definition
An instance variable has four properties:
- Per-object. Every object created from the class gets its own independent copy. Changing Rajesh's balance does not affect Priya's.
- Auto-initialized. If you do not assign a value, Java gives it a safe default:
0for numbers,falsefor boolean,'\u0000'for char, andnullfor references. - Object-scoped. It is visible to every constructor, instance method, and instance initializer block in the class.
- Heap-resident. It lives on the heap as part of the object and survives across method calls until the object is garbage collected.
Think of a bank passbook. Every customer gets their own passbook with their own balance, name, and transaction count printed inside. If the bank opens a new passbook and forgets to write a starting balance, the printer fills in zero by default. Any teller at the branch can read or update the passbook. And the passbook exists as long as the account is open. That is exactly how instance variables work.
Understanding the Concepts
What is an instance variable?
An instance variable is a field declared directly inside a class body, outside any method, constructor, or block, without the static keyword. It belongs to each individual object, not to the class itself.
public class Account {
// These are instance variables – each Account object has its own copy
private String accountId;
private String holderName;
private BigDecimal balance;
private int transactionCount;
private boolean active;
}
When you write new Account(), Java allocates space on the heap for all five fields. When you write new Account() again, Java allocates a completely separate set of five fields. The two objects share nothing.
How is it different from a local variable and a static variable?
Java has three kinds of variables. They differ in where they are declared, where they live in memory, and how long they last.
| Property | Instance variable | Local variable | Static variable |
|---|---|---|---|
| Declared in | Class body, outside methods | Inside a method, constructor, or block | Class body, with static keyword |
| Belongs to | Each object | The method call | The class itself |
| Lives in | Heap (inside the object) | Stack (inside the stack frame) | Heap (in the class metadata area) |
| Default value | Yes (0, false, null) | No – must be assigned before use | Yes (0, false, null) |
| Lifetime | Until object is garbage collected | Until the method returns | Until the class is unloaded |
| Thread safety | Shared if object is shared | Thread-safe by default (stack-confined) | Shared across all threads |
Here is a class that shows all three side by side:
public class VariableTypes {
// Instance variable – one per object
private int instanceCounter = 0;
// Static variable – one for the entire class, shared by all objects
private static int staticCounter = 0;
public void process() {
// Local variable – exists only during this method call
int localCounter = 0;
instanceCounter++;
staticCounter++;
localCounter++;
System.out.println("instance=" + instanceCounter
+ " static=" + staticCounter
+ " local=" + localCounter);
}
}
VariableTypes a = new VariableTypes();
VariableTypes b = new VariableTypes();
a.process(); // instance=1 static=1 local=1
a.process(); // instance=2 static=2 local=1 (local resets every call)
b.process(); // instance=1 static=3 local=1 (instance is per-object, static is shared)
Notice that localCounter is always 1 because it is created fresh on every method call. instanceCounter grows per object. staticCounter grows across all objects because there is only one copy.
What default values do instance variables get?
If you do not explicitly assign a value, Java assigns a safe default. This is different from local variables, which have no default and cause a compile error if you read them before assigning.
byte,short,int,longdefault to0floatdefaults to0.0f,doubledefaults to0.0chardefaults to'\u0000'(the null character)booleandefaults tofalse- Any object reference (
String,BigDecimal,Account) defaults tonull
public class DefaultValues {
private int count; // 0
private double rate; // 0.0
private boolean active; // false
private String name; // null
private BigDecimal balance; // null
public void print() {
// This compiles and runs fine – all fields have defaults
System.out.println(count + " " + rate + " " + active + " " + name + " " + balance);
// Output: 0 0.0 false null null
}
}
Be careful with reference defaults. If balance is null and you call balance.add(...), you get a NullPointerException. Always initialize reference fields in the constructor if they will be used immediately.
What are the three ways to initialize an instance variable?
You can set the value in three places, and they execute in a specific order during object construction:
1. At the point of declaration
public class Account {
private boolean active = true; // initialized right here
private int transactionCount = 0; // explicit, though int defaults to 0 anyway
}
2. In an instance initializer block
An instance initializer block is a bare pair of braces { } inside the class body. It runs for every object, every time.
public class Account {
private boolean active;
private LocalDateTime createdAt;
// Instance initializer block
{
active = true;
createdAt = LocalDateTime.now();
}
}
This is useful when the initialization logic is shared by all constructors and is too complex for a simple inline assignment.
3. In a constructor
public class Account {
private final String accountId;
private String holderName;
private BigDecimal balance;
public Account(String accountId, String holderName, BigDecimal balance) {
this.accountId = accountId;
this.holderName = holderName;
this.balance = balance;
}
}
What is the construction order?
When you call new Account(...), Java executes these steps in this exact order:
super()runs first. The parent class constructor (at minimum,Object()) executes.- Field initializers and instance initializer blocks run in source order. If you have
private boolean active = true;on line 3 and an init block on line 5, the field assignment runs first, then the block. - The constructor body runs. This is where
this.balance = balance;happens.
This order matters because it means you cannot read a constructor parameter from an instance initializer block. The block runs before the constructor body.
public class Account {
private boolean active;
private BigDecimal balance;
// Runs BEFORE the constructor body
{
active = true;
// balance is still null here – constructor has not run yet
}
public Account(BigDecimal balance) {
// Runs AFTER the init block
this.balance = balance;
}
}
What is variable shadowing and why does it matter?
Shadowing happens when a method parameter or local variable has the same name as an instance variable. Inside that method, the local name wins and hides the field.
This is one of the most common bugs in Java:
// BUG: this setter does nothing
public void setBalance(BigDecimal balance) {
balance = balance; // assigns parameter to itself – field is unchanged!
}
// FIX: use this. to refer to the field
public void setBalance(BigDecimal balance) {
this.balance = balance; // field = parameter
}
The this keyword refers to the current object. Writing this.balance explicitly means "the instance variable named balance in this object," which avoids the ambiguity.
Make it a habit to always use this. in constructors and setters.
What access modifiers should you use?
The standard practice is to keep instance variables private and provide access through methods. This is called encapsulation.
privatemeans only the class itself can read or write the field. This is the default choice for banking entities.protectedmeans subclasses and classes in the same package can access it. Use rarely.- Default (no keyword) means package-private access. Avoid for domain objects.
publicmeans anyone can read and write the field directly. Never use this for domain fields.
Why does this matter? If balance is public, any code anywhere can set it to a negative number. If it is private, you control access through a debit() method that validates the amount first:
public class Account {
private BigDecimal balance; // private – only this class can touch it
public void debit(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if (balance.compareTo(amount) < 0) {
throw new IllegalStateException("Insufficient funds in account");
}
balance = balance.subtract(amount);
}
public BigDecimal getBalance() {
return balance; // read-only access
}
}
What happens with inheritance?
When a subclass extends a parent class, it inherits all the parent's instance variables. The subclass object contains both the parent's fields and its own fields.
public class Account {
private String accountId;
private BigDecimal balance;
}
public class SavingsAccount extends Account {
private double interestRate; // additional field
}
A SavingsAccount object has three fields on the heap: accountId and balance from Account, plus interestRate from SavingsAccount.
Important: if the parent field is private, the subclass cannot access it directly. It must use the parent's getters and setters. This is encapsulation working correctly across inheritance.
Java does not support instance variable overriding. If the subclass declares a field with the same name as the parent, it creates a separate field that hides the parent's field. This is called field hiding, and it is almost always a mistake:
public class Account {
protected String type = "GENERAL";
}
public class SavingsAccount extends Account {
protected String type = "SAVINGS"; // hides parent's type – confusing!
}
Account a = new SavingsAccount();
System.out.println(a.type); // prints "GENERAL" – resolves by reference type, not object type
Avoid field hiding. Use methods (which are properly overridden) instead.
What exceptions can occur?
The most common exception related to instance variables is NullPointerException. It happens when a reference field has its default value null and you try to call a method on it:
public class Account {
private BigDecimal balance; // defaults to null
public void showBalance() {
// NullPointerException! balance is null
System.out.println(balance.toPlainString());
}
}
The fix is to always initialize reference fields in the constructor, or check for null before using them.
What about thread safety?
Instance variables live on the heap. If two threads share the same object and both modify an instance variable, you can get a race condition.
// UNSAFE: two threads debiting the same account simultaneously
Account shared = new Account("SAV2001", "Rajesh", new BigDecimal("1000"));
// Thread 1: shared.debit(500)
// Thread 2: shared.debit(500)
// Both read balance=1000, both subtract 500, final balance=500 instead of 0
Solutions:
- Mark the method
synchronizedso only one thread can execute it at a time. - Use
volatileon the field if you only need visibility (not atomicity). - Use
AtomicReferencefor lock-free thread-safe updates. - In Spring applications, keep request-scoped beans to avoid sharing objects across threads.
Local variables do not have this problem because each thread has its own stack.
What about final, volatile, and transient?
finalmeans the field must be assigned exactly once, by the end of every constructor. After that it cannot change. Use this for immutable fields likeaccountId.volatileguarantees that reads and writes to the field are visible across all threads immediately, without caching in CPU registers. It does not make compound operations (like read-then-write) atomic.transientmeans the field is excluded from Java serialization. If you serialize anAccountobject, thetransientfields will be set to their default values when deserialized. Use this for derived or sensitive data you do not want persisted.
public class Account {
private final String accountId; // immutable after construction
private volatile boolean frozen; // visible across threads immediately
private transient BigDecimal cachedEMI; // not serialized
}
How are instance variables stored in memory?
Every Java object on the heap has an object header (typically 12 bytes on 64-bit JVMs with compressed oops) followed by the instance field data. Fields are laid out with padding to align to their natural boundaries.
For an Account with a String accountId, BigDecimal balance, int txnCount, and boolean active:
- Object header: 12 bytes
accountIdreference: 4 bytes (compressed oop)balancereference: 4 bytestxnCountint: 4 bytesactiveboolean: 1 byte + 3 bytes padding- Total: 28 bytes (may vary by JVM)
You can inspect this using the JOL (Java Object Layout) tool:
# Add JOL dependency and run
java -jar jol-cli.jar internals com.bank.core.Account
How do instance variables work with Java records?
Since Java 16, records provide a compact syntax for immutable data carriers. The record components automatically become private final instance variables with auto-generated getters, constructor, equals, hashCode, and toString.
// Traditional class: 30+ lines
public class AccountSummary {
private final String accountId;
private final BigDecimal balance;
// constructor, getters, equals, hashCode, toString...
}
// Record: 1 line, same result
public record AccountSummary(String accountId, BigDecimal balance) {}
The record components accountId and balance are instance variables. They are private final and cannot be reassigned after construction. Records are ideal for DTOs, API responses, and value objects where you do not need mutable state.
What are common mistakes?
- Forgetting
this.in constructors. Writingbalance = balanceassigns the parameter to itself. The field stays at its default. - Relying on null references. Calling a method on an uninitialized reference field causes
NullPointerException. Always initialize in the constructor. - Making fields public. This lets any code bypass your validation logic. Always use
privatewith controlled access methods. - Field hiding in subclasses. Declaring a field with the same name as a parent field creates two separate fields with confusing behavior. Use method overriding instead.
- Assuming thread safety. Instance variables on a shared object are not thread-safe by default. You need synchronization.
Visual Overview
┌─ Instance Variable Lifecycle ──────────────────────────┐
│ │
│ new Account("SAV2001", "Rajesh", 1000000) │
│ │ │
│ ├─ 1. Allocate heap memory (header + fields) │
│ ├─ 2. Set all fields to default (0, false, null) │
│ ├─ 3. Run super() (Object constructor) │
│ ├─ 4. Run field initializers + init blocks │
│ │ (source order) │
│ ├─ 5. Run constructor body │
│ │ (this.accountId = accountId) │
│ └─ 6. Return reference to the caller │
│ │
│ rajesh.debit(50000) │
│ └─ accesses this.balance (instance scope) │
│ │
│ rajesh = null │
│ └─ object becomes unreachable → eligible for GC │
│ └─ instance variables cease to exist │
│ │
└─────────────────────────────────────────────────────────┘
┌─ Memory Layout (Heap) ─────────────────────┐ │ │ │ Account object (rajesh) │ │ ┌─────────────────────────────────────┐ │ │ │ Object header (12 bytes) │ │ │ │ accountId: ref → "SAV2001" │ │ │ │ holderName: ref → "Rajesh Kumar" │ │ │ │ balance: ref → BigDecimal(1000000) │ │ │ │ txnCount: int = 0 │ │ │ │ active: boolean = true │ │ │ └─────────────────────────────────────┘ │ │ │ │ Account object (priya) ← separate copy │ │ ┌─────────────────────────────────────┐ │ │ │ Object header (12 bytes) │ │ │ │ accountId: ref → "CUR3001" │ │ │ │ holderName: ref → "Priya Sharma" │ │ │ │ balance: ref → BigDecimal(250000) │ │ │ │ txnCount: int = 0 │ │ │ │ active: boolean = true │ │ │ └─────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────┘
Key Takeaway
An instance variable is a field that belongs to an individual object. Declare it inside the class body, outside any method, without the static keyword, and every object you create gets its own independent copy. That is why Rajesh's balance and Priya's balance never interfere with each other.
If you forget to assign a value, Java gives the field a safe default: zero for numbers, false for boolean, null for references. You can initialize in three places: right at the declaration, in an instance initializer block, or in the constructor. They execute in that order, after the parent constructor finishes.
The scope of an instance variable is the entire object. Any constructor, instance method, or initializer block can access it. It lives on the heap and survives across method calls until the object is garbage collected. If a method parameter has the same name as a field, the parameter shadows the field, so you use this.balance to reach the field explicitly.
In professional code, keep instance variables private and expose controlled access through behavior methods like debit() and credit() that validate inputs. A final field must be set once by the end of every constructor and can never change after that, which is how you build immutable value objects. And remember that instance variables on a shared object are not thread-safe by default: if two threads modify the same field, you need synchronization.
One sentence to remember: an instance variable is a per-object field that lives on the heap, gets a default value automatically, and is accessible to every instance member of the class for the entire lifetime of the object.
---