Constructors and Initialization
Duration: 45 min
Constructors and Initialization
Duration: 45 min
Introduction
Constructors are special methods that initialize objects in a consistent, controlled state. Java provides multiple ways to initialize: default constructors, parameterized constructors, constructor chaining, copy constructors, and initialization blocks. Mastering these patterns ensures objects are always valid, prevents bugs, and makes code maintainable.
This module covers constructor overloading, the this() call for chaining, copy constructors, initialization blocks, and best practices for ensuring proper object initialization.
Constructor Overloading
Java allows multiple constructors with different parameter lists. Each provides a different way to initialize an object.
public class Rectangle {
private double width;
private double height;
// Constructor 1: No arguments - default dimensions
public Rectangle() {
this.width = 1.0;
this.height = 1.0;
}
// Constructor 2: Both dimensions specified
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
// Constructor 3: Square (width = height)
public Rectangle(double side) {
this.width = side;
this.height = side;
}
public double getArea() {
return width * height;
}
public void display() {
System.out.printf("Rectangle: %.1f × %.1f, Area: %.2f%n", width, height, getArea());
}
}public class RectangleDemo {
public static void main(String[] args) {
Rectangle r1 = new Rectangle(); // Uses Constructor 1
r1.display(); // 1.0 × 1.0
Rectangle r2 = new Rectangle(5, 3); // Uses Constructor 2
r2.display(); // 5.0 × 3.0
Rectangle r3 = new Rectangle(4); // Uses Constructor 3
r3.display(); // 4.0 × 4.0 (square)
}
}
Constructors are distinguished by parameter type and count (not return type). Providing multiple constructors gives flexibility—users choose the most convenient version. This is the overloading principle.
Constructor Chaining
Constructor chaining uses this() to call another constructor in the same class. This eliminates duplication and ensures consistent initialization logic.
public class Date {
private int day;
private int month;
private int year;
// Constructor 1: Full initialization
public Date(int day, int month, int year) {
this.day = day;
this.month = month;
this.year = year;
}
// Constructor 2: Current year assumed
public Date(int day, int month) {
this(day, month, 2024); // Calls Constructor 1
}
// Constructor 3: Default to January 1st
public Date() {
this(1, 1, 2024); // Calls Constructor 2, which calls Constructor 1
}
public void display() {
System.out.printf("%02d/%02d/%04d%n", month, day, year);
}
}public class DateDemo {
public static void main(String[] args) {
Date d1 = new Date(15, 3, 2023);
d1.display(); // 03/15/2023
Date d2 = new Date(25, 12);
d2.display(); // 12/25/2024
Date d3 = new Date();
d3.display(); // 01/01/2024
}
}
The this() call must be the first statement in a constructor. Chaining reduces code duplication and ensures all initialization flows through the fullest constructor. This makes maintenance easier—change initialization logic in one place.
Copy Constructors
A copy constructor creates a new object as a copy of an existing one. It takes an object of the same class as parameter.
public class Person {
private String name;
private int age;
private String email;
// Standard constructor
public Person(String name, int age) {
this.name = name;
this.age = age;
this.email = "";
}
// Copy constructor
public Person(Person other) {
this.name = other.name;
this.age = other.age;
this.email = other.email;
}
public void setEmail(String email) {
this.email = email;
}
public void display() {
System.out.println(name + " (" + age + ") - " + email);
}
}public class CopyConstructorDemo {
public static void main(String[] args) {
Person original = new Person("Alice", 30);
original.setEmail("alice@example.com");
// Create independent copy
Person copy = new Person(original);
// Modify copy
copy.setEmail("alice.copy@example.com");
// Original unchanged
original.display(); // Alice (30) - alice@example.com
copy.display(); // Alice (30) - alice.copy@example.com
}
}
Copy constructors are essential when objects contain mutable fields (lists, arrays, other objects). Without proper copying, modifications to the copy affect the original. This is called a "shallow copy" issue for complex objects—deep copying requires recursive copying of nested objects.
Immutable Objects with Constructor Initialization
Immutable objects cannot be changed after creation. Constructors are the only way to set their state.
public class ImmutablePoint {
private final int x;
private final int y;
// Immutable: only way to set values
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
// Getters - no setters!
public int getX() {
return x;
}
public int getY() {
return y;
}
// Create new object instead of modifying
public ImmutablePoint translate(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy);
}
@Override
public String toString() {
return "(" + x + "," + y + ")";
}
}public class ImmutableDemo {
public static void main(String[] args) {
ImmutablePoint p1 = new ImmutablePoint(10, 20);
System.out.println(p1); // (10,20)
// Create new object instead of modifying
ImmutablePoint p2 = p1.translate(5, -3);
System.out.println(p1); // (10,20) - unchanged
System.out.println(p2); // (15,17)
}
}
Immutable objects are thread-safe and predictable. Use final keyword for fields. Provide no setters; instead, return new objects for modifications. Java's String class follows this pattern.
Initialization Blocks
Initialization blocks run before constructors. Instance blocks run once per object; static blocks run once when class loads. Use them for complex initialization logic.
public class InitializationExample {
private static int classCounter;
private int instanceCounter;
private java.util.List items;
// Static initializer - runs once when class loads
static {
System.out.println("Static initializer running");
classCounter = 0;
}
// Instance initializer - runs before every constructor
{
System.out.println("Instance initializer running");
instanceCounter = 0;
items = new java.util.ArrayList<>();
classCounter++;
}
public InitializationExample() {
System.out.println("Constructor running");
}
public InitializationExample(String item) {
items.add(item); // Can use initialized field
System.out.println("Constructor with parameter running");
}
}public class InitializationDemo {
public static void main(String[] args) {
System.out.println("Creating object 1");
InitializationExample ex1 = new InitializationExample();
System.out.println("\nCreating object 2");
InitializationExample ex2 = new InitializationExample("item");
}
}
Execution order: static blocks (once), field initializers, instance blocks, constructor. This is rarely needed—keep constructors simple and readable instead.
Defensive Copying
When fields contain mutable objects, defensive copying prevents external modification of internal state.
import java.util.ArrayList;
import java.util.Date;public class DefensiveCopyExample {
private String name;
private ArrayList hobbies;
private Date createdDate;
// Constructor with defensive copy
public DefensiveCopyExample(String name, ArrayList hobbies) {
this.name = name;
// Copy list instead of storing reference
this.hobbies = new ArrayList<>(hobbies);
this.createdDate = new Date();
}
public ArrayList getHobbies() {
// Return copy instead of original
return new ArrayList<>(hobbies);
}
public void addHobby(String hobby) {
hobbies.add(hobby);
}
public void display() {
System.out.println(name + ": " + hobbies);
}
}
public class DefensiveCopyDemo {
public static void main(String[] args) {
ArrayList hobbies = new ArrayList<>();
hobbies.add("reading");
DefensiveCopyExample person = new DefensiveCopyExample("Alice", hobbies);
// Modify original list
hobbies.add("gaming"); // Doesn't affect person's hobbies
// Modify returned list
ArrayList returned = person.getHobbies();
returned.add("swimming"); // Doesn't affect person's hobbies
person.display(); // Only shows "reading"
}
}
Without defensive copying, external code could modify your object's internal state. Always copy mutable objects in constructors and return copies from getters when exposing mutable fields.
Validation in Constructors
Constructor parameters should be validated to prevent invalid objects.
public class Employee {
private String employeeId;
private double salary;
private int yearsExperience;
public Employee(String employeeId, double salary, int yearsExperience) {
// Validate parameters
if (employeeId == null || employeeId.trim().isEmpty()) {
throw new IllegalArgumentException("Employee ID cannot be empty");
}
if (salary < 0) {
throw new IllegalArgumentException("Salary cannot be negative");
}
if (yearsExperience < 0) {
throw new IllegalArgumentException("Years cannot be negative");
}
this.employeeId = employeeId.trim();
this.salary = salary;
this.yearsExperience = yearsExperience;
}
public void display() {
System.out.println("ID: " + employeeId + ", Salary: $" + salary +
", Experience: " + yearsExperience + " years");
}
}public class ValidationDemo {
public static void main(String[] args) {
try {
Employee emp1 = new Employee("E001", 50000, 5);
emp1.display();
// This throws exception
Employee emp2 = new Employee("", 50000, 5);
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Validate early in constructors. Throw exceptions for invalid inputs. This prevents invalid objects from existing—fail fast rather than silently creating broken objects.
Constructor Anti-Patterns and Pitfalls
Avoiding Common Mistakes
public class ConstructorPitfalls {
static class BadExample {
private String name;
private int age;
// Anti-pattern: constructor does too much
public BadExample(String name, int age) {
this.name = name;
this.age = age;
loadFromDatabase(); // Side effects!
sendNotification();
initializeConnections();
}
private void loadFromDatabase() { }
private void sendNotification() { }
private void initializeConnections() { }
}
static class GoodExample {
private String name;
private int age;
// Constructor only initializes fields
public GoodExample(String name, int age) {
this.name = name;
this.age = age;
}
// Separate methods for other operations
public void initializeResources() { }
public void startProcessing() { }
}
}
Advanced Initialization Patterns
Fluent Builder
public class FluentBuilder {
static class Configuration {
private String host;
private int port;
private String database;
private boolean ssl;
private int timeout;
public Configuration(String host) {
this.host = host;
this.port = 5432; // defaults
this.ssl = false;
this.timeout = 30;
}
public Configuration withPort(int port) {
this.port = port;
return this;
}
public Configuration withDatabase(String database) {
this.database = database;
return this;
}
public Configuration withSSL(boolean ssl) {
this.ssl = ssl;
return this;
}
public Configuration withTimeout(int timeout) {
this.timeout = timeout;
return this;
}
public void connect() {
System.out.println("Connecting to " + host + ":" + port);
}
}
public static void main(String[] args) {
new Configuration("localhost")
.withPort(3306)
.withDatabase("mydb")
.withSSL(true)
.withTimeout(60)
.connect();
}
}
Factory Methods as Alternatives
public class DataSource {
private String location;
private String type;
private DataSource(String location, String type) {
this.location = location;
this.type = type;
}
// Factory methods provide clear intent
public static DataSource createDatabase(String host, int port) {
return new DataSource(host + ":" + port, "database");
}
public static DataSource createFile(String path) {
return new DataSource(path, "file");
}
public static DataSource createMemory() {
return new DataSource("memory", "cache");
}
public static void main(String[] args) {
DataSource db = DataSource.createDatabase("localhost", 5432);
DataSource file = DataSource.createFile("/data/export.csv");
DataSource cache = DataSource.createMemory();
}
}
Initialization Ordering
public class InitializationOrder {
static class Demo {
static int staticField = initStatic();
int instanceField = initInstance();
static {
System.out.println("Static initializer block");
}
{
System.out.println("Instance initializer block");
}
public Demo() {
System.out.println("Constructor");
}
static int initStatic() {
System.out.println("Initializing static field");
return 1;
}
int initInstance() {
System.out.println("Initializing instance field");
return 2;
}
}
public static void main(String[] args) {
System.out.println("Creating first object:");
new Demo();
System.out.println("\nCreating second object:");
new Demo();
}
}
// Output:
// Initializing static field
// Static initializer block
// Creating first object:
// Initializing instance field
// Instance initializer block
// Constructor
Singleton Pattern
public class SingletonExample {
// Eager singleton
static class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
// Lazy singleton with thread safety
static class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
// Double-checked locking singleton (more efficient)
static class OptimizedSingleton {
private static volatile OptimizedSingleton instance;
private OptimizedSingleton() {}
public static OptimizedSingleton getInstance() {
if (instance == null) {
synchronized (OptimizedSingleton.class) {
if (instance == null) {
instance = new OptimizedSingleton();
}
}
}
return instance;
}
}
}
Key Takeaways
1. Constructors initialize objects in a consistent, controlled state
2. Overload constructors for different initialization needs
3. The this() call must be the first statement in a constructor
4. Constructor chaining reduces code duplication
5. Copy constructors create independent copies of objects
6. Immutable objects use constructors for all state initialization
7. Initialization blocks run before constructors (rarely needed)
8. Defensive copying protects mutable internal state
9. Validate constructor parameters—fail fast for invalid inputs
10. Consider factory methods for complex object creation
11. Avoid side effects in constructors—keep them focused on initialization
12. Singleton pattern can be useful for shared resources, but use carefully
Quiz
Question 1: What's printed when calling new Rectangle()?
public class Rectangle {
public Rectangle() {
System.out.println("No-arg");
}
public Rectangle(double side) {
System.out.println("One-arg");
}
}
- A) No-arg ✓
- B) One-arg
- C) Both
- D) Compilation error
Question 2: What does this() do?
- A) Refers to the current object
- B) Calls another constructor in the same class ✓
- C) Creates a new object
- D) Refers to the parent class
Question 3: Which statement about copy constructors is true?
- A) They create a backup of the class
- B) They create an independent copy of an object ✓
- C) They're required for all classes
- D) They copy memory addresses
Question 4: What's the correct order of execution?
- A) Constructor → Instance block → Field initialization
- B) Static block → Instance block → Constructor
- C) Field initialization → Instance block → Constructor ✓
- D) Constructor → Field initialization → Instance block
Question 5: What does defensive copying prevent?
- A) Memory leaks
- B) External modification of internal mutable objects ✓
- C) Constructor calls
- D) Field access
Question 6: When should you throw an exception in a constructor?
- A) Always
- B) When parameters are invalid ✓
- C) Only for numeric values
- D) Never