Engineering/Backend

What Happens When Java Creates an Object?

Isaac S. Lee 2026. 7. 29. 21:10

Creating an object in Java takes only one line of code.

BankAccount account =
        new BankAccount("100-001", 100_000);

At first glance, the process seems straightforward: new creates an object, and the constructor assigns the arguments to its fields.

That explanation is good enough for a simple class. It becomes incomplete, however, as soon as inheritance, static fields, field initializers, and initialization blocks enter the picture.

If both a parent class and a child class declare constructors, which one runs first? Are field initializers executed before or after the constructor body? Does a static initialization block run every time an object is created? If one constructor calls another constructor, are the instance fields initialized twice?

It is possible to memorize a sequence such as:

parent static initialization
→ child static initialization
→ parent instance initialization
→ parent constructor
→ child instance initialization
→ child constructor

Memorizing the order may help with a quiz, but it does not explain why Java follows that order. As soon as the code changes slightly, the sequence becomes confusing again.

A better approach is to separate the process into two different kinds of initialization.

The first is class initialization. This prepares the class itself and executes its static field initializers and static initialization blocks.

The second is instance initialization. This prepares each individual object by assigning default field values, running instance field initializers and initialization blocks, and executing constructors across the inheritance hierarchy.

Once these two processes are understood separately, Java’s object creation order becomes much easier to follow.


Before an object can be created, its class must be ready

When Java evaluates the following expression, the JVM must first ensure that BankAccount is ready to be used.

BankAccount account =
        new BankAccount();

A class being loaded does not necessarily mean that it has already been initialized.

Class loading makes the class definition available to the JVM. Class initialization is the later step in which static field initializers and static initialization blocks are actually executed.

A class is initialized when it is actively used for the first time, such as when:

  • an instance of the class is created
  • a static method declared by the class is invoked
  • a non-constant static field declared by the class is accessed
  • certain reflective operations request initialization

Before initializing a class, Java initializes its superclass first.

Consider the following example:

public class BankAccount {

    private static int nextSequence =
            loadInitialSequence();

    static {
        System.out.println(
                "BankAccount class initialized"
        );
    }

    private static int loadInitialSequence() {
        System.out.println(
                "Static field initialized"
        );

        return 1;
    }
}

When BankAccount is initialized for the first time, the output is:

Static field initialized
BankAccount class initialized

The static field initializer runs before the static block because it appears first in the source code.

This initialization belongs to the class, not to an individual BankAccount object. Once the class has been initialized successfully, creating additional instances does not run the static initialization again.


Static state belongs to the class, not to each object

Static and instance fields belong to different levels.

public class BankAccount {

    private static int accountCount;

    private final String accountNumber;
    private long balance;
}

accountCount belongs to the BankAccount class. Every instance observes and modifies the same static field.

accountNumber and balance, by contrast, belong to individual objects.

BankAccount first =
        new BankAccount("100-001", 100_000);

BankAccount second =
        new BankAccount("100-002", 50_000);

The two accounts have different account numbers and balances, but they share the same accountCount.

This distinction is reflected in initialization timing:

  • static fields and static blocks run when the class is initialized
  • instance fields and instance initialization blocks run for every new object
  • constructors run for every new object
  • static initialization normally runs only once per initialized class

For that reason, a static block should not be understood as an object initialization block that happens to run for the first instance.

It is part of preparing the class itself.


Static fields and static blocks run in source order

When a class contains several static field initializers and static blocks, Java executes them in the order in which they appear.

public class ServerConfig {

    private static String host =
            loadHost();

    static {
        System.out.println(
                "First static block"
        );
    }

    private static int port =
            loadPort();

    static {
        System.out.println(
                "Second static block"
        );
    }

    private static String loadHost() {
        System.out.println(
                "Host initialized"
        );

        return "localhost";
    }

    private static int loadPort() {
        System.out.println(
                "Port initialized"
        );

        return 8080;
    }
}

The output is:

Host initialized
First static block
Port initialized
Second static block

Java does not initialize every static field first and then execute all static blocks afterward.

The field initializers and static blocks together form one ordered initialization sequence.

This is also why static initialization code should not depend too heavily on declaration order.

Consider the following design:

private static String baseUrl =
        host + ":" + port;

private static String host =
        "localhost";

private static int port =
        8080;

When baseUrl is initialized, host and port have not yet received their explicit values. They still contain their default values.

The result may therefore be very different from what the developer intended.

Static initialization is easiest to understand when it remains simple. When several static values depend on one another, it may be clearer to initialize them through one explicit method or place them inside a separate configuration object.


A superclass is initialized before its subclass

A subclass depends on the class definition and static state of its superclass.

For that reason, Java initializes the superclass before initializing the subclass.

class Parent {

    static {
        System.out.println(
                "Parent static block"
        );
    }
}

class Child extends Parent {

    static {
        System.out.println(
                "Child static block"
        );
    }
}

When Child is actively used for the first time:

new Child();

the output begins with:

Parent static block
Child static block

If the inheritance hierarchy contains several levels, initialization proceeds from the highest superclass down to the class being used.

highest superclass
→ intermediate superclass
→ direct superclass
→ current subclass

This order is not an arbitrary rule added for constructor puzzles.

A subclass is defined on top of its superclass. The superclass must therefore be ready before the subclass can safely use the state and behavior it inherits.


A compile-time constant may not initialize its declaring class

Not every static field access triggers class initialization.

Consider this class:

public class Limits {

    public static final int MAX_RETRY = 3;

    static {
        System.out.println(
                "Limits initialized"
        );
    }
}

Now suppose another class reads the constant:

System.out.println(
        Limits.MAX_RETRY
);

MAX_RETRY is a compile-time constant. Its value can be copied directly into the bytecode of the class that uses it.

As a result, reading it may not initialize Limits, and the static block may not run.

A value produced through a method call is different:

public static final int MAX_RETRY =
        loadMaxRetry();

This is not a compile-time constant. Accessing it requires the declaring class to be initialized first.

The important distinction is that static final does not automatically mean “compile-time constant.”

The initializer must itself be a constant expression of an appropriate type.


Once the class is ready, Java begins constructing the instance

After class initialization is complete, Java can begin constructing the requested object.

Before explicit instance initializers or constructor bodies run, the object’s fields receive their default values.

Common default values include:

TypeDefault value

byte, short, int, long 0
float, double 0.0
boolean false
char the null character
Reference types null

Suppose a class declares explicit field initializers:

public class Member {

    private String name = "Unknown";
    private int age = 20;
}

Conceptually, the fields first receive their default values:

name: null
age: 0

The explicit initializers then replace them:

name: "Unknown"
age: 20

In ordinary code, this intermediate state is not visible outside the object.

It can become visible, however, when partially constructed objects are exposed too early or when a superclass constructor calls a method overridden by a subclass.

Those cases matter because subclass initialization has not yet finished.


Instance field initializers and initialization blocks also follow source order

Instance field initializers and instance initialization blocks run every time a new object is created.

Like their static counterparts, they run in the order in which they appear in the class body.

public class BankAccount {

    private String accountNumber =
            createTemporaryNumber();

    {
        System.out.println(
                "Instance initializer"
        );
    }

    private long balance =
            loadInitialBalance();

    public BankAccount() {
        System.out.println(
                "Constructor"
        );
    }

    private String createTemporaryNumber() {
        System.out.println(
                "Account number initialized"
        );

        return "TEMP";
    }

    private long loadInitialBalance() {
        System.out.println(
                "Balance initialized"
        );

        return 0;
    }
}

Creating an object produces:

Account number initialized
Instance initializer
Balance initialized
Constructor

Java does not execute all instance fields first and all instance blocks afterward. They form one ordered sequence based on their position in the source code.

The constructor body runs after that sequence for the class has completed.

Instance initialization blocks can be useful when several constructors need identical setup logic.

{
    createdAt = Instant.now();
    status = Status.CREATED;
}

In application code, however, putting important initialization directly in a constructor is often easier to understand.

When several initialization blocks are scattered between fields, readers must reconstruct the order by scanning the entire class body.

A language feature can be valid without being the clearest choice for everyday design.


In an inheritance hierarchy, the parent portion is initialized first

Now consider instance initialization across a parent and child class.

class Parent {

    private int parentValue =
            print("1. Parent field");

    {
        print("2. Parent instance block");
    }

    Parent() {
        print("3. Parent constructor");
    }

    protected static int print(
            String message
    ) {
        System.out.println(message);
        return 0;
    }
}

class Child extends Parent {

    private int childValue =
            print("4. Child field");

    {
        print("5. Child instance block");
    }

    Child() {
        print("6. Child constructor");
    }
}

Creating a Child object:

new Child();

produces:

1. Parent field
2. Parent instance block
3. Parent constructor
4. Child field
5. Child instance block
6. Child constructor

The parent’s instance fields and initialization blocks run before the parent constructor body.

Only after the parent constructor completes does Java initialize the child’s instance fields and initialization blocks. The child constructor body runs last.

The flow can be summarized as:

parent initialization
    parent field initializers
    parent instance initialization blocks
    parent constructor body

child initialization
    child field initializers
    child instance initialization blocks
    child constructor body

Java is not creating one Parent object and then another Child object.

There is only one Child object.

That object contains the state inherited from Parent as well as the state declared by Child. Java initializes the parent-defined portion before moving on to the child-defined portion.


Seeing class and instance initialization together

The following example includes static fields, static blocks, instance fields, instance blocks, and constructors.

class Parent {

    private static int parentStatic =
            print("Parent static field");

    static {
        print("Parent static block");
    }

    private int parentField =
            print("Parent instance field");

    {
        print("Parent instance block");
    }

    Parent() {
        print("Parent constructor");
    }

    protected static int print(
            String message
    ) {
        System.out.println(message);
        return 0;
    }
}

class Child extends Parent {

    private static int childStatic =
            print("Child static field");

    static {
        print("Child static block");
    }

    private int childField =
            print("Child instance field");

    {
        print("Child instance block");
    }

    Child() {
        print("Child constructor");
    }
}

public class Main {

    public static void main(String[] args) {
        System.out.println("== First object ==");
        new Child();

        System.out.println("== Second object ==");
        new Child();
    }
}

The output is:

== First object ==
Parent static field
Parent static block
Child static field
Child static block
Parent instance field
Parent instance block
Parent constructor
Child instance field
Child instance block
Child constructor

== Second object ==
Parent instance field
Parent instance block
Parent constructor
Child instance field
Child instance block
Child constructor

When the first Child object is created, neither Parent nor Child has been initialized yet.

Java therefore performs class initialization first:

Parent static initialization
→ Child static initialization

It then performs instance initialization for the new object:

Parent instance initialization
→ Parent constructor
→ Child instance initialization
→ Child constructor

When the second object is created, both classes are already initialized. The static fields and static blocks do not run again.

Only the per-object initialization steps are repeated.


Calling a parent constructor does not create a separate parent object

A child constructor commonly calls a parent constructor through super().

class Child extends Parent {

    Child() {
        super();
    }
}

This does not create a separate Parent object.

super() initializes the parent-defined portion of the Child object that is already being constructed.

Conceptually, one child object contains state defined at multiple levels of its inheritance hierarchy:

one Child object

┌──────────────────────────┐
│ state declared by Parent │
├──────────────────────────┤
│ state declared by Child  │
└──────────────────────────┘

The parent portion must be established first because the child class may rely on inherited state and behavior.

This is also why a child constructor cannot simply skip superclass construction.

Every constructor chain must eventually reach a superclass constructor.


A parent constructor is called even when super() is not written

The following constructor contains no visible call to super():

class Child extends Parent {

    Child() {
        System.out.println(
                "Child constructor"
        );
    }
}

That does not mean the parent constructor is skipped.

When no explicit constructor invocation is present, Java behaves as though super() had been inserted.

Child() {
    super();

    System.out.println(
            "Child constructor"
    );
}

This works only when the superclass has an accessible no-argument constructor.

Consider the following parent:

class Parent {

    Parent(String name) {
        // ...
    }
}

A child must explicitly select that constructor:

class Child extends Parent {

    Child() {
        super("child");
    }
}

Without the explicit call, the compiler attempts to call Parent(), which does not exist.

The program therefore fails to compile.


A default constructor is not always provided

When a class declares no constructors, the compiler provides a default constructor.

public class Member {
}

Conceptually, the generated constructor resembles:

public class Member {

    public Member() {
        super();
    }
}

The compiler stops providing that default constructor as soon as the developer declares any constructor.

public class Member {

    private final String name;

    public Member(String name) {
        this.name = name;
    }
}

The following code is no longer valid:

new Member();

A no-argument constructor must now be declared explicitly if it is required.

public Member() {
    this("Unknown");
}

The phrase “default constructor” does not mean a constructor that every class always has.

It means the constructor supplied by the compiler when the class declares no constructors of its own.

That generated constructor must also be able to call an accessible no-argument superclass constructor.


this() calls another constructor in the same class

A class may provide several constructors for different creation paths.

public class BankAccount {

    private final String accountNumber;
    private final long balance;

    public BankAccount(
            String accountNumber
    ) {
        this(accountNumber, 0);
    }

    public BankAccount(
            String accountNumber,
            long balance
    ) {
        this.accountNumber =
                accountNumber;

        this.balance = balance;
    }
}

The one-argument constructor delegates to the two-argument constructor.

new BankAccount("100-001");

The constructor chain is conceptually:

BankAccount(String)
→ BankAccount(String, long)
→ superclass constructor
→ instance initialization
→ BankAccount(String, long) body
→ BankAccount(String) body

This technique is known as constructor chaining.

It is useful for keeping validation and field assignment in one primary constructor.

public BankAccount(
        String accountNumber
) {
    this(accountNumber, 0);
}

public BankAccount(
        String accountNumber,
        long balance
) {
    validate(accountNumber, balance);

    this.accountNumber =
            accountNumber;

    this.balance = balance;
}

The field initializers and instance initialization blocks do not run once for every constructor in the chain.

Only one object is being created. The constructors are cooperating to initialize that one object.

The chain must eventually reach a superclass constructor, and constructors cannot form a cycle by repeatedly calling one another.


A constructor cannot directly call both this() and super()

A constructor may explicitly delegate to another constructor in the same class:

this(...);

or directly invoke a superclass constructor:

super(...);

It cannot directly do both.

Child() {
    this("child");
    super(); // compilation error
}

The constructor called through this(...) will eventually reach a constructor that invokes super(...).

Child()
→ Child(String)
→ Parent(...)

A constructor chain follows one path up the inheritance hierarchy. It does not invoke multiple superclass constructors for the same object.


Constructor syntax changed in Java 25

For many years, Java required an explicit this(...) or super(...) invocation to appear as the first statement of a constructor body.

That remains the rule in Java 17.

The following constructor does not compile in Java 17:

class Employee extends Person {

    Employee(
            String name,
            int age
    ) {
        if (age < 18) {
            throw new IllegalArgumentException(
                    "Employee must be an adult."
            );
        }

        super(name, age);
    }
}

The validation statement appears before super(...).

Java 25 introduced flexible constructor bodies as a permanent language feature. A constructor may now execute a limited prologue before explicitly calling this(...) or super(...).

This allows arguments to be checked or prepared before the superclass constructor is invoked.

class Employee extends Person {

    Employee(
            String name,
            int age
    ) {
        if (age < 18) {
            throw new IllegalArgumentException(
                    "Employee must be an adult."
            );
        }

        super(name, age);
    }
}

The object is not fully initialized during this prologue. Java therefore restricts access to this, super, instance fields, and instance methods before the explicit constructor invocation.

For developers working on Java 17, the older rule still applies: an explicit constructor invocation must come first.

This is a useful reminder that initialization rules should always be discussed together with the Java version being used.


A constructor works with an object that is still being completed

Because this can be used inside a constructor, it is easy to imagine that the object already exists in a fully usable state.

It does exist, but it is still being initialized.

public class Member {

    private final String name;

    public Member(String name) {
        this.name = name;
    }
}

Inside the constructor, this refers to the object currently under construction.

The object may not yet have completed all of its initialization steps, especially when inheritance is involved.

Exposing this during construction can therefore be dangerous.

public class EventListener {

    public EventListener(
            EventBus eventBus
    ) {
        eventBus.register(this);
    }
}

The constructor registers the object with another component before construction has completed.

That other component may invoke methods on the object immediately or expose it to another thread.

This situation is commonly called this escape.

In concurrent code, another thread may observe state before the constructor has finished establishing the object’s invariants.

Safer alternatives may include:

  • registering the object after construction
  • using a factory method that creates and then registers the object
  • separating construction from lifecycle activation
  • letting a dependency-injection container manage the lifecycle

A constructor should avoid publishing a reference to the object before the object is ready to be used.


Calling an overridable method from a constructor is dangerous

One of the most important consequences of Java’s initialization order appears when a constructor calls a method that a subclass can override.

class Parent {

    Parent() {
        printState();
    }

    void printState() {
        System.out.println(
                "Parent"
        );
    }
}

class Child extends Parent {

    private String message =
            "Child is ready";

    @Override
    void printState() {
        System.out.println(message);
    }
}

Now create a Child:

new Child();

The output is:

null

The Parent constructor calls printState(), but Java uses dynamic method dispatch for instance methods.

The actual object being constructed is a Child, so Child.printState() is invoked.

At that moment, however, the child’s instance field initializer has not yet run.

private String message =
        "Child is ready";

message still contains its default value, null.

The sequence is:

allocate the Child object
→ assign default field values
   message = null

→ enter the Parent constructor
→ Parent constructor calls printState()
→ dynamic dispatch selects Child.printState()
→ Child.printState() reads message
→ message is still null

→ Parent constructor finishes
→ Child field initializer runs
   message = "Child is ready"
→ Child constructor body runs

The parent constructor cannot know which subclass fields an overridden method may depend on.

For that reason, constructors should generally avoid calling overridable instance methods.

Possible alternatives include:

  • using a private helper method
  • using a final method when overriding must be prevented
  • performing polymorphic behavior after construction
  • moving complex creation logic into a factory or builder

This problem also leads naturally into the broader topic of overriding and dynamic dispatch.


If construction fails, no completed object is returned

A constructor can reject invalid state by throwing an exception.

public BankAccount(
        String accountNumber,
        long balance
) {
    if (balance < 0) {
        throw new IllegalArgumentException(
                "Balance must not be negative."
        );
    }

    this.accountNumber =
            accountNumber;

    this.balance = balance;
}

The following expression fails:

BankAccount account =
        new BankAccount(
                "100-001",
                -1
        );

The new expression does not complete normally, and no successfully constructed reference is assigned to account.

This is one reason constructor validation is valuable.

Instead of creating an invalid object and expecting other code to repair it later, the constructor prevents the invalid object from entering the rest of the program.

Other code can then rely on a useful guarantee:

If a BankAccount object exists, it satisfies the basic rules established by its constructor.

Constructors should still be careful about external side effects.

Suppose a constructor opens a file, registers a callback, or contacts another system before later throwing an exception. Those external actions may already have occurred even though the object was never successfully returned.

For that reason, constructors are usually best kept focused on validating and establishing the object’s own state. Failure-prone external work is often better coordinated by a service or factory.


Failure during static initialization has a wider effect

Static initialization failure can affect every future use of a class.

public class ApplicationConfig {

    public static final String API_KEY =
            loadApiKey();

    private static String loadApiKey() {
        throw new IllegalStateException(
                "API key is missing"
        );
    }
}

The first active use of ApplicationConfig begins class initialization.

Because loadApiKey() throws an exception, initialization fails. The JVM may report an ExceptionInInitializerError.

The class is then considered to be in an erroneous state for that class loader. A later attempt to use it may result in a NoClassDefFoundError rather than another clean initialization attempt.

This makes static initialization a poor place for work that commonly fails or depends on the external environment.

Examples include:

  • reading external files
  • opening network connections
  • connecting to a database
  • validating complex environment configuration
  • building a large object graph
  • performing remote service calls

Some configuration must be validated during application startup. Even then, performing the validation through an explicit application lifecycle or configuration component usually produces clearer errors and better control over recovery.

Static initialization is safest when it is deterministic, local, and simple.


Mutable static state is shared by every caller

Static fields are important not only because of initialization order, but also because they create shared state.

public class SequenceGenerator {

    private static long sequence;

    public static long next() {
        return ++sequence;
    }
}

There is only one sequence field for the class.

Every thread and every caller uses the same value.

In a single-threaded example, the code may appear correct. Under concurrent access, ++sequence is not guaranteed to behave as one indivisible operation.

Several threads can read and update the value at overlapping times, leading to lost updates or duplicate results.

A thread-safe implementation could use AtomicLong:

private static final AtomicLong
        SEQUENCE = new AtomicLong();

public static long next() {
    return SEQUENCE.incrementAndGet();
}

Concurrency control is only part of the design question.

Before creating mutable static state, it is worth asking whether the state should truly be global.

Static mutable state can:

  • leak between tests
  • create hidden dependencies
  • make initialization order important
  • complicate parallel execution
  • make state ownership unclear
  • become difficult to replace or isolate

When the state belongs to a specific application component, an instance field managed through dependency injection may provide a clearer design.


Applying initialization rules to real code

The purpose of understanding initialization order is not merely to predict console output.

It should influence how classes and constructors are designed.

Keep static initialization simple

A fixed regular expression is a reasonable static value:

private static final Pattern
        ACCOUNT_PATTERN =
        Pattern.compile("\\d{3}-\\d{3}");

The input is local, the behavior is predictable, and initialization does not depend on an external system.

Reading configuration files or opening network connections during static initialization is far more fragile.

Establish required state in the constructor

public BankAccount(
        String accountNumber,
        long balance
) {
    validate(accountNumber, balance);

    this.accountNumber =
            accountNumber;

    this.balance = balance;
}

An object that is valid immediately after construction is easier to use than one that requires several setter calls before it becomes meaningful.

Delegate constructors toward one primary path

public BankAccount(
        String accountNumber
) {
    this(accountNumber, 0);
}

public BankAccount(
        String accountNumber,
        long balance
) {
    validate(accountNumber, balance);

    this.accountNumber =
            accountNumber;

    this.balance = balance;
}

Keeping validation and final field assignment in one constructor prevents different construction paths from enforcing different rules.

Do not publish this during construction

Avoid registering the object with callbacks, event buses, threads, or shared registries before construction has completed.

Do not call overridable methods from constructors

Subclass state may still contain default values when the superclass constructor invokes the method.

Do not make constructors coordinate an entire application workflow

When object creation requires database queries, network access, or several external resources, a factory or application service can perform that coordination.

public final class BankAccountFactory {

    private final CustomerRepository
            customerRepository;

    public BankAccount create(
            Long customerId,
            String accountNumber
    ) {
        Customer customer =
                customerRepository
                        .findById(customerId)
                        .orElseThrow();

        return new BankAccount(
                customer,
                accountNumber,
                0
        );
    }
}

The factory retrieves external data. The constructor remains responsible for validating and establishing the BankAccount itself.


Initialization order is the order in which an object’s responsibilities are established

At first, Java’s initialization sequence may look like a rule that simply needs to be memorized:

parent static initialization
→ child static initialization
→ parent instance initialization
→ parent constructor
→ child instance initialization
→ child constructor

The sequence becomes more meaningful once each step is connected to its responsibility.

Static initialization prepares the state shared by the class.

In an inheritance hierarchy, the superclass must be prepared before the subclass.

When an object is created, the parent-defined portion of that one object is initialized before the child-defined portion. Only after the parent constructor finishes can the subclass complete its own fields and constructor body.

this() connects several constructors that cooperate to initialize the same object. super() initializes the superclass portion of that object. Neither expression creates an additional parent or child object.

Understanding this sequence also explains several practical risks:

  • an overridden method may observe subclass fields before they are initialized
  • publishing this may expose a partially constructed object
  • failed static initialization can make a class unusable
  • global mutable static state can introduce concurrency and test-isolation problems
  • constructors with external side effects can leave work partially completed

A constructor is not merely a convenient place to assign fields.

It is the boundary between an object that is still being assembled and an object that the rest of the program is allowed to trust.

A good constructor does not try to perform every related task. It ensures that the object receives the state it needs, rejects invalid input, and does not escape before its invariants have been established.

That is the practical reason to understand Java’s initialization order.

The goal is not only to know which line prints first. It is to recognize when an object is still incomplete—and to avoid writing code that treats it as though it were already ready.

References