☕ Java

Constructor Overloading

Constructor overloading means defining multiple constructors in the same class, each with a different parameter list. It gives callers several ways to create an object — with full details, with partial details using sensible defaults, or from a different form of data entirely. The Java compiler distinguishes overloaded constructors by their parameter count and types, choosing the correct one at compile time based on the arguments provided.

Why Overload Constructors

Different callers often need different amounts of information to create an object. A User object might be created with just an email address during registration, with a full profile including name, age, and preferences during onboarding, or from a database record that includes an ID and timestamps. All of these represent the same kind of object — a User — but each creation context has different available information. Without constructor overloading, you would be forced to accept all possible fields in one giant constructor, leaving callers to pass null or dummy values for fields they don't have. Or you would use a single no-arg constructor followed by a series of setter calls, losing the guarantee that the object is fully valid after construction. Constructor overloading solves this by providing multiple well-named paths to a valid object. Each overloaded constructor handles a specific creation context and ensures the object is fully initialised — either from the provided arguments or by supplying sensible defaults for the missing ones. The fundamental rule of overloading is that constructors must differ in their parameter list — either in the number of parameters, the types of parameters, or the order of parameter types. The compiler looks at these signatures to decide which constructor to call. Two constructors with exactly the same parameter types in the same order are considered duplicates and will not compile, even if the parameter names are different.
Java
// ── Why overloading is needed: ────────────────────────────────────────
// Scenario: creating a User in different contexts

// Context 1: quick sign-up — only email known:
User newSignup = new User("alice@example.com");

// Context 2: full profile during onboarding:
User fullProfile = new User("alice@example.com", "Alice Smith", 28);

// Context 3: loading from database — has ID and timestamps too:
User fromDb = new User(42L, "alice@example.com", "Alice Smith", 28,
                        LocalDateTime.now(), LocalDateTime.now());

// All three are valid User objects — different amounts of initial data.

// ── Overloaded constructors — different parameter lists: ──────────────
public class User {
    private Long          id;
    private String        email;
    private String        name;
    private int           age;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    // Constructor 1: minimal — email only:
    public User(String email) {
        this(email, "Anonymous", 0);
    }

    // Constructor 2: standard — email, name, age:
    public User(String email, String name, int age) {
        this(null, email, name, age, LocalDateTime.now(), LocalDateTime.now());
    }

    // Constructor 3: full — all fields (used when loading from DB):
    public User(Long id, String email, String name, int age,
                LocalDateTime createdAt, LocalDateTime updatedAt) {
        this.id        = id;
        this.email     = Objects.requireNonNull(email, "email required");
        this.name      = name;
        this.age       = age;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
    }

    @Override
    public String toString() {
        return String.format("User{id=%d, email='%s', name='%s', age=%d}",
            id, email, name, age);
    }
}

User u1 = new User("bob@example.com");
User u2 = new User("carol@example.com", "Carol", 35);
System.out.println(u1);   // User{id=null, email='bob@example.com', name='Anonymous', age=0}
System.out.println(u2);   // User{id=null, email='carol@example.com', name='Carol', age=35}

Overloading Rules and Disambiguation

The Java compiler resolves overloaded constructors using a process called overload resolution. When new is called, the compiler looks at the number and types of the provided arguments and finds the constructor whose parameter list most closely matches. If no constructor matches exactly, the compiler tries widening type conversions (int to long, float to double). If still no match is found, the call is a compile error. Ambiguity arises when two constructors could both match a given argument list after type widening. This is a compile error — the programmer must either use an explicit cast to select the intended constructor, or redesign the overloads so they are unambiguous. A common source of subtle bugs is confusing which overload is selected. If a class has constructors for (int) and (long), calling new Foo(42) selects the (int) version because 42 is an int literal. To select the (long) version you must write new Foo(42L). Similarly, (String) and (Object) overloads can cause confusion — passing a String always selects the String version since it is a more specific type.
Java
// ── Overload resolution — compiler picks the best match: ────────────
public class Timer {
    private long durationMs;

    public Timer(int seconds) {
        this.durationMs = seconds * 1000L;
        System.out.println("int constructor: " + seconds + " seconds");
    }

    public Timer(long milliseconds) {
        this.durationMs = milliseconds;
        System.out.println("long constructor: " + milliseconds + " ms");
    }

    public Timer(double seconds) {
        this.durationMs = (long)(seconds * 1000);
        System.out.println("double constructor: " + seconds + " seconds");
    }
}

new Timer(30);       // int literal → Timer(int)    — int constructor: 30 seconds
new Timer(30L);      // long literal → Timer(long)  — long constructor: 30000 ms
new Timer(1.5);      // double literal → Timer(double) — double constructor: 1.5 seconds
new Timer(30000L);   // long literal → Timer(long)  — long constructor: 30000 ms

// ── Ambiguity — compile error: ────────────────────────────────────────
public class Ambiguous {
    public Ambiguous(int a, double b)  { System.out.println("int, double"); }
    public Ambiguous(double a, int b)  { System.out.println("double, int"); }
}
// new Ambiguous(1, 1);  // COMPILE ERROR — ambiguous: both require one widening

// Resolve with explicit cast:
new Ambiguous(1, (double) 1);  // selects (int, double)
new Ambiguous((double) 1, 1);  // selects (double, int)

// ── Constructors must differ by parameter list — not name or return: ──
public class Demo {
    public Demo(String name, int age) { }
    // public Demo(String label, int count) { }  // COMPILE ERROR — same signature
    // Names don't matter — only types and count determine uniqueness.

    public Demo(int age, String name) { }  // ✓ different ORDER is allowed
    public Demo(String name)          { }  // ✓ different COUNT is allowed
    public Demo(String name, long age){ }  // ✓ different TYPE is allowed
}

Constructor Chaining to Avoid Duplication

When multiple overloaded constructors share common logic, duplicating that logic in each constructor is a maintenance problem. Change the validation rules, and you must remember to update every constructor. The solution is constructor chaining: one constructor handles all the actual work and all other constructors delegate to it using this(...). The canonical pattern is the telescoping constructor: each shorter constructor calls the next longer one, adding default values for the parameters it omits, until the fullest constructor is reached. The fullest constructor does the actual validation and field assignment. This guarantees there is exactly one place in the class where fields are set, making the class easier to maintain. Constructor chaining with this(...) is strictly more correct than extracting shared logic into a private helper method, for one important reason: final fields can only be set in the constructor, not in a method called from a constructor. If a constructor calls a helper method that tries to set final fields, it will not compile. Constructor chaining with this(...) does not have this problem because the delegated constructor is itself a proper constructor.
Java
// ── Telescoping constructor pattern: ─────────────────────────────────
public class HttpRequest {
    private final String  method;
    private final String  url;
    private final Map<String, String> headers;
    private final String  body;
    private final int     timeoutMs;

    // Most specific — all logic, all validation here:
    public HttpRequest(String method, String url,
                        Map<String, String> headers,
                        String body, int timeoutMs) {
        this.method    = Objects.requireNonNull(method, "method required")
                                .toUpperCase();
        this.url       = Objects.requireNonNull(url, "url required");
        this.headers   = headers != null
                            ? new HashMap<>(headers)
                            : new HashMap<>();
        this.body      = body;
        this.timeoutMs = timeoutMs > 0 ? timeoutMs : 30_000;
    }

    // No body — delegates up with null body:
    public HttpRequest(String method, String url,
                        Map<String, String> headers, int timeoutMs) {
        this(method, url, headers, null, timeoutMs);
    }

    // No headers, no body — delegates with empty headers:
    public HttpRequest(String method, String url, int timeoutMs) {
        this(method, url, null, null, timeoutMs);
    }

    // Default timeout — delegates with 30 second timeout:
    public HttpRequest(String method, String url) {
        this(method, url, null, null, 30_000);
    }

    public String getMethod()    { return method;    }
    public String getUrl()       { return url;       }
    public String getBody()      { return body;      }
    public int    getTimeoutMs() { return timeoutMs; }

    @Override
    public String toString() {
        return method + " " + url + " (timeout=" + timeoutMs + "ms)";
    }
}

// ── Usage — all paths lead to valid, fully initialised objects: ───────
HttpRequest req1 = new HttpRequest("GET", "https://api.example.com/users");
HttpRequest req2 = new HttpRequest("GET", "https://api.example.com/users", 5000);
HttpRequest req3 = new HttpRequest("POST", "https://api.example.com/users",
                        Map.of("Content-Type", "application/json"), 10_000);
HttpRequest req4 = new HttpRequest("POST", "https://api.example.com/users",
                        Map.of("Content-Type", "application/json"),
                        "{"name":"Alice"}", 10_000);

System.out.println(req1);  // GET https://api.example.com/users (timeout=30000ms)
System.out.println(req2);  // GET https://api.example.com/users (timeout=5000ms)

Builder Pattern — Alternative to Many Constructors

When a class has many optional fields, the telescoping constructor pattern breaks down. A class with ten fields and five optional ones would need dozens of constructor overloads to cover every combination. The Builder pattern solves this by separating the construction process from the final object. A nested Builder class accumulates configuration through method calls, each returning the Builder for chaining, and a final build() method validates everything and constructs the immutable object. The Builder pattern trades constructor overloading for a more readable, flexible API. Where new Config("a", null, null, null, true, 8080) requires the caller to count arguments and guess what null means, Config.builder().host("a").debug(true).port(8080).build() is self-documenting. It also makes it easy to add new optional fields without breaking existing callers.
Java
// ── Builder pattern for complex object construction: ─────────────────
public final class DatabaseConfig {
    // All fields final — immutable once built:
    private final String host;
    private final int    port;
    private final String database;
    private final String username;
    private final String password;
    private final int    maxPoolSize;
    private final int    connectionTimeoutMs;
    private final boolean sslEnabled;

    // Private constructor — only Builder can call it:
    private DatabaseConfig(Builder builder) {
        this.host                 = builder.host;
        this.port                 = builder.port;
        this.database             = builder.database;
        this.username             = builder.username;
        this.password             = builder.password;
        this.maxPoolSize          = builder.maxPoolSize;
        this.connectionTimeoutMs  = builder.connectionTimeoutMs;
        this.sslEnabled           = builder.sslEnabled;
    }

    public String  getHost()                { return host; }
    public int     getPort()                { return port; }
    public String  getDatabase()            { return database; }
    public boolean isSslEnabled()           { return sslEnabled; }
    public int     getMaxPoolSize()         { return maxPoolSize; }
    public int     getConnectionTimeoutMs() { return connectionTimeoutMs; }

    // ── Static nested Builder class: ──────────────────────────────────
    public static class Builder {
        // Required fields:
        private final String host;
        private final String database;

        // Optional fields — sensible defaults:
        private int     port                = 5432;
        private String  username            = "postgres";
        private String  password            = "";
        private int     maxPoolSize         = 10;
        private int     connectionTimeoutMs = 30_000;
        private boolean sslEnabled          = false;

        public Builder(String host, String database) {
            this.host     = Objects.requireNonNull(host,     "host required");
            this.database = Objects.requireNonNull(database, "database required");
        }

        public Builder port(int port) {
            if (port < 1 || port > 65535)
                throw new IllegalArgumentException("Invalid port: " + port);
            this.port = port;
            return this;            // return this for method chaining
        }

        public Builder username(String username) {
            this.username = Objects.requireNonNull(username);
            return this;
        }

        public Builder password(String password) {
            this.password = Objects.requireNonNull(password);
            return this;
        }

        public Builder maxPoolSize(int size) {
            if (size < 1) throw new IllegalArgumentException(
                "Pool size must be at least 1");
            this.maxPoolSize = size;
            return this;
        }

        public Builder connectionTimeout(int ms) {
            this.connectionTimeoutMs = ms;
            return this;
        }

        public Builder ssl(boolean enabled) {
            this.sslEnabled = enabled;
            return this;
        }

        public DatabaseConfig build() {
            // Final validation before constructing:
            if (sslEnabled && password.isEmpty())
                throw new IllegalStateException(
                    "Password required when SSL is enabled");
            return new DatabaseConfig(this);
        }
    }
}

// ── Fluent, readable construction: ───────────────────────────────────
DatabaseConfig config = new DatabaseConfig.Builder("db.example.com", "orders")
    .port(5433)
    .username("app_user")
    .password("s3cur3!")
    .maxPoolSize(20)
    .ssl(true)
    .build();

System.out.println(config.getHost());        // db.example.com
System.out.println(config.isSslEnabled());   // true

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.
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.