Skip to main content

6.3 Constructor

When creating an object from a class, a special code block that initializes instance variables to desired values at the moment of creation is called a Constructor.

1. What Is a Constructor?

A constructor looks like a method but has a few unique characteristics.

Three Characteristics of a Constructor

  1. Its name must be exactly the same as the class name.
  2. It has no return type— not even void.
  3. It is automatically called exactly once when an object is created with the new keyword.
public class Person {
String name;
int age;

// Constructor: same name as class, no return type
Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Person object created: " + name + ", age " + age);
}
}

public class PersonTest {
public static void main(String[] args) {
// The constructor runs automatically when new Person(...) is called
Person p1 = new Person("Hong Gildong", 25);
// Output: Person object created: Hong Gildong, age 25

Person p2 = new Person("Yi Sunsin", 40);
// Output: Person object created: Yi Sunsin, age 40
}
}
Why Use a Constructor?

Even without a constructor, you can assign values to fields directly after creating an object. However, using a constructor guarantees initialization at the moment of object creation, preventing incomplete objects from being used.

2. Default Constructor

A constructor with no parameters is called a Default Constructor.

Condition for the Compiler to Provide One Automatically

Only when no constructor at all is declared in the class does the compiler automatically add an empty default constructor.

// Developer writes no constructor
class Dog {
String name;
}
// Compiler automatically adds: Dog() { }

Dog d = new Dog(); // works fine

When the Default Constructor Disappears

class Dog {
String name;

// Once a parameterized constructor is declared,
// the compiler no longer adds the default constructor automatically!
Dog(String name) {
this.name = name;
}
}

public class DogTest {
public static void main(String[] args) {
// Dog d = new Dog(); // Compile error! No default constructor
Dog d = new Dog("Rex"); // works fine
}
}
Default Constructor Caution

As soon as you declare any parameterized constructor, the default constructor is no longer added automatically. If you still need a default constructor, declare one explicitly.

3. Constructor Overloading

Constructors can be overloaded just like methods. Multiple constructors with different parameter types, counts, or orders can be declared.

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

// Constructor 1: default constructor
Person() {
name = "No Name";
age = 0;
email = "";
}

// Constructor 2: name only
Person(String name) {
this.name = name;
this.age = 0;
this.email = "";
}

// Constructor 3: name + age
Person(String name, int age) {
this.name = name;
this.age = age;
this.email = "";
}

// Constructor 4: all fields
Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}

void printInfo() {
System.out.printf("Name: %s, Age: %d, Email: %s%n", name, age, email);
}
}

public class PersonTest {
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = new Person("Hong Gildong");
Person p3 = new Person("Yi Sunsin", 40);
Person p4 = new Person("Jang Bogo", 35, "jbg@korea.com");

p1.printInfo(); // Name: No Name, Age: 0, Email:
p2.printInfo(); // Name: Hong Gildong, Age: 0, Email:
p3.printInfo(); // Name: Yi Sunsin, Age: 40, Email:
p4.printInfo(); // Name: Jang Bogo, Age: 35, Email: jbg@korea.com
}
}

4. Constructor Chaining with this()

The constructors above have duplicated initialization code. Using this() can remove that duplication.

Rules for this()

  1. It can only be written as the first line of a constructor.
  2. It can only be used inside a constructor, not a method.
public class Person {
String name;
int age;
String email;

// Default constructor: delegates to the constructor with the most parameters
Person() {
this("No Name", 0, ""); // constructor chaining!
}

Person(String name) {
this(name, 0, ""); // constructor chaining!
}

Person(String name, int age) {
this(name, age, ""); // constructor chaining!
}

// Core constructor: where the actual initialization logic lives
Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
}
Advantage of Constructor Chaining

By concentrating initialization logic in the constructor with the most parameters, only that constructor needs to be modified when initialization logic changes. This reduces code duplication and improves maintainability.

5. Copy Constructor

Used to create a new object with the same state as an existing object.

public class Point {
int x;
int y;

// Regular constructor
Point(int x, int y) {
this.x = x;
this.y = y;
}

// Copy constructor: receives an existing Point object and initializes to same values
Point(Point other) {
this.x = other.x;
this.y = other.y;
}

@Override
public String toString() {
return "Point(" + x + ", " + y + ")";
}
}

public class CopyConstructorTest {
public static void main(String[] args) {
Point original = new Point(3, 7);
Point copy = new Point(original); // use copy constructor

System.out.println("Original: " + original); // Point(3, 7)
System.out.println("Copy: " + copy); // Point(3, 7)

// Changing the copy does not affect the original (independent objects)
copy.x = 100;
System.out.println("Original after change: " + original); // Point(3, 7)
System.out.println("Copy after change: " + copy); // Point(100, 7)
}
}

6. Immutable Object

An object whose state cannot be changed after creation. Uses private final fields and initializes only through the constructor.

public final class ImmutablePoint {
// final fields: cannot be changed once set
private final int x;
private final int y;

// Can only be initialized in the constructor
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}

// Only getters — no setters
public int getX() { return x; }
public int getY() { return y; }

// To move to a new position, return a new object
public ImmutablePoint move(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy);
}

@Override
public String toString() {
return "ImmutablePoint(" + x + ", " + y + ")";
}
}

public class ImmutableTest {
public static void main(String[] args) {
ImmutablePoint p1 = new ImmutablePoint(3, 5);
System.out.println("Original: " + p1); // ImmutablePoint(3, 5)

// p1.x = 10; // Compile error! final field cannot be modified

// Moving creates a new object (original is unchanged)
ImmutablePoint p2 = p1.move(2, 3);
System.out.println("Original after move: " + p1); // ImmutablePoint(3, 5)
System.out.println("p2 after move: " + p2); // ImmutablePoint(5, 8)
}
}
Benefits of Immutable Objects
  • Thread-Safe: Safe for concurrent access since the state never changes.
  • Predictable: The value is always the same no matter where it is read, making debugging easy.
  • Usage in Java: String, Integer, LocalDate, and others are immutable classes.

7. Introduction to the Builder Pattern

Constructors with many parameters are hard to read. The Builder pattern solves this.

public class Computer {
// Required fields
private final String cpu;
private final String ram;
// Optional fields
private final String storage;
private final String gpu;
private final boolean wifi;

// private constructor: can only be created through the Builder
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.storage = builder.storage;
this.gpu = builder.gpu;
this.wifi = builder.wifi;
}

// Inner Builder class
public static class Builder {
private final String cpu; // required
private final String ram; // required
private String storage = "256GB SSD"; // default value
private String gpu = "Integrated Graphics"; // default value
private boolean wifi = true; // default value

// Required values in Builder constructor
public Builder(String cpu, String ram) {
this.cpu = cpu;
this.ram = ram;
}

// Optional values via method chaining
public Builder storage(String storage) {
this.storage = storage;
return this;
}

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

public Builder wifi(boolean wifi) {
this.wifi = wifi;
return this;
}

public Computer build() {
return new Computer(this);
}
}

@Override
public String toString() {
return String.format("Computer{cpu='%s', ram='%s', storage='%s', gpu='%s', wifi=%b}",
cpu, ram, storage, gpu, wifi);
}
}

public class BuilderTest {
public static void main(String[] args) {
// Basic configuration
Computer basic = new Computer.Builder("Intel i5", "8GB")
.build();

// High-end configuration
Computer gaming = new Computer.Builder("Intel i9", "32GB")
.storage("2TB NVMe SSD")
.gpu("RTX 4090")
.wifi(true)
.build();

System.out.println(basic);
System.out.println(gaming);
}
}

Output:

Computer{cpu='Intel i5', ram='8GB', storage='256GB SSD', gpu='Integrated Graphics', wifi=true}
Computer{cpu='Intel i9', ram='32GB', storage='2TB NVMe SSD', gpu='RTX 4090', wifi=true}

8. Practical Example: Complete Person Class

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

// Default constructor
public Person() {
this("Default Name", 0, "", "");
}

// Name only
public Person(String name) {
this(name, 0, "", "");
}

// Name + age
public Person(String name, int age) {
this(name, age, "", "");
}

// Name + age + email
public Person(String name, int age, String email) {
this(name, age, email, "");
}

// Core constructor
public Person(String name, int age, String email, String phone) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name is required.");
}
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Invalid age: " + age);
}
this.name = name;
this.age = age;
this.email = email;
this.phone = phone;
}

// Copy constructor
public Person(Person other) {
this(other.name, other.age, other.email, other.phone);
}

// Getters
public String getName() { return name; }
public int getAge() { return age; }
public String getEmail() { return email; }
public String getPhone() { return phone; }

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

public class PersonTest {
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = new Person("Yi Sunsin");
Person p3 = new Person("Jang Bogo", 35);
Person p4 = new Person("King Sejong", 50, "sejong@joseon.kr", "010-1234-5678");
Person p5 = new Person(p4); // copy

System.out.println(p1);
System.out.println(p2);
System.out.println(p3);
System.out.println(p4);
System.out.println(p5);

// Validation test
try {
Person invalid = new Person("", -5); // exception thrown
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
}
}

Output:

Person{name='Default Name', age=0, email='', phone=''}
Person{name='Yi Sunsin', age=0, email='', phone=''}
Person{name='Jang Bogo', age=35, email='', phone=''}
Person{name='King Sejong', age=50, email='sejong@joseon.kr', phone='010-1234-5678'}
Person{name='King Sejong', age=50, email='sejong@joseon.kr', phone='010-1234-5678'}
Error: Name is required.

Summary

ConceptKey Content
ConstructorSame name as class, no return type, automatically called by new
Default constructorAutomatically added by compiler when no constructor exists
Constructor overloadingMultiple constructors with different parameters
this()Calls another constructor in the same class; must be on the first line
Copy constructorCreates a new object with the same state as an existing object
Immutable objectprivate final fields + initialized only through constructor
Builder patternReadable object creation when there are many parameters