Course contents

A guide to creating objects in Java

Every way to get an object in Java (new, static factories, builders, reflection, method handles, clone, deserialization), which ones skip your constructor, what HotSpot does on new step by step, TLAB allocation and escape analysis.

PremiumOften askedLesson16 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: a car factory with several doors
  2. Object creation under the hood
  3. What happens when new runs, step by step
  4. Real world: the constructor that wasn't called
  5. Advanced: escape analysis, Unsafe and the edge cases
  6. How to say it in the interview

🏭 What does it mean to create an object in Java?

Creating an object means asking the JVM for a fresh block of heap memory shaped like a class, then getting a reference back that points at it. Most of the time you do it with new Order(42, "alice"), but it is not the only door: static factories, builders, clone(), reflection and deserialization all hand you objects too, and some of them never run your constructor.

Interviewers like this topic because it has two layers. The first layer is a list ("what are the ways to create an object?") that most people can recite. The second layer is what separates candidates: which of those paths skip the constructor, what the JVM physically does between new and the reference landing in your variable, and why allocating a million short-lived objects is cheap while keeping them alive is not.

What you’ll learn

  • List every creation path and say which run a constructor and which skip it (clone(), Java deserialization)
  • Read the bytecode for new (new, dup, invokespecial <init>) and explain why the dup is there
  • Walk through the six steps of new: class init check, TLAB bump allocation, zeroing, header, constructor chain, publish
  • Create objects reflectively the modern way with getDeclaredConstructor().newInstance() and MethodHandles.Lookup.findConstructor
  • Choose between a constructor, a static factory and a builder, and avoid clone() in new code
  • Explain what escape analysis and scalar replacement remove, and why allocation is cheap but survivors are not

For this lesson, we'll focus on every way to get an instance, the exact sequence HotSpot follows on new, and the escape analysis optimisation that can make an allocation disappear.

Below the line (out of scope)

The 30-second answer

You can create an object with new, with a static factory or builder (which call new internally), with reflection (Constructor.newInstance or a MethodHandle), with clone(), or by deserializing bytes. new, factories, builders and reflection all run a constructor. clone() copies the fields of an existing object without running any constructor, and Java serialization allocates the object and runs only the no-arg constructor of the first non-serializable superclass, so your own constructor's validation is skipped. On new, the JVM makes sure the class is loaded and initialised, bumps a pointer in the thread's TLAB to grab memory, zeroes the fields, writes the object header, runs the constructor chain up to Object, and only then stores the reference. The JIT can remove the allocation entirely if escape analysis proves the object never leaves the method.

Interview radar

Asked very often: What are the different ways to create an object in Java, and which of them do not call the constructor? Senior rounds probe: What happens step by step when new runs, what a TLAB is, and what escape analysis can and cannot eliminate. Production angle: An object loaded from a cache or a message queue that breaks an invariant your constructor was supposed to enforce, because deserialization skipped it.

01Picture it first: a car factory with several doors

Think of a car factory. Most cars roll off the main assembly line: raw parts in, every inspection station passed, finished car out. That's new with a constructor. The showroom manager might take orders through a sales desk (a static factory) or a custom configurator (a builder), but both of those still send the car down the main line.

Then there are the side doors. A photocopier for cars (clone()) stamps out an exact copy of an existing car without visiting any inspection station. A warehouse (deserialization) reassembles a car from a crate of labelled parts, and again skips the line. A remote control (reflection) can start the main line from outside, even for models the showroom keeps private.

The map below sorts every creation path by one question that matters in interviews: does your constructor run?

Fig. 1

A map of six ways to create an Order: new, static factory, builder and reflection all funnel into the constructor and its validation; clone copies fields from an existing object and Java deserialization fills fields from bytes, both skipping the Order constructor

A map of six ways to create an Order: new, static factory, builder and reflection all funnel into the constructor and its validation; clone copies fields from an existing object and Java deserialization fills fields from bytes, both skipping the Order constructor

Notice three things:

  • Four of the six paths end at the same constructor. Factories and builders are API design choices, not new JVM mechanisms.
  • The two side doors skip your constructor. That's where invariants quietly break.
  • Reflection is not a side door for construction. It finds a constructor at run time, but it still runs it.

02Object creation under the hood

Let's use a small value class from a payments service. It validates in its constructor, which makes it easy to see which paths respect that check.

public final class Payment implements Cloneable, java.io.Serializable {
    private final String id;
    private final long amountCents;

    public Payment(String id, long amountCents) {
        if (amountCents <= 0) throw new IllegalArgumentException("amount must be positive");
        this.id = id;
        this.amountCents = amountCents;
        System.out.println("constructor ran for " + id);
    }

    public static Payment of(String id, long amountCents) {   // static factory
        return new Payment(id, amountCents);
    }

    @Override
    public Payment clone() {
        try {
            return (Payment) super.clone();                   // Object.clone(): field copy
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);                      // cannot happen, we are Cloneable
        }
    }
}

The everyday paths: new, factories and builders

Try it yourself first

Before reading on, count how many times constructor ran is printed.

Payment a = new Payment("p-1", 500);                         // prints: constructor ran for p-1
Payment b = Payment.of("p-2", 700);                          // prints: constructor ran for p-2
Payment c = a.clone();                                       // prints nothing
System.out.println(c != a);                                  // prints: true (a separate object)

Two lines print. Notice:

  • Payment.of is a plain method that calls new. The value of a static factory is in the API: it has a name, it can return a cached instance (Integer.valueOf(100), List.of()), it can return a subtype, and it can run setup after the object is fully built.
  • clone() printed nothing. Object.clone() is a native method that allocates a new object of the same runtime class and copies every field bit for bit. No constructor runs, at any level of the hierarchy.
  • clone() needs Cloneable. Without the marker interface, Object.clone() throws CloneNotSupportedException. It is also protected, which is why I override it as public with a covariant return type.

A builder is the same idea as a factory, for classes with many optional parameters. Instead of a constructor with eight arguments where two longs are easy to swap, you set named values and call build(), which calls the real constructor once.

Refund refund = Refund.builder()
        .paymentId("p-1")
        .amountCents(200)
        .reason("damaged")
        .build();          // build() calls new Refund(this) and validates once

The builder itself is an extra short-lived object. In a request handler that's irrelevant; in a loop doing ten million iterations, it's one more thing for escape analysis to remove (more on that below).

Reflection and method handles

Frameworks such as Spring, Jackson and JPA don't know your classes at compile time, so they create instances from a Class object. The modern reflective call is getDeclaredConstructor(...).newInstance(...).

Constructor<Payment> ctor = Payment.class.getDeclaredConstructor(String.class, long.class);
Payment r = ctor.newInstance("p-3", 900);                   // prints: constructor ran for p-3

try {
    ctor.newInstance("p-4", -1);
} catch (InvocationTargetException e) {
    System.out.println(e.getCause());   // prints: java.lang.IllegalArgumentException: amount must be positive
}

MethodHandle mh = MethodHandles.lookup()
        .findConstructor(Payment.class, MethodType.methodType(void.class, String.class, long.class));
Payment m = (Payment) mh.invoke("p-5", 1_000L);              // prints: constructor ran for p-5

Notice:

  • The constructor still runs, including its validation. Exceptions come back wrapped in InvocationTargetException, which trips people up when they log only the outer exception.
  • Class.newInstance() is deprecated since Java 9. It could rethrow checked exceptions the caller never declared. Use getDeclaredConstructor().newInstance().
  • Private constructors need setAccessible(true), and since Java 16 strong encapsulation of JDK internals means that call fails with InaccessibleObjectException for packages a module doesn't open.
  • A MethodHandle is checked once, at lookup. After that the JIT can inline it like a normal call, which is why newer frameworks prefer it in hot paths.

Deserialization

Java serialization rebuilds an object graph from a byte stream. For a Serializable class, ObjectInputStream allocates the instance, runs the no-arg constructor of the first non-serializable superclass (for Payment, that's Object()), then writes the field values from the stream straight into the object. Payment(String, long) never runs, so a crafted or old stream can produce a Payment with amountCents = -500.

There are two well-known exceptions. Externalizable classes must have a public no-arg constructor, which is called before readExternal. Records (Java 16+) are deserialized through their canonical constructor, so their validation does run. That alone is a good reason to model wire data as records. JSON libraries are a different mechanism: Jackson calls a constructor or factory you annotate (or a no-arg constructor followed by setters), so what runs depends on how you configured it.

03What happens when new runs, step by step

Try it yourself first

Before reading on, list the steps between new Payment("p-1", 500) and the reference being stored in a. Try for five.

At the bytecode level the expression is new, dup, push the arguments, invokespecial Payment.<init>, then astore. The dup exists because invokespecial consumes one copy of the reference as the constructor's this, and the caller needs a second copy to store. Inside HotSpot, that turns into six steps, shown below.

Premium lesson

Keep reading with the Java Pass

The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.

  • Real world: the constructor that wasn't called
  • Advanced: escape analysis, Unsafe and the edge cases
  • How to say it in the interview

From ₹199 · one-time payment · No auto-renewal

Practise this topicClasses & objects: interview questions & scenarios
Further reading (9)