☕ Java

Object Creation

Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.

Constructors — Deep Dive

A constructor is a special method with the same name as the class and no return type — not even void. Its purpose is to initialise a newly created object so it is in a valid, consistent state from the moment it exists. If a class declares no constructor, Java provides a default no-argument constructor automatically. Once any constructor is explicitly declared, the default is no longer provided — if you still want a no-arg constructor, you must declare it yourself. The execution order during object creation is precise and worth understanding exactly: first, if the constructor begins with a super() or this() call, that runs; otherwise an implicit super() is inserted and runs. Then instance field initialisers and instance initialiser blocks run in the order they appear in the source file. Finally the rest of the constructor body runs. This order ensures the superclass is fully initialised before the subclass and that fields have their declared initial values before the constructor body executes. Constructors should do the minimum work required to establish a valid object. They should not call overridable methods (a subclass override could run before the subclass is initialised), should not start threads or register callbacks that could expose a partially constructed this reference, and should validate their parameters early — failing fast is better than allowing an invalid object to exist.
Java
// ── Constructor fundamentals ─────────────────────────────────────────
public class Temperature {

    private final double value;
    private final String unit;    // "CELSIUS", "FAHRENHEIT", "KELVIN"

    // ── Explicit constructor — validates immediately ───────────────────
    public Temperature(double value, String unit) {
        // Validate before any assignment
        Objects.requireNonNull(unit, "Unit cannot be null");

        String u = unit.toUpperCase();
        if (!Set.of("CELSIUS","FAHRENHEIT","KELVIN").contains(u)) {
            throw new IllegalArgumentException(
                "Unknown unit: " + unit);
        }
        if (u.equals("KELVIN") && value < 0) {
            throw new IllegalArgumentException(
                "Kelvin cannot be negative");
        }

        this.value = value;
        this.unit  = u;
    }

    // ── Constructor execution order ────────────────────────────────────
    // 1. super() inserted implicitly (calls Object())
    // 2. Field initialisers run: value and unit set to 0.0 and null
    // 3. Constructor body runs: validate, then assign value and unit

    public double getValue() { return value; }
    public String getUnit()  { return unit;  }

    public Temperature toCelsius() {
        return switch (unit) {
            case "CELSIUS"    -> this;
            case "FAHRENHEIT" -> new Temperature(
                (value - 32) * 5.0 / 9.0, "CELSIUS");
            case "KELVIN"     -> new Temperature(
                value - 273.15, "CELSIUS");
            default -> throw new IllegalStateException();
        };
    }

    @Override
    public String toString() {
        return value + "°" + unit.charAt(0);
    }
}

// ── No-arg constructor — explicitly provided ───────────────────────────
public class Config {

    private String host    = "localhost";
    private int    port    = 8080;
    private int    timeout = 30;

    // Explicit no-arg constructor — allows creation with all defaults
    public Config() {}

    // Once this is declared, Config() must be explicit too
    public Config(String host, int port) {
        this.host = host;
        this.port = port;
    }
}

Constructor Overloading and Chaining

A class can declare multiple constructors with different parameter lists — this is constructor overloading. Overloaded constructors give callers flexibility: they can provide a complete specification, just the required minimum, or any useful combination. The key design rule is that all constructors should ultimately initialise all fields to valid values, and the easiest way to ensure this without duplication is constructor chaining. Constructor chaining with this() routes every constructor through one canonical constructor that contains all the real initialisation and validation logic. The less specific constructors simply provide default values and delegate. This means there is exactly one place where the initialisation logic lives, and every construction path passes through it — changes to validation or initialisation need to be made in only one place. The constructor chain must converge — every this() call must ultimately reach a constructor that does not call this(), the "root" constructor. Cycles (A calls B calls A) are a compile error.
Java
// ── Constructor chain — all routes lead to the full constructor ────────
public class HttpRequest {

    private final String   method;
    private final String   url;
    private final Map<String, String> headers;
    private final String   body;
    private final int      timeoutSeconds;

    // ── Root constructor — all fields, full validation ─────────────────
    public HttpRequest(String method, String url,
                       Map<String, String> headers,
                       String body, int timeoutSeconds) {
        Objects.requireNonNull(method, "method");
        Objects.requireNonNull(url,    "url");
        if (!url.startsWith("http://") && !url.startsWith("https://")) {
            throw new IllegalArgumentException("URL must start with http/https");
        }
        if (timeoutSeconds <= 0) {
            throw new IllegalArgumentException("Timeout must be positive");
        }

        this.method         = method.toUpperCase();
        this.url            = url;
        this.headers        = Map.copyOf(headers != null
            ? headers : Map.of());
        this.body           = body != null ? body : "";
        this.timeoutSeconds = timeoutSeconds;
    }

    // ── Convenience constructors — delegate upward ────────────────────
    public HttpRequest(String method, String url,
                       Map<String, String> headers) {
        this(method, url, headers, null, 30);
    }

    public HttpRequest(String method, String url, String body) {
        this(method, url, null, body, 30);
    }

    public HttpRequest(String method, String url) {
        this(method, url, null, null, 30);
    }

    // ── Static convenience constructors ──────────────────────────────
    public static HttpRequest get(String url) {
        return new HttpRequest("GET", url);
    }

    public static HttpRequest post(String url, String body) {
        return new HttpRequest("POST", url, body);
    }

    public String getMethod()  { return method;         }
    public String getUrl()     { return url;            }
    public String getBody()    { return body;           }
    public int    getTimeout() { return timeoutSeconds; }

    @Override public String toString() {
        return method + " " + url;
    }
}

// ── Usage ─────────────────────────────────────────────────────────────
HttpRequest req1 = new HttpRequest("GET", "https://api.example.com/users");
HttpRequest req2 = HttpRequest.get("https://api.example.com/users");
HttpRequest req3 = HttpRequest.post("/orders",
    "{"item":"book"}");

Factory Methods

A factory method is a static method that creates and returns objects. It has significant advantages over constructors: it can have descriptive names that communicate what kind of object is being created (Money.ofDollars() vs new Money()), it can return a cached or pooled instance instead of always creating a new one, it can return a subtype without the caller knowing the concrete type, and it can perform more complex creation logic that would be awkward in a constructor. The naming conventions for factory methods are established in the JDK itself: of() and copyOf() for value-based creation (Set.of(), List.copyOf()), from() for conversion (Instant.from(temporal)), valueOf() for boxed primitives (Integer.valueOf()), getInstance() for singletons, newInstance() to guarantee a new instance, and create() for general creation. Choosing the right name makes the method's semantics immediately clear.
Java
// ── Factory methods with meaningful names ────────────────────────────
public final class Money {

    private final long   amountCents;  // stored as cents to avoid float
    private final String currency;

    // Private constructor — callers must use factory methods
    private Money(long amountCents, String currency) {
        this.amountCents = amountCents;
        this.currency    = currency;
    }

    // ── Named factory methods ──────────────────────────────────────────
    public static Money ofCents(long cents, String currency) {
        if (cents < 0) throw new IllegalArgumentException(
            "Amount cannot be negative");
        return new Money(cents, currency.toUpperCase());
    }

    public static Money ofDollars(double dollars) {
        return ofCents(Math.round(dollars * 100), "USD");
    }

    public static Money ofEuros(double euros) {
        return ofCents(Math.round(euros * 100), "EUR");
    }

    public static Money zero(String currency) {
        return ofCents(0, currency);
    }

    // ── Caching example — reuse commonly needed instances ─────────────
    private static final Map<String, Money> ZERO_CACHE =
        Map.of("USD", ofCents(0, "USD"),
               "EUR", ofCents(0, "EUR"),
               "GBP", ofCents(0, "GBP"));

    public static Money zeroFor(String currency) {
        return ZERO_CACHE.getOrDefault(
            currency.toUpperCase(),
            ofCents(0, currency));
    }

    // ── Conversion factory ────────────────────────────────────────────
    public static Money from(BigDecimal amount, String currency) {
        return ofCents(
            amount.multiply(BigDecimal.valueOf(100))
                  .longValueExact(),
            currency);
    }

    public Money add(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException(
                "Cannot add different currencies");
        }
        return new Money(amountCents + other.amountCents, currency);
    }

    public BigDecimal toDecimal() {
        return BigDecimal.valueOf(amountCents, 2);
    }

    @Override public String toString() {
        return toDecimal().toPlainString() + " " + currency;
    }
}

// ── Clear semantics at call sites ─────────────────────────────────────
Money price    = Money.ofDollars(29.99);
Money tax      = Money.ofCents(299, "USD");
Money total    = price.add(tax);
Money empty    = Money.zero("EUR");

The Builder Pattern

The builder pattern solves the telescoping constructor problem: when a class has many fields, some optional and some required, the number of constructor combinations explodes. A builder is a companion object that accumulates field values through fluent setter calls and then creates the target object in a single build() call. The target class's constructor is private, so the only way to create it is through the builder. Builders provide several benefits: the calling code is self-documenting because each setter has a name that describes the field being set; optional fields simply do not need to be set; the build() method can perform cross-field validation that would be impractical in a constructor; and the builder can create different configurations of the same object without multiple constructors. Lombok's @Builder annotation generates the builder pattern automatically, eliminating all the boilerplate. For hand-written builders, the standard structure is: a static inner class named Builder, one field per target field, a private constructor on the target taking a Builder, and a build() method that constructs the target.
Java
// ── Builder pattern — hand-written ────────────────────────────────────
public final class EmailMessage {

    // ── All fields in the target class ────────────────────────────────
    private final String        from;
    private final List<String>  to;
    private final List<String>  cc;
    private final List<String>  bcc;
    private final String        subject;
    private final String        body;
    private final boolean       htmlBody;
    private final List<String>  attachments;
    private final Instant       scheduledAt;

    // ── Private constructor — only Builder can call ───────────────────
    private EmailMessage(Builder builder) {
        this.from        = builder.from;
        this.to          = List.copyOf(builder.to);
        this.cc          = List.copyOf(builder.cc);
        this.bcc         = List.copyOf(builder.bcc);
        this.subject     = builder.subject;
        this.body        = builder.body;
        this.htmlBody    = builder.htmlBody;
        this.attachments = List.copyOf(builder.attachments);
        this.scheduledAt = builder.scheduledAt;
    }

    // ── Getters ───────────────────────────────────────────────────────
    public String       getFrom()        { return from;        }
    public List<String> getTo()          { return to;          }
    public String       getSubject()     { return subject;     }
    public String       getBody()        { return body;        }
    public boolean      isHtmlBody()     { return htmlBody;    }
    public List<String> getAttachments() { return attachments; }
    public Instant      getScheduledAt() { return scheduledAt; }

    // ── Entry point to the builder ────────────────────────────────────
    public static Builder builder(String from, String subject) {
        return new Builder(from, subject);
    }

    // ── Static inner Builder class ────────────────────────────────────
    public static final class Builder {

        // Required fields — set in Builder constructor
        private final String from;
        private final String subject;

        // Optional fields — have defaults
        private List<String> to          = new ArrayList<>();
        private List<String> cc          = new ArrayList<>();
        private List<String> bcc         = new ArrayList<>();
        private String       body        = "";
        private boolean      htmlBody    = false;
        private List<String> attachments = new ArrayList<>();
        private Instant      scheduledAt = null;

        private Builder(String from, String subject) {
            this.from    = Objects.requireNonNull(from);
            this.subject = Objects.requireNonNull(subject);
        }

        public Builder to(String... addresses) {
            this.to.addAll(Arrays.asList(addresses));
            return this;
        }

        public Builder cc(String... addresses) {
            this.cc.addAll(Arrays.asList(addresses));
            return this;
        }

        public Builder body(String body) {
            this.body = body;
            return this;
        }

        public Builder htmlBody(String html) {
            this.body     = html;
            this.htmlBody = true;
            return this;
        }

        public Builder attach(String filePath) {
            this.attachments.add(filePath);
            return this;
        }

        public Builder schedule(Instant at) {
            this.scheduledAt = at;
            return this;
        }

        public EmailMessage build() {
            // Cross-field validation
            if (to.isEmpty() && cc.isEmpty() && bcc.isEmpty()) {
                throw new IllegalStateException(
                    "Email must have at least one recipient");
            }
            return new EmailMessage(this);
        }
    }
}

// ── Fluent, readable creation ─────────────────────────────────────────
EmailMessage email = EmailMessage
    .builder("noreply@myapp.com", "Welcome to MyApp!")
    .to("alice@example.com", "bob@example.com")
    .cc("manager@myapp.com")
    .htmlBody("<h1>Welcome!</h1><p>Thanks for joining.</p>")
    .attach("/invoices/welcome-pack.pdf")
    .build();

Copy Constructors and Deep vs Shallow Copy

A copy constructor creates a new object that is a copy of an existing object. It takes one parameter of the same type as the class. Copy constructors are preferred over clone() — which is broken by design — because they are explicit, do not require checked exceptions, and work correctly with final fields and class hierarchies. Shallow copy copies the references to nested objects but not the objects themselves. Two copies share the same nested objects — mutating a nested object in one affects the other. Deep copy recursively copies all nested objects so the two copies are entirely independent. The right choice depends on whether the nested objects are mutable and whether independence is required. Knowing whether you need a shallow or deep copy is a design question: if the nested objects are immutable (String, Integer, LocalDate), shallow copy is safe because they cannot be mutated. If they are mutable (ArrayList, HashMap, custom mutable classes), deep copy is usually required to prevent unexpected coupling between the original and the copy.
Java
// ── Shallow copy — copies references, not nested objects ─────────────
public class ShallowCopyExample {

    private String       name;      // String is immutable — safe to share
    private List<String> tags;      // List IS mutable — DANGER

    public ShallowCopyExample(ShallowCopyExample other) {
        this.name = other.name;     // safe — String is immutable
        this.tags = other.tags;     // SHALLOW — same List object!
    }
}

// Problem:
ShallowCopyExample original = new ShallowCopyExample("item",
    new ArrayList<>(List.of("a", "b")));
ShallowCopyExample copy = new ShallowCopyExample(original);

copy.tags.add("c");
System.out.println(original.tags);  // [a, b, c] — original is affected!

// ── Deep copy — independent copies of all mutable nested objects ──────
public class Order {

    private final String       orderId;
    private final String       customerId;
    private final List<OrderItem> items;
    private final Map<String, String> metadata;
    private       OrderStatus  status;

    // ── Normal constructor ─────────────────────────────────────────────
    public Order(String orderId, String customerId,
                 List<OrderItem> items) {
        this.orderId    = orderId;
        this.customerId = customerId;
        this.items      = new ArrayList<>(items);  // defensive copy
        this.metadata   = new HashMap<>();
        this.status     = OrderStatus.PENDING;
    }

    // ── Deep copy constructor ──────────────────────────────────────────
    public Order(Order other) {
        this.orderId    = other.orderId;     // String — immutable, safe
        this.customerId = other.customerId;  // String — immutable, safe
        this.status     = other.status;      // enum — immutable, safe

        // Deep copy the mutable List — copy each item too
        this.items = other.items.stream()
            .map(OrderItem::new)             // OrderItem copy constructor
            .collect(Collectors.toList());

        // Deep copy the mutable Map
        this.metadata = new HashMap<>(other.metadata);
    }

    public void addTag(String key, String value) {
        metadata.put(key, value);
    }
    public List<OrderItem> getItems() {
        return Collections.unmodifiableList(items);
    }
}

// ── Deep copy in action ───────────────────────────────────────────────
Order original = new Order("ORD-1", "CUST-1",
    List.of(new OrderItem("prod1", 2)));
Order copy = new Order(original);

copy.addTag("note", "priority");
System.out.println(original.metadata);  // {} — original unaffected

// ── When to use each approach ─────────────────────────────────────────
//
// Shallow copy: all nested objects are immutable (String, Integer,
//               LocalDate, ImmutableList, etc.)
//
// Deep copy:    nested objects are mutable AND independence required
//               (ArrayList, HashMap, custom mutable classes)
//
// No copy needed: the class is immutable — share freely

Related Topics in Object-Oriented Programming

OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Default Constructor
A default constructor is a constructor that takes no parameters. If you write a class without any constructor, the Java compiler automatically inserts a no-argument constructor that calls the superclass no-argument constructor and does nothing else. The moment you define any constructor yourself — with or without parameters — the compiler no longer inserts the default constructor. Understanding when the default constructor exists, when it disappears, and what it initialises is fundamental to understanding how Java objects come into existence.