Loading learning content...
Every object tells a story. Not through its class definition or its method signatures, but through its state—the specific values of its attributes at any given moment in time. Understanding object state is fundamental to mastering object-oriented design because state is what makes objects come alive.
Consider a simple bank account. The class defines that accounts have balances, but it's the state—your actual balance of $1,247.83 versus someone else's $50,000—that gives each account object its unique identity and meaning. The same class produces infinitely different objects because each carries its own state.
By the end of this page, you will understand what object state truly means, how to identify and model state in your designs, the difference between essential and derived state, and how state forms the foundation for reasoning about object behavior and correctness.
Object state is the complete collection of values stored in an object's attributes at a specific point in time. It represents everything the object "remembers" about itself—its data, its configuration, its current condition.
Formally, if an object has attributes a₁, a₂, ..., aₙ, then its state is the tuple (v₁, v₂, ..., vₙ) where each vᵢ is the current value of attribute aᵢ. This seemingly simple definition has profound implications for how we design and reason about objects.
1234567891011121314151617
public class BankAccount { // Attributes that constitute state private String accountNumber; // Immutable identifier private String ownerName; // Can change (marriage, legal change) private BigDecimal balance; // Changes frequently private AccountStatus status; // ACTIVE, FROZEN, CLOSED private LocalDateTime createdAt; // Immutable timestamp private LocalDateTime lastAccessed; // Changes on every access // State example: At 3:00 PM on January 9th, 2026, this object's state is: // ("ACC-12345", "Alice Johnson", $1247.83, ACTIVE, // "2024-03-15T10:00:00", "2026-01-09T14:55:30") // At 3:01 PM, after a deposit, the state becomes: // ("ACC-12345", "Alice Johnson", $1447.83, ACTIVE, // "2024-03-15T10:00:00", "2026-01-09T15:01:00")}Don't confuse state with identity. Identity is which object we're talking about (usually memory address or unique ID). State is what values that object currently holds. Two BankAccount objects with identical state are still different objects with different identities.
Not all state is created equal. Understanding the different kinds of state helps you design objects that are easier to understand, test, and maintain. Let's dissect the components of object state:
Rectangle, this is width and height. For a User, it's the unique userId. Essential state cannot be computed from other attributes.Rectangle's area can be derived from width × height. A ShoppingCart's totalPrice is derived from its items. Derived state presents a design choice: store or compute?Logger's logLevel or a Connection's timeout are configuration. These often have sensible defaults.Order's status (PENDING → CONFIRMED → SHIPPED → DELIVERED) or a Thread's state (NEW → RUNNABLE → BLOCKED → TERMINATED).12345678910111213141516171819202122
public class Order { // Essential State - defines this specific order private final String orderId; // Immutable identifier private final String customerId; // Who placed the order private final List<OrderItem> items; // What was ordered // Derived State - could be stored or computed private BigDecimal totalAmount; // Sum of item prices private int itemCount; // items.size() // Configuration State - controls behavior private Currency currency; // USD, EUR, etc. private boolean expeditedShipping; // Affects processing // Transient State - not persisted private transient BigDecimal taxCache; // Cached tax calculation private transient Connection dbConn; // DB connection handle // Lifecycle State - tracks order progression private OrderStatus status; // PENDING, CONFIRMED, etc. private LocalDateTime statusChangedAt; // When status last changed}Should you store derived state or compute it? Storing saves computation but risks inconsistency if you forget to update it when essential state changes. Computing guarantees consistency but may be expensive. The right choice depends on access patterns: compute-heavy, rarely-read data should be computed; frequently-accessed, expensive-to-compute data should be cached.
The state space of an object is the set of all possible combinations of attribute values. For a class with n attributes, each with a range of possible values, the state space can be enormous. However, not all mathematically possible states are valid states.
Valid states are those that satisfy the object's invariants—conditions that must always be true for the object to be in a consistent, meaningful state. Understanding state space and invariants is crucial for designing robust objects.
| Attribute | Type | Possible Values | Valid Values |
|---|---|---|---|
| width | double | -∞ to +∞ | 0 |
| height | double | -∞ to +∞ | 0 |
| State Space | — | Infinite (all ℝ × ℝ) | Quadrant I only (ℝ⁺ × ℝ⁺) |
Invariants narrow the state space to valid states:
Invariants are constraints that must hold true at all times after object construction and before/after every method call (though they may temporarily be violated during method execution). They define what it means for an object to be "valid."
Well-designed classes make it impossible to create objects with invalid state. They enforce invariants through:
1234567891011121314151617181920212223242526272829303132333435363738
public class DateRange { private final LocalDate startDate; private final LocalDate endDate; // INVARIANT: startDate <= endDate (always!) public DateRange(LocalDate start, LocalDate end) { // Enforce invariant at construction if (start.isAfter(end)) { throw new IllegalArgumentException( "Start date must be on or before end date: " + start + " > " + end ); } this.startDate = start; this.endDate = end; // Invariant established; object in valid state } public DateRange extendBy(int days) { // Returns new object; invariant preserved because // new endDate = old endDate + days, which is still >= startDate return new DateRange(this.startDate, this.endDate.plusDays(days)); } public DateRange shiftStart(LocalDate newStart) { // Must validate to preserve invariant if (newStart.isAfter(this.endDate)) { throw new IllegalArgumentException( "New start date would violate invariant" ); } return new DateRange(newStart, this.endDate); } // State space: all (start, end) pairs where start <= end // This is a proper subset of LocalDate × LocalDate}Invariants form a contract between the class and its clients. The class promises that objects will always be in valid states; in return, clients don't need to check for impossible conditions. A robust Account class guarantees balance >= 0 (if overdrafts aren't allowed), so clients never check for negative balances—they trust the invariant.
How you represent state affects your design's clarity, performance, and correctness. There are multiple strategies for representing the same conceptual state, each with trade-offs:
int hour, int minute, int secondint secondsSinceMidnight1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// APPROACH 1: Direct Representationpublic class TimeOfDayDirect { private int hour; // 0-23 private int minute; // 0-59 private int second; // 0-59 // State space: 24 × 60 × 60 = 86,400 valid states // Memory: 3 integers = 12 bytes (typically) public String format() { return String.format("%02d:%02d:%02d", hour, minute, second); } public void addSeconds(int seconds) { // Complex: need to handle overflow across minute/hour boundaries int totalSeconds = toSeconds() + seconds; this.hour = (totalSeconds / 3600) % 24; this.minute = (totalSeconds % 3600) / 60; this.second = totalSeconds % 60; }} // APPROACH 2: Encoded Representation public class TimeOfDayEncoded { private int secondsSinceMidnight; // 0-86399 // State space: 86,400 valid states (same as above) // Memory: 1 integer = 4 bytes (saves 8 bytes per instance) public String format() { int h = secondsSinceMidnight / 3600; int m = (secondsSinceMidnight % 3600) / 60; int s = secondsSinceMidnight % 60; return String.format("%02d:%02d:%02d", h, m, s); } public void addSeconds(int seconds) { // Simple: just add and mod secondsSinceMidnight = (secondsSinceMidnight + seconds) % 86400; if (secondsSinceMidnight < 0) secondsSinceMidnight += 86400; }} // APPROACH 3: Using Enums for Discrete Statepublic class TrafficLight { // Instead of: String color; (allows "purple", "invalid", etc.) // Use enum to constrain state space exactly private TrafficLightState state; // RED, YELLOW, GREEN only enum TrafficLightState { RED, YELLOW, GREEN } // State space: exactly 3 valid states // Impossible to have invalid state!}The best way to prevent invalid state is to make it unrepresentable. Use enums instead of strings for discrete values. Use unsigned integers for quantities that can't be negative (in languages that support them). Use value objects that enforce constraints. The type system becomes your first line of defense.
Objects expose their state through observer methods (also called getters or accessors). But there's more nuance here than simply returning attribute values. Understanding how to properly observe and compare state is essential for correct object-oriented design.
State Observation Principles:
Encapsulated Access: State should be observed through methods, not direct field access. This allows computed properties, validation, and future implementation changes.
Defensive Copying: When returning mutable objects, return copies to prevent external modification of internal state.
State Projection: Sometimes you want to expose a view of state rather than raw values—formatted, filtered, or transformed.
State Equality: Two objects have equal state if all their corresponding attribute values are equal. This is distinct from reference equality (same object in memory).
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
public class Person { private String name; private LocalDate birthDate; private List<String> nicknames; // SIMPLE OBSERVATION: immutable value, safe to return directly public String getName() { return name; } // SIMPLE OBSERVATION: immutable value, safe to return directly public LocalDate getBirthDate() { return birthDate; } // COMPUTED OBSERVATION: derived from essential state public int getAge() { return Period.between(birthDate, LocalDate.now()).getYears(); } // DEFENSIVE COPY: mutable collection, must copy! public List<String> getNicknames() { // Return unmodifiable view or copy return List.copyOf(nicknames); // OR: return Collections.unmodifiableList(nicknames); } // STATE PROJECTION: formatted view of state public String getDisplayName() { if (nicknames.isEmpty()) { return name; } return name + " (" + nicknames.get(0) + ")"; } // STATE EQUALITY: compare by values, not identity @Override public boolean equals(Object o) { if (this == o) return true; // Same identity → equal state if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return Objects.equals(name, person.name) && Objects.equals(birthDate, person.birthDate) && Objects.equals(nicknames, person.nicknames); } @Override public int hashCode() { return Objects.hash(name, birthDate, nicknames); }}Returning mutable internal state (like a List or Date) allows callers to modify your object's internals without going through your methods. This breaks encapsulation and can violate invariants. Always return defensive copies or immutable views of mutable state.
Understanding state is crucial for debugging and testing. Bugs often manifest as objects being in unexpected states, and tests verify that operations produce the expected state transitions. Here's how state-centric thinking improves your development workflow:
toString() reveals object state at a glance. Include all essential state, format for readability. Invaluable in logs and debuggers.123456789101112131415161718192021222324252627282930313233343536373839404142
public class ShoppingCartTest { @Test void shouldMaintainCorrectStateAfterAddingItems() { // ARRANGE: Create object in known initial state ShoppingCart cart = new ShoppingCart(); // Assert initial state assertEquals(0, cart.getItemCount()); assertEquals(BigDecimal.ZERO, cart.getTotalPrice()); assertTrue(cart.isEmpty()); // ACT: Perform operation cart.addItem(new Product("Widget", new BigDecimal("29.99")), 2); // ASSERT: Verify resulting state assertEquals(2, cart.getItemCount()); assertEquals(new BigDecimal("59.98"), cart.getTotalPrice()); assertFalse(cart.isEmpty()); // Verify invariants still hold assertTrue(cart.getItemCount() >= 0); // Invariant: non-negative count assertTrue(cart.getTotalPrice().compareTo(BigDecimal.ZERO) >= 0); // Invariant: non-negative total } @Test void shouldPreserveStateInvariantOnRemoval() { // ARRANGE: Set up state with items ShoppingCart cart = new ShoppingCart(); cart.addItem(new Product("Widget", new BigDecimal("10.00")), 3); // ACT: Remove more than exists cart.removeItem("Widget", 5); // Only 3 exist // ASSERT: State should be consistent, not negative assertEquals(0, cart.getItemCount()); // Removed all, not -2 assertEquals(BigDecimal.ZERO, cart.getTotalPrice()); assertTrue(cart.isEmpty()); // Invariants preserved even with edge case input }}The AAA Pattern and State:
The Arrange-Act-Assert pattern maps directly to state thinking:
Good tests are state transition tests. They verify that starting from state S₁ and applying operation O, the object arrives at state S₂.
One of the subtle but important distinctions in object-oriented programming is between identity and state. These concepts are related but fundamentally different, and confusing them leads to bugs.
| Aspect | Identity | State |
|---|---|---|
| Definition | Which object this is | What values this object currently holds |
| Changes over time | Never (fixed at creation) | Can change frequently |
| Uniqueness | Always unique per object | Can be identical across objects |
| Comparison | == operator (reference) | equals() method (value) |
| Example | Memory address, UUID | Name, balance, status |
Entity vs Value Objects:
This distinction leads to two fundamental kinds of objects in domain modeling:
Entity Objects have a persistent identity that matters. Two Customer objects with the same name and address are still different customers if they have different IDs. Identity is primary; state can change.
Value Objects are defined entirely by their state. Two Money objects representing $50 are interchangeable—there's no meaningful "identity" beyond the value. State is primary; identity doesn't matter.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// ENTITY: Identity matters, state can changepublic class Customer { private final CustomerId id; // Identity - never changes private String name; // State - can change private String email; // State - can change private Address address; // State - can change @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; Customer customer = (Customer) o; // Compare by IDENTITY only, not state! return id.equals(customer.id); } @Override public int hashCode() { return id.hashCode(); }} // VALUE OBJECT: State IS identity, immutablepublic final class Money { private final BigDecimal amount; private final Currency currency; public Money(BigDecimal amount, Currency currency) { this.amount = amount; this.currency = currency; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Money)) return false; Money money = (Money) o; // Compare by STATE - two $50 USD are equal return amount.equals(money.amount) && currency.equals(money.currency); } @Override public int hashCode() { return Objects.hash(amount, currency); } // Value objects are typically immutable public Money add(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException("Cannot add different currencies"); } return new Money(this.amount.add(other.amount), this.currency); }}Ask: "If two objects have identical attribute values, are they the same thing?" If yes (like two $50 bills), it's a value object. If no (like two people named "John Smith"), it's an entity. Value objects should be immutable; entities may be mutable with a stable identity.
Object state is foundational to object-oriented design. Let's consolidate the key insights:
What's Next:
Now that we understand what object state is, the next page explores how state changes. We'll examine state transitions, object lifecycles, and how to model the progression of objects through different states over their lifetime.
You now understand object state as the foundation of object-oriented design. You can identify different categories of state, constrain state space with invariants, and distinguish between identity and state. Next, we'll explore how objects transition between states over time.