Skip to content
Part IA Michaelmas Term

Constructor Overloading

Multiple Ways to Construct

A class often needs to support different ways of creating an instance. Constructor overloading provides multiple entry points for initialisation, each with a distinct parameter list.

public class Temperature {
    private final double celsius;

    public Temperature(double celsius) {
        this.celsius = celsius;
    }

    public Temperature() {
        this(0.0);
    }

    public Temperature(Temperature other) {
        this(other.celsius);
    }
}

Three constructors: one takes an explicit value, one creates a default (freezing point of water), and one is a copy constructor.

Constructor Chaining with this(…)

Constructor overloading is most useful when combined with constructor chaining: one constructor delegates to another using this(...) to avoid duplicating initialisation logic.

public class BankAccount {
    private final String accountNumber;
    private final String holderName;
    private double balance;
    private static int nextId = 1;

    public BankAccount(String holderName, double initialDeposit, String accountNumber) {
        this.holderName = holderName;
        this.balance = initialDeposit;
        this.accountNumber = accountNumber;
        nextId = nextId + 1;
    }

    public BankAccount(String holderName, double initialDeposit) {
        this(holderName, initialDeposit, "ACC-" + String.format("%04d", nextId));
    }

    public BankAccount(String holderName) {
        this(holderName, 0.0);
    }
}

The three-argument constructor is the “full” constructor — it does all the real work. The two-argument constructor delegates to it with a generated account number. The one-argument constructor delegates to the two-argument version with a default deposit of 0.0. The initialisation logic lives in one place.

The First-Statement Rule

A call to this(...) — or to super(...) — must be the first statement in a constructor body. You cannot put any other code before it:

public BankAccount(String holderName) {
    System.out.println("Creating account...");  // COMPILE ERROR
    this(holderName, 0.0);                      // this(...) must be first
}

The reason: the JVM must initialise the object’s state through the constructor chain before any instance-specific logic runs. The delegated-to constructor will initialise the fields; code before the delegation might access uninitialised state.

The Default Constructor

If you write a class with no constructors at all, the Java compiler automatically provides a default no-argument constructor:

public class Point {
    private int x;
    private int y;

}

Point p = new Point();   // OK — default constructor exists

This implicitly supplied constructor is public, takes no arguments, and initialises all fields to their default values (0, false, null).

However, once you write any constructor, the default constructor disappears:

public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

}

Point p1 = new Point(3, 4);  // OK
Point p2 = new Point();       // COMPILE ERROR — no no-arg constructor

If you need both a no-arg constructor and a parameterised one, you must write both explicitly.

Common Constructor Patterns

The Full Constructor + Convenience Pattern

Write one constructor that accepts all fields, then define convenience constructors that supply defaults:

public class Person {
    private String name;
    private int age;
    private String email;

    public Person(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }

    public Person(String name, int age) {
        this(name, age, "[email protected]");
    }
}

The Copy Constructor

A copy constructor creates a new object from an existing one, copying its state:

public Person(Person other) {
    this(other.name, other.age, other.email);
}

This is a shallow copy — the fields are copied directly. For mutable fields (e.g. arrays, collections), a deep copy might be needed to achieve full independence. See the encapsulation and immutability notes for defensive copying.

The Static Factory Method Alternative

Sometimes, factory methods are clearer than constructor overloading because they can have descriptive names:

public static Temperature fromFahrenheit(double fahrenheit) {
    return new Temperature((fahrenheit - 32.0) * 5.0 / 9.0);
}

public static Temperature fromKelvin(double kelvin) {
    return new Temperature(kelvin - 273.15);
}

Constructors must share the class name; factory methods can express intent through their names. This is discussed further in the static methods notes.

Constructor Invocation Order

When an object is created, the JVM follows a specific sequence:

  1. Memory is allocated for all fields (instance and inherited)
  2. Fields are initialised to their default values (0, false, null)
  3. Field initialisers (int x = 5;) and initialisation blocks run, in source order
  4. The constructor body executes

If the constructor calls this(...), step 4 chains to another constructor’s step 4. If it calls super(...) (explicitly or implicitly), the superclass constructor runs first.

Common Tripos Traps

  • Default constructor disappears: adding any constructor removes the automatic no-arg constructor. If a subclass constructor does not explicitly call super(...), the compiler inserts super() — which fails if the superclass has no no-arg constructor.
  • this(…) not first: placing any statement before this(...) or super(...) is a compile error.
  • Recursive constructor call: public Foo() { this(); } — infinite recursion, compile error (the compiler detects this).
  • Return type on constructor: public void BankAccount() { ... } — this is not a constructor, it is a regular method with the same name as the class (legal but never correct).