Suppose you build a BankAccount without a constructor. Every new account starts with balance = 0, which is fine, but owner starts as null. The first caller who tries to use that account gets a NullPointerException because the object was born incomplete. That is the problem constructors solve: they guarantee every object begins life in a valid, known state. Without a constructor, you are asking every caller to remember to initialise every field, and in a large program someone will forget. A constructor is not optional decoration — it is the contract between the class and the rest of the program that says, if you want one of these, you must provide what it needs to be whole. That contract is enforced by the compiler: miss a required argument and the build fails before the program ever runs.
flowchart LR A["new BankAccount(#quot;Mara#quot;, 100)"] --> B["fields set to defaults<br/>owner=null, balance=0"] B --> C["instance initializers run"] C --> D["constructor runs<br/>owner=#quot;Mara#quot;, balance=100"] D --> E["object ready"] style D fill:#c2410c,color:#fff
The anatomy of a constructor
A constructor looks like a method but has two distinguishing rules: its name is exactly the class name, and it has no return type — not even void. You cannot accidentally type int Person() or void Person(); the compiler will reject both. When you write new BankAccount("Mara", 100), Java allocates memory, sets fields to default values (0 for numbers, false for booleans, null for objects), and then runs the constructor you wrote. If you do not write any constructor, Java quietly inserts a default constructor that takes no arguments and does nothing. That is why new BankAccount() sometimes compiles and sometimes does not — the moment you add a constructor with parameters, the default one vanishes. We will come back to that trap, because it bites every Java developer at least once.
public class Main {
static class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
void greet() {
System.out.println("Hi, I am " + name);
}
}
public static void main(String[] args) {
Person p = new Person("Jules", 30);
p.greet();
}
}
Overloading and chaining
A class can have many constructors as long as their parameter lists differ — this is constructor overloading. The no-argument constructor might supply sensible defaults, while the full-argument constructor accepts every value explicitly. To avoid repeating logic, one constructor can call another with this(...). The call must be the very first line of the constructor, and it routes to the constructor that matches the argument types. You cannot call this(...) after any other statement because the object must be fully initialised before you do anything else. This is how you centralise validation: put the checks in one constructor, and have every other constructor funnel through it. If the canonical constructor rejects a negative age, every shortcut constructor rejects it too.
flowchart TD A["Person()"] -->|"this(#quot;Unknown#quot;, 0)"| B["Person(String, int)"] C["Person(String name)"] -->|"this(name, 0)"| B B --> D["validation runs<br/>fields set"] style B fill:#c2410c,color:#fff
public class Main {
static class Counter {
int value;
String label;
Counter() {
this(0, "default");
}
Counter(int value) {
this(value, "count");
}
Counter(int value, String label) {
this.value = value;
this.label = label;
}
void print() {
System.out.println(label + ": " + value);
}
}
public static void main(String[] args) {
Counter a = new Counter();
Counter b = new Counter(5);
a.print();
b.print();
}
}
Initialization order
When an object is created, Java follows a strict sequence. First, fields are set to default values (0, false, null). Second, any instance initializer blocks — anonymous braces at class level — run in the order they appear. Third, the constructor runs. This order is reliable, so you can reason about it, but it also means a field assignment written inline (int value = 10;) is effectively moved into the instance-initializer stage. If an initializer block and the constructor both touch the same field, the constructor wins because it runs last. You can use this deliberately — set a safe default inline, then override it in the constructor with a caller-supplied value — but mixing too many initialisation styles in one class makes the code hard to follow.
flowchart TD A["1. Fields to defaults"] --> B["2. Instance initializers"] B --> C["3. Constructor body"] C --> D["Object ready"] style C fill:#c2410c,color:#fff
public class Main {
static class Demo {
int a = 1;
int b;
{
b = 2;
}
Demo() {
a = 3;
}
}
public static void main(String[] args) {
Demo d = new Demo();
System.out.println("a=" + d.a + ", b=" + d.b);
}
}
What does this program print? Read it carefully and type the exact output.
public class Main {
static class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public static void main(String[] args) {
Point p = new Point(3, 4);
System.out.println(p.x + p.y);
}
}
The constructor stores 3 in x and 4 in y.
p.x + p.y is 3 + 4.
Initialization order check. Predict the exact two-line output.
public class Main {
static class Demo {
int a = 1;
int b;
{
b = 2;
}
Demo() {
a = 3;
}
}
public static void main(String[] args) {
Demo d = new Demo();
System.out.println(d.a);
System.out.println(d.b);
}
}
Fields get defaults first, then a = 1 from the inline assignment.
The instance initializer sets b = 2.
The constructor runs last, overwriting a to 3.
You define class Person { Person(String name) {} } and then write new Person(). What happens?
Once you define any constructor, Java stops providing the default no-arg constructor. You must write Person() yourself if you want it.
Constructor chaining. Predict the exact one-line output.
public class Main {
static class Message {
String text;
Message() {
this("Hello");
}
Message(String text) {
this.text = text;
}
void show() {
System.out.println(text);
}
}
public static void main(String[] args) {
Message m = new Message();
m.show();
}
}
The no-arg constructor calls this("Hello"), which routes to the String constructor.
The field is set before show() reads it.
You need a Rectangle class that can be created as new Rectangle(), new Rectangle(5), or new Rectangle(5, 10). Which constructor design satisfies this?
Option A fails because the default no-arg disappears once a parameterized constructor exists. Option C lacks the two-arg form. Option D uses varargs, which compiles but does not match the required signatures exactly. Option B provides all three and uses this(...) to centralise logic.
Recap
- A constructor has the same name as the class and no return type. It runs automatically when
newcreates an object. - If you write no constructor, Java supplies a default no-arg constructor. The moment you write any constructor, that default disappears.
- You can overload constructors and chain them with
this(...), but the call must be the first statement in the body. - Java initializes objects in a fixed order: fields to defaults, then inline field assignments and instance initializer blocks, then the constructor.
- Because the constructor runs last, it can override values set earlier.
Next you will learn how to control what outside code can see and change, using access modifiers and encapsulation.