Constructor Overload
Constructor overloading means giving a class more than one constructor, each with a different parameter list. It lets callers create objects in whichever way suits the information they have, while the class still guarantees every object starts in a valid state. String, ArrayList, Thread, and most JDK classes offer several constructors for exactly this reason.
Basic Overloading
public class Rectangle {
private final double width;
private final double height;
public Rectangle() { // no arguments: a unit square
width = 1;
height = 1;
}
public Rectangle(double side) { // one argument: a square
width = side;
height = side;
}
public Rectangle(double width, double height) { // two arguments: general case
this.width = width;
this.height = height;
}
public double area() { return width * height; }
}
System.out.println(new Rectangle().area()); // 1.0
System.out.println(new Rectangle(5).area()); // 25.0
System.out.println(new Rectangle(4, 3).area()); // 12.0
The compiler decides which constructor to run by examining the arguments in each new expression. All three constructors produce a Rectangle; they differ only in how much the caller must specify.
How the Compiler Chooses
Constructors are distinguished by their parameter lists: the number, types, and order of parameters. Parameter names do not count.
public Rectangle(double width, double height) { ... }
public Rectangle(double height, double width) { ... } // compile error: same signature (double, double)
Both constructors take (double, double). Different names make no difference to the compiler, so the class would have two constructors with the same signature, which is not allowed.
Valid overloads differ in at least one of these:
| Difference | Example |
|---|---|
| Number of parameters | Box(), Box(int), Box(int, int) |
| Parameter types | Temperature(double celsius), Temperature(String text) |
| Order of different types | Item(String, int), Item(int, String) |
The last form is legal but invites mistakes, since swapping two arguments silently selects a different constructor. Avoid overloads that differ only in parameter order.
Matching rules
When arguments do not match any constructor exactly, the compiler applies conversions in a fixed order of preference:
- An exact match, or a widening primitive conversion (
int→long→double). - Autoboxing or unboxing (
int↔Integer). - Varargs.
public class Demo {
Demo(long x) { System.out.println("long"); }
Demo(Integer x) { System.out.println("Integer"); }
Demo(int... x) { System.out.println("varargs"); }
}
new Demo(5); // prints "long": widening is preferred over boxing and varargs
If two constructors are equally good matches, the call is ambiguous and does not compile:
public class Pair {
Pair(Integer a, long b) {}
Pair(long a, Integer b) {}
}
new Pair(1, 2); // compile error: reference to Pair is ambiguous
Constructor Chaining with this(...)
Writing the same assignments and validation in every constructor duplicates code and invites inconsistencies. Instead, one constructor can call another in the same class with this(...):
public class Rectangle {
private final double width;
private final double height;
public Rectangle(double width, double height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("dimensions must be positive");
}
this.width = width;
this.height = height;
}
public Rectangle(double side) {
this(side, side); // reuse the two-argument constructor
}
public Rectangle() {
this(1); // reuse the one-argument constructor
}
}
new Rectangle() → this(1) → this(1, 1) → validation and assignment
Validation now exists in exactly one place. Every path to creating a Rectangle passes through it, so new Rectangle(-5) is rejected the same way as new Rectangle(-5, 2).
The principal-constructor pattern
The most complete constructor, the one that takes every value, does the real work. The others are shortcuts that fill in defaults and delegate to it. This keeps behavior consistent and makes it obvious where to look when a class's initialization needs to change.
Rules for this(...)
this(...)can call only constructors of the same class;super(...)calls the parent's.- A constructor can contain at most one
this(...)orsuper(...)call, not both. - On Java 24 and earlier, the call must be the first statement in the constructor. Java 25 allows statements before it, provided they do not use the object under construction.
- Chains must end. A cycle is a compile error:
public Loop() { this(0); }
public Loop(int x) { this(); } // compile error: recursive constructor invocation
Overloading with Different Input Types
Overloads are especially useful when the same object can be built from different representations:
import java.time.LocalTime;
public class Duration24 {
private final int totalMinutes;
public Duration24(int minutes) {
if (minutes < 0) throw new IllegalArgumentException("negative duration");
this.totalMinutes = minutes;
}
public Duration24(int hours, int minutes) {
this(hours * 60 + minutes);
}
public Duration24(String text) { // "2h 30m", "45m", "1h"
this(parse(text));
}
public Duration24(LocalTime start, LocalTime end) {
this((int) java.time.Duration.between(start, end).toMinutes());
}
private static int parse(String text) {
int minutes = 0;
for (String part : text.strip().split("\\s+")) {
int value = Integer.parseInt(part.substring(0, part.length() - 1));
char unit = part.charAt(part.length() - 1);
if (unit == 'h') minutes += value * 60;
else if (unit == 'm') minutes += value;
else throw new IllegalArgumentException("Unknown unit in: " + part);
}
return minutes;
}
@Override
public String toString() {
return totalMinutes / 60 + "h " + totalMinutes % 60 + "m";
}
}
System.out.println(new Duration24(150)); // 2h 30m
System.out.println(new Duration24(1, 45)); // 1h 45m
System.out.println(new Duration24("3h 15m")); // 3h 15m
System.out.println(new Duration24(LocalTime.of(9, 0), LocalTime.of(17, 30))); // 8h 30m
Every constructor converts its input into minutes and delegates to Duration24(int), which performs the only validation. The String constructor calls a private static helper inside the this(...) call. The helper must be static because it runs before the object is initialized, when instance methods cannot yet be used.
Overloaded Constructors in the JDK
| Class | Constructors | Purpose |
|---|---|---|
ArrayList | (), (int initialCapacity), (Collection c) | Empty, pre-sized, or copied from another collection |
String | (), (char[]), (byte[], Charset), (StringBuilder), ... | Build text from many sources |
Thread | (), (Runnable), (Runnable, String name), ... | Different levels of configuration |
Scanner | (InputStream), (File), (Path), (String) | Read from different input sources |
List<String> a = new ArrayList<>(); // default capacity
List<String> b = new ArrayList<>(10_000); // avoid repeated resizing for a large list
List<String> c = new ArrayList<>(List.of("x", "y")); // copy of another collection
When Overloading Becomes a Problem
As optional parameters multiply, constructor overloads multiply too, and calls become hard to read:
new Pizza("large", true, false, true, false);
// which boolean is cheese? which is olives?
Constructors with many parameters of the same type are error-prone because the compiler cannot catch swapped arguments. Two alternatives scale better.
Static factory methods with descriptive names
public class Rectangle {
private final double width, height;
private Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public static Rectangle of(double width, double height) { return new Rectangle(width, height); }
public static Rectangle square(double side) { return new Rectangle(side, side); }
public static Rectangle unit() { return new Rectangle(1, 1); }
}
Names state intent (square versus of), and two factory methods with the same parameter types can coexist, which two constructors cannot.
The Builder pattern
For objects with many optional settings, a builder names each value as it is set:
public class Pizza {
private final String size;
private final boolean cheese, olives, mushrooms;
private Pizza(Builder b) {
this.size = b.size;
this.cheese = b.cheese;
this.olives = b.olives;
this.mushrooms = b.mushrooms;
}
public static class Builder {
private final String size; // required
private boolean cheese, olives, mushrooms;
public Builder(String size) { this.size = size; }
public Builder cheese() { cheese = true; return this; }
public Builder olives() { olives = true; return this; }
public Builder mushrooms() { mushrooms = true; return this; }
public Pizza build() { return new Pizza(this); }
}
@Override
public String toString() {
return size + (cheese ? " +cheese" : "") + (olives ? " +olives" : "")
+ (mushrooms ? " +mushrooms" : "");
}
}
Pizza p = new Pizza.Builder("large").cheese().mushrooms().build();
System.out.println(p); // large +cheese +mushrooms
Required values go into the builder's constructor; optional ones are set by name, in any order. HttpRequest.newBuilder(), StringBuilder, and many libraries' configuration APIs follow this pattern.
| Situation | Approach |
|---|---|
| Two to four parameters, few combinations | Overloaded constructors with this(...) chaining |
| Variants that need meaningful names | Static factory methods |
| Many optional parameters | Builder |
Practical Example: Notification Settings
public class Notification {
private final String recipient;
private final String message;
private final String channel;
private final int priority;
public Notification(String recipient, String message, String channel, int priority) {
if (recipient == null || recipient.isBlank()) {
throw new IllegalArgumentException("recipient required");
}
if (!channel.equals("email") && !channel.equals("sms") && !channel.equals("push")) {
throw new IllegalArgumentException("unknown channel: " + channel);
}
if (priority < 1 || priority > 5) {
throw new IllegalArgumentException("priority must be 1-5");
}
this.recipient = recipient;
this.message = message;
this.channel = channel;
this.priority = priority;
}
public Notification(String recipient, String message, String channel) {
this(recipient, message, channel, 3);
}
public Notification(String recipient, String message) {
this(recipient, message, recipient.contains("@") ? "email" : "sms");
}
@Override
public String toString() {
return "[" + channel + " p" + priority + "] " + recipient + ": " + message;
}
public static void main(String[] args) {
System.out.println(new Notification("ada@example.com", "Class starts at 10"));
System.out.println(new Notification("+919000000000", "OTP is 4821"));
System.out.println(new Notification("ada@example.com", "Server down", "push", 5));
}
}
Output:
[email p3] ada@example.com: Class starts at 10
[sms p3] +919000000000: OTP is 4821
[push p5] ada@example.com: Server down
The four-argument constructor is the principal one and holds all validation. The three-argument form supplies a default priority. The two-argument form picks a channel automatically, using an expression inside this(...). One caveat: because that expression calls recipient.contains(...) before the principal constructor's null check, passing null here throws NullPointerException instead of the friendlier IllegalArgumentException. Moving the decision into a static helper that handles null first would fix it. Callers choose how much to specify, and every notification, however it was created, passes the same checks.