Loading content...
Every bug has a birthplace. For a surprising number of defects, that birthplace is object initialization. An object born in an invalid state carries that invalidity through its entire lifetime, eventually manifesting as a mysterious failure far from the original sin.
The goal of this page is simple but profound: Learn to create objects that cannot be wrong. Objects that, if they exist at all, are guaranteed to be in a valid, usable state. This is not aspirational—it's achievable through disciplined application of specific practices.
By the end of this page, you will master validation strategies, understand field initialization order and its implications, learn defensive initialization techniques, and internalize the principle that constructors are your first and best line of defense against invalid objects.
The fundamental principle:
When a constructor completes successfully, the resulting object must be in a valid, fully usable state. No further initialization should be required, and the object should be safe to use immediately.
This principle has far-reaching implications for how we design constructors:
All invariants established — Every rule that defines a valid object must be satisfied before the constructor returns.
No partial initialization — Every field that matters for object validity must be set.
Fail-fast on invalid input — If valid initialization is impossible, throw an exception rather than creating an invalid object.
Complete, not configurable — The object should not require setter calls to become usable.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
public final class DateRange { private final LocalDate startDate; private final LocalDate endDate; /** * Creates a valid date range. * * Invariants established by constructor: * 1. startDate is not null * 2. endDate is not null * 3. startDate <= endDate * * These invariants ALWAYS hold for any DateRange instance. */ public DateRange(LocalDate startDate, LocalDate endDate) { // Validate inputs - fail fast if invalid Objects.requireNonNull(startDate, "Start date cannot be null"); Objects.requireNonNull(endDate, "End date cannot be null"); if (startDate.isAfter(endDate)) { throw new IllegalArgumentException( "Start date must be before or equal to end date. " + "Got start=" + startDate + ", end=" + endDate ); } // All validations passed - safe to initialize this.startDate = startDate; this.endDate = endDate; // Constructor complete - object is GUARANTEED valid } // No setters - object is immutable and always valid public boolean contains(LocalDate date) { // Safe assumption: startDate and endDate are non-null // and startDate <= endDate return !date.isBefore(startDate) && !date.isAfter(endDate); } public long getDays() { // Safe assumption: invariants hold return ChronoUnit.DAYS.between(startDate, endDate) + 1; }} // UsageDateRange valid = new DateRange(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 12, 31));// 'valid' is guaranteed to represent a valid date range // This throws immediately - no invalid object can exist// DateRange invalid = new DateRange(LocalDate.of(2024, 12, 31), LocalDate.of(2024, 1, 1));// IllegalArgumentException: Start date must be before...When the constructor enforces all invariants, every method in the class can safely assume those invariants hold. No defensive checks needed in every method—the constructor did that work once, at the right time.
Validation is the heart of safe object initialization. A systematic approach to validation ensures no invalid data slips through.
Types of validation:
| Validation Type | What It Checks | Example |
|---|---|---|
| Null checks | Required values are present | email != null |
| Format validation | Data matches expected pattern | email contains '@' |
| Range validation | Values within acceptable bounds | 0 <= age <= 150 |
| Consistency validation | Multiple fields are coherent | startDate <= endDate |
| Business rule validation | Domain rules are satisfied | account balance >= minimumBalance |
| Referential validation | Referenced objects are valid | order.customer != null && customer.isActive() |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
public class CreditCard { private final String cardNumber; private final String cardholderName; private final YearMonth expirationDate; private final String cvv; public CreditCard(String cardNumber, String cardholderName, YearMonth expirationDate, String cvv) { // 1. NULL CHECKS - required values present Objects.requireNonNull(cardNumber, "Card number is required"); Objects.requireNonNull(cardholderName, "Cardholder name is required"); Objects.requireNonNull(expirationDate, "Expiration date is required"); Objects.requireNonNull(cvv, "CVV is required"); // 2. FORMAT VALIDATION - correct structure String normalizedNumber = cardNumber.replaceAll("\\s+", ""); if (!normalizedNumber.matches("^\\d{13,19}$")) { throw new IllegalArgumentException( "Card number must be 13-19 digits" ); } if (!cvv.matches("^\\d{3,4}$")) { throw new IllegalArgumentException( "CVV must be 3 or 4 digits" ); } // 3. ALGORITHM VALIDATION - Luhn checksum if (!passesLuhnCheck(normalizedNumber)) { throw new IllegalArgumentException( "Invalid card number (checksum failed)" ); } // 4. RANGE VALIDATION - reasonable bounds if (cardholderName.length() < 2 || cardholderName.length() > 100) { throw new IllegalArgumentException( "Cardholder name must be 2-100 characters" ); } // 5. BUSINESS RULE - card must not be expired if (expirationDate.isBefore(YearMonth.now())) { throw new IllegalArgumentException( "Card is expired: " + expirationDate ); } // All validations passed - initialize this.cardNumber = normalizedNumber; this.cardholderName = cardholderName.trim(); this.expirationDate = expirationDate; this.cvv = cvv; } private boolean passesLuhnCheck(String number) { int sum = 0; boolean alternate = false; for (int i = number.length() - 1; i >= 0; i--) { int digit = Character.getNumericValue(number.charAt(i)); if (alternate) { digit *= 2; if (digit > 9) digit -= 9; } sum += digit; alternate = !alternate; } return sum % 10 == 0; } // Masked getter for security public String getMaskedNumber() { return "**** **** **** " + cardNumber.substring(cardNumber.length() - 4); }}Validate in order of severity: null checks first (avoid NullPointerException), then format/range (avoid processing garbage), then business rules (avoid complex downstream errors). Early validation prevents wasting cycles on data that will fail anyway.
Fields are initialized in a specific order that every developer must understand. Getting this wrong leads to subtle, hard-to-debug issues.
The initialization sequence in Java:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
public class InitOrderDemo { static class Parent { static String PARENT_STATIC = "Parent static initialized"; String parentField = "Parent field initialized"; Parent() { System.out.println("1. Parent constructor running"); System.out.println(" parentField = " + parentField); // Child fields are NOT yet initialized! printState(); // Calls overridden method - DANGER! } void printState() { System.out.println(" Parent.printState()"); } } static class Child extends Parent { static String CHILD_STATIC = "Child static initialized"; String childField = "Child field initialized"; int counter = 10; Child() { super(); // Parent constructor runs first System.out.println("2. Child constructor running"); System.out.println(" childField = " + childField); System.out.println(" counter = " + counter); } @Override void printState() { // DANGER: Called from parent constructor // but childField is still null, counter is still 0! System.out.println(" Child.printState()"); System.out.println(" childField = " + childField); // null! System.out.println(" counter = " + counter); // 0! } }} // Output when creating new Child():// 1. Parent constructor running// parentField = Parent field initialized// Child.printState()// childField = null <-- NOT yet initialized!// counter = 0 <-- NOT yet initialized!// 2. Child constructor running// childField = Child field initialized// counter = 10When a parent constructor calls an overridable method, the child's override runs before the child's fields are initialized. This is why constructors should never call overridable methods—it leads to NullPointerExceptions and invalid state that appears impossible.
Defensive initialization protects your object from external mutation. When constructors accept mutable objects, those objects can be changed after construction, breaking invariants.
The problem:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// ❌ VULNERABLE: Stores external references directlypublic class Period { private final Date start; private final Date end; public Period(Date start, Date end) { if (start.after(end)) { throw new IllegalArgumentException("Start must be before end"); } this.start = start; // Stores the caller's Date reference! this.end = end; // Stores the caller's Date reference! }} // ATTACK: Modify the dates after "valid" constructionDate start = new Date();Date end = new Date(start.getTime() + 1000);Period period = new Period(start, end); // Seems valid... // But wait - we still have references to the internals!end.setTime(start.getTime() - 1000); // end is now BEFORE start!// The Period's invariant is BROKEN - start is after end // ✅ SAFE: Defensive copiespublic class Period { private final Date start; private final Date end; public Period(Date start, Date end) { // Make defensive copies FIRST this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); // Then validate the copies if (this.start.after(this.end)) { throw new IllegalArgumentException("Start must be before end"); } } // Also protect on output public Date getStart() { return new Date(start.getTime()); // Return copy, not original } public Date getEnd() { return new Date(end.getTime()); // Return copy, not original }}Key defensive techniques:
Copy on input — Make copies of mutable parameters before storing them.
Copy on output — Return copies of mutable fields, not the fields themselves.
Validate after copying — Validate the copies, not the originals (prevents TOCTOU attacks).
Use immutable types — When possible, use immutable types that don't need copying (e.g., LocalDate instead of Date).
Use unmodifiable wrappers — For collections, return Collections.unmodifiableList() or make a copy.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
public class Team { private final String name; private final List<Player> players; // Mutable collection of mutable objects public Team(String name, List<Player> players) { this.name = Objects.requireNonNull(name); // Defensive copy of the list this.players = new ArrayList<>(); for (Player player : players) { // Also copy each player if Player is mutable this.players.add(new Player(player)); } } // Return unmodifiable view (no copying, but prevents modification) public List<Player> getPlayers() { return Collections.unmodifiableList(players); } // Or return a copy for full protection public List<Player> getPlayersCopy() { List<Player> copy = new ArrayList<>(); for (Player player : players) { copy.add(new Player(player)); } return copy; }} // Modern Java: Use immutable collections where possiblepublic class ImmutableTeam { private final String name; private final List<Player> players; // Immutable list public ImmutableTeam(String name, List<Player> players) { this.name = Objects.requireNonNull(name); // List.copyOf creates an unmodifiable copy this.players = List.copyOf(players); } public List<Player> getPlayers() { return players; // Safe to return - it's immutable }}Always copy BEFORE validating, then validate the COPY. If you validate the original first, an attacker can modify it between validation and copying (Time-of-Check to Time-of-Use vulnerability). The copy is your data; the original is the caller's data.
Beyond constructors, languages offer initialization blocks—anonymous code blocks that run during object creation. Understanding when to use them is part of mastering initialization.
Types of initialization blocks:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
public class LoggingService { // --- STATIC INITIALIZATION --- private static final Logger logger; private static final Map<String, Level> levelCache; // Static initializer block - runs once when class loads static { // Complex initialization that can't be done in declaration logger = LoggerFactory.getLogger(LoggingService.class); levelCache = new HashMap<>(); levelCache.put("DEBUG", Level.DEBUG); levelCache.put("INFO", Level.INFO); levelCache.put("WARN", Level.WARN); levelCache.put("ERROR", Level.ERROR); logger.info("LoggingService class initialized"); } // --- INSTANCE INITIALIZATION --- private final String instanceId; private final LocalDateTime createdAt; private int messageCount; // Instance initializer block - runs for each new object { // Shared setup for all constructors this.instanceId = UUID.randomUUID().toString(); this.createdAt = LocalDateTime.now(); this.messageCount = 0; logger.debug("New LoggingService instance created: {}", instanceId); } // Multiple constructors all benefit from the initializer public LoggingService() { // instanceId, createdAt, messageCount already set } public LoggingService(int initialCount) { // instanceId, createdAt, messageCount already set this.messageCount = initialCount; }}Use instance initializer blocks when multiple constructors need the same setup logic. Prefer constructors for most initialization. Use static initializer blocks for complex static field initialization that can't be done in a declaration. In modern Java, consider static factory methods instead of complex static blocks.
Static factory methods are an alternative to constructors that offer significant advantages in many scenarios. Understanding when to prefer them is an important design skill.
createEmpty() or fromJson().1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
public final class Duration { private final long nanos; // Private constructor - only factory methods can create instances private Duration(long nanos) { this.nanos = nanos; } // Descriptive factory methods - clear intent public static Duration ofNanos(long nanos) { return new Duration(nanos); } public static Duration ofMicros(long micros) { return new Duration(micros * 1000L); } public static Duration ofMillis(long millis) { // Caching common values if (millis == 0) { return ZERO; } return new Duration(millis * 1_000_000L); } public static Duration ofSeconds(long seconds) { return new Duration(seconds * 1_000_000_000L); } public static Duration ofMinutes(long minutes) { return new Duration(minutes * 60L * 1_000_000_000L); } // Common instance cached and reused private static final Duration ZERO = new Duration(0); public static Duration zero() { return ZERO; // Returns same instance every time } // Parsing factory public static Duration parse(String text) { // "5s" -> 5 seconds, "100ms" -> 100 milliseconds if (text.endsWith("ns")) { return ofNanos(Long.parseLong(text.replace("ns", ""))); } else if (text.endsWith("ms")) { return ofMillis(Long.parseLong(text.replace("ms", ""))); } else if (text.endsWith("s")) { return ofSeconds(Long.parseLong(text.replace("s", ""))); } throw new IllegalArgumentException("Cannot parse: " + text); }} // Usage is expressive and clearDuration timeout = Duration.ofSeconds(30);Duration pollInterval = Duration.ofMillis(100);Duration noWait = Duration.zero();Duration configured = Duration.parse("5s"); // Compare to constructor approach (less clear):// Duration timeout = new Duration(30_000_000_000L); // Is this seconds?When to use static factory methods:
| Situation | Constructor or Factory? |
|---|---|
| Simple object creation | Constructor is fine |
| Multiple ways to create with same parameter types | Factory methods (named) |
| May return cached instances | Factory methods |
| May return subtype or interface | Factory methods |
| Complex validation logic | Factory methods |
| Need to return Optional/null | Factory methods |
| Framework requires constructor | Constructor (perhaps also factories) |
Use this checklist when designing constructors for production code:
Objects.requireNonNull() or equivalent.Before finishing any constructor: Can I create an invalid object using this constructor? If yes, fix it. Can external code break my object's invariants after construction? If yes, add defensive copies. Will every method work correctly assuming only this constructor was called?
We've covered the essential practices for professional-grade object initialization. Let's consolidate the key lessons:
What's next:
Objects are not just born and used—they also die. The next page explores object destruction and cleanup, covering resource management, the finally mechanism, destructors, finalizers, try-with-resources, and patterns for gracefully ending object lifecycles.
You now have a comprehensive toolkit for object initialization. Apply these practices consistently, and you'll build systems where invalid objects simply cannot exist—eliminating entire categories of bugs at their source.