☕ Java
Parameterized Constructor
A parameterized constructor is a constructor that accepts one or more arguments, allowing the caller to supply initial values for the object's fields at the moment of creation. Parameterized constructors enforce that objects are always created in a complete, valid state — mandatory data is provided upfront rather than being set piecemeal through setter calls after construction. They are the primary mechanism for implementing immutable objects and for making required fields genuinely required.
Syntax and Basic Usage
A parameterized constructor is written exactly like a no-arg constructor but with a parameter list between the parentheses. Each parameter provides a value that the constructor uses to initialise a field. The this keyword is used to distinguish between the field name and the parameter name when they are the same — this.name refers to the instance field, while name alone refers to the parameter.
Using this to qualify field names in constructors is the universal Java convention. It is always explicit and always correct — there is never ambiguity about whether you are setting the field or assigning to the parameter. Omitting this when parameter names shadow field names is a classic bug: writing name = name in a constructor does nothing because both sides refer to the parameter, leaving the field at its default value.
The caller provides values for each parameter in the exact order and type declared. The compiler enforces this at compile time — passing the wrong number of arguments, the wrong types, or arguments in the wrong order is a compile error. This compile-time safety is one of the great advantages of parameterized constructors over setters: you cannot forget to initialise a required field, and the compiler tells you immediately if you try.
Java
// ── Basic parameterized constructor: ─────────────────────────────────
public class Student {
private String name;
private int age;
private String major;
// Parameterized constructor — all required fields set at creation:
public Student(String name, int age, String major) {
this.name = name; // this.name = field, name = parameter
this.age = age;
this.major = major;
}
public String getName() { return name; }
public int getAge() { return age; }
public String getMajor() { return major; }
@Override
public String toString() {
return "Student{name='" + name + "', age=" + age +
", major='" + major + "'}";
}
}
// ── Creating objects — values provided at instantiation: ──────────────
Student alice = new Student("Alice", 20, "Computer Science");
Student bob = new Student("Bob", 22, "Mathematics");
System.out.println(alice); // Student{name='Alice', age=20, major='Computer Science'}
System.out.println(bob); // Student{name='Bob', age=22, major='Mathematics'}
// ── The this keyword is critical — without it, fields stay at default: ─
public class BrokenStudent {
private String name;
private int age;
public BrokenStudent(String name, int age) {
name = name; // BUG: assigns parameter to itself — field stays null
age = age; // BUG: assigns parameter to itself — field stays 0
}
}
BrokenStudent broken = new BrokenStudent("Alice", 20);
System.out.println(broken.getName()); // null — field was never set!
// ── Correct version: ──────────────────────────────────────────────────
public class FixedStudent {
private String name;
private int age;
public FixedStudent(String name, int age) {
this.name = name; // this.name = field, name = parameter
this.age = age;
}
}Validation Inside Constructors
One of the most important uses of parameterized constructors is validating the arguments before assigning them to fields. If invalid data is rejected in the constructor, the object can never be created in an invalid state. This is a fundamental guarantee: if a reference to an object exists, the object was valid at the moment of creation.
Validation in constructors should throw IllegalArgumentException for invalid input values (wrong type semantics, out-of-range values) and NullPointerException (or use Objects.requireNonNull) for null values that are not allowed. The exception message should clearly state what was wrong and what was expected — a stack trace alone is not enough information for the caller to understand how to fix the problem.
This approach is called defensive programming or fail-fast programming. Rather than allowing an invalid object to be created and failing later when a method is called on it — possibly far from where the invalid value was introduced — the constructor fails immediately with a precise error. The closer a failure is to its cause in code and time, the easier it is to diagnose and fix.
Java
// ── Constructor validation — fail fast with clear messages: ──────────
public class BankAccount {
private final String accountNumber;
private final String owner;
private double balance;
public BankAccount(String accountNumber, String owner,
double initialBalance) {
// Validate all arguments before touching any field:
if (accountNumber == null || accountNumber.isBlank())
throw new IllegalArgumentException(
"Account number cannot be null or blank");
if (!accountNumber.matches("[A-Z]{2}\d{8}"))
throw new IllegalArgumentException(
"Account number must be 2 letters followed by 8 digits, got: "
+ accountNumber);
if (owner == null || owner.isBlank())
throw new IllegalArgumentException(
"Owner name cannot be null or blank");
if (initialBalance < 0)
throw new IllegalArgumentException(
"Initial balance cannot be negative, got: " + initialBalance);
// All valid — assign to fields:
this.accountNumber = accountNumber;
this.owner = owner.trim();
this.balance = initialBalance;
}
public String getAccountNumber() { return accountNumber; }
public String getOwner() { return owner; }
public double getBalance() { return balance; }
}
// ── Valid creation: ───────────────────────────────────────────────────
BankAccount acc = new BankAccount("GB12345678", "Alice", 1000.0);
// ── Invalid creation — exception thrown immediately: ──────────────────
try {
BankAccount bad = new BankAccount(null, "Bob", 500.0);
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
// Account number cannot be null or blank
}
try {
BankAccount bad = new BankAccount("GB12345678", "Carol", -100.0);
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
// Initial balance cannot be negative, got: -100.0
}
// ── Objects.requireNonNull — concise null check: ──────────────────────
import java.util.Objects;
public class Product {
private final String name;
private final double price;
public Product(String name, double price) {
// Throws NullPointerException with descriptive message:
this.name = Objects.requireNonNull(name, "name cannot be null");
if (price < 0) throw new IllegalArgumentException(
"price cannot be negative: " + price);
this.price = price;
}
}Parameterized Constructors and Immutability
Parameterized constructors are the foundation of immutable objects. An immutable object is one whose state cannot change after construction. Java's String class is the most famous example — once created, a String's character sequence never changes. Immutable objects are inherently thread-safe, can be freely shared and cached, and are much easier to reason about because their state never changes unexpectedly.
To create an immutable class, declare all fields as private and final. Provide a parameterized constructor that sets all fields. Provide no setters. If any field holds a reference to a mutable object (such as a List or Date), the constructor should make a defensive copy rather than storing the reference directly — otherwise a caller who still holds a reference to the original mutable object can change it and indirectly mutate the supposedly immutable object.
The Java standard library is full of immutable classes: String, Integer, Double, BigDecimal, LocalDate, LocalDateTime, and Duration are all immutable. The record keyword introduced in Java 16 provides a shorthand for creating immutable data-holding classes with minimal boilerplate.
Java
// ── Immutable class — all fields final, no setters: ──────────────────
public final class Money { // final class — cannot be subclassed
private final double amount; // final — set once, never changed
private final String currency;
public Money(double amount, String currency) {
if (amount < 0)
throw new IllegalArgumentException(
"Amount cannot be negative: " + amount);
if (currency == null || currency.length() != 3)
throw new IllegalArgumentException(
"Currency must be a 3-letter ISO code: " + currency);
this.amount = amount;
this.currency = currency.toUpperCase();
}
public double getAmount() { return amount; }
public String getCurrency() { return currency; }
// Operations return NEW objects — do not modify this object:
public Money add(Money other) {
if (!this.currency.equals(other.currency))
throw new IllegalArgumentException(
"Cannot add different currencies: "
+ this.currency + " and " + other.currency);
return new Money(this.amount + other.amount, this.currency);
}
public Money multiply(double factor) {
return new Money(this.amount * factor, this.currency);
}
@Override
public String toString() {
return String.format("%.2f %s", amount, currency);
}
}
Money price = new Money(10.00, "GBP");
Money tax = new Money(2.00, "GBP");
Money total = price.add(tax); // new Money — price unchanged
Money discount = total.multiply(0.9); // new Money — total unchanged
System.out.println(price); // 10.00 GBP — unchanged
System.out.println(total); // 12.00 GBP
System.out.println(discount); // 10.80 GBP
// ── Defensive copy for mutable field references: ──────────────────────
public final class Schedule {
private final String name;
private final List<String> tasks; // mutable — needs defensive copy
public Schedule(String name, List<String> tasks) {
this.name = Objects.requireNonNull(name, "name is required");
// Defensive copy — don't store the caller's reference:
this.tasks = new ArrayList<>(
Objects.requireNonNull(tasks, "tasks is required"));
}
public String getName() { return name; }
// Return defensive copy — don't expose internal mutable list:
public List<String> getTasks() {
return new ArrayList<>(tasks); // caller cannot mutate our list
}
}
// ── Java record — concise immutable class (Java 16+): ─────────────────
public record Point(double x, double y) {
// Compact canonical constructor for validation:
public Point {
if (Double.isNaN(x) || Double.isNaN(y))
throw new IllegalArgumentException("Coordinates cannot be NaN");
}
// Generates: private final double x, y;
// public Point(double x, double y) { ... }
// public double x() { return x; }
// public double y() { return y; }
// equals(), hashCode(), toString() — all for free
}
Point p = new Point(3.0, 4.0);
System.out.println(p); // Point[x=3.0, y=4.0]
System.out.println(p.x()); // 3.0The this Keyword in Constructors
Inside a constructor, this is a reference to the object currently being constructed. It serves two distinct purposes: qualifying field names to distinguish them from parameter names (this.name), and calling another constructor in the same class (this(...)).
Calling another constructor with this(...) is called constructor chaining. It must be the very first statement in the constructor body — you cannot do anything else before the this(...) call. Constructor chaining is the correct way to share initialisation logic across multiple constructors. Without it, you would either duplicate the initialisation code in every constructor (brittle — changes must be made in multiple places) or extract it into a private init method (works, but bypasses the guarantee that final fields are set exactly once).
Constructor chaining with this(...) always targets another constructor in the same class. Calling a superclass constructor uses super(...) instead. The two are mutually exclusive — a constructor can start with this(...) or super(...) but not both.
Java
// ── this(...) — constructor chaining within the same class: ──────────
public class Rectangle {
private final double width;
private final double height;
private final String colour;
// Most specific constructor — all logic lives here:
public Rectangle(double width, double height, String colour) {
if (width <= 0) throw new IllegalArgumentException(
"Width must be positive: " + width);
if (height <= 0) throw new IllegalArgumentException(
"Height must be positive: " + height);
if (colour == null || colour.isBlank()) throw new IllegalArgumentException(
"Colour cannot be blank");
this.width = width;
this.height = height;
this.colour = colour;
}
// Default colour — delegates to the full constructor:
public Rectangle(double width, double height) {
this(width, height, "Black"); // must be FIRST statement
// Cannot do anything before this(...) call.
}
// Square constructor — delegates to the two-parameter constructor:
public Rectangle(double side) {
this(side, side); // chains to Rectangle(double, double)
}
// No-arg constructor — unit square:
public Rectangle() {
this(1.0); // chains to Rectangle(double)
}
public double area() { return width * height; }
public double perimeter() { return 2 * (width + height); }
@Override
public String toString() {
return String.format("Rectangle{%.1f x %.1f, %s}",
width, height, colour);
}
}
// ── Chain of constructor calls: ──────────────────────────────────────
// new Rectangle() → this(1.0)
// → this(1.0, 1.0)
// → this(1.0, 1.0, "Black") ← runs the full init
Rectangle r1 = new Rectangle();
Rectangle r2 = new Rectangle(5.0);
Rectangle r3 = new Rectangle(4.0, 6.0);
Rectangle r4 = new Rectangle(3.0, 5.0, "Red");
System.out.println(r1); // Rectangle{1.0 x 1.0, Black}
System.out.println(r2); // Rectangle{5.0 x 5.0, Black}
System.out.println(r3); // Rectangle{4.0 x 6.0, Black}
System.out.println(r4); // Rectangle{3.0 x 5.0, Red}
// ── this(...) MUST be the first statement: ────────────────────────────
public Rectangle(double side) {
// System.out.println("Creating square"); // COMPILE ERROR if before this()
this(side, side); // this() must come first
// System.out.println("Square created"); // ✓ code after this() is fine
}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.