Setting every field by hand right after new TestUser() gets old fast — and it's easy to forget one. A constructor is a special method that runs automatically when you create an object with new. It's the place to set initial values, validate inputs, and refuse to construct a half-built object. Once you've written a constructor, callers go from this:
TestUser admin = new TestUser();
admin.name = "Alice";
admin.email = "alice@test.com";
admin.role = "admin";…to this:
TestUser admin = new TestUser("Alice", "alice@test.com", "admin");One line. Every field guaranteed populated. That's the win.
Defining a constructor
A constructor looks like a method with two differences: its name is the same as the class, and there is no return type (not even void).
public class TestUser {
String name;
String email;
String role;
public TestUser(String name, String email, String role) {
this.name = name;
this.email = email;
this.role = role;
}
}Calling it via new:
public class UserDemo {
public static void main(String[] args) {
TestUser admin = new TestUser("Alice", "alice@test.com", "admin");
System.out.println(admin.name + " (" + admin.role + ")");
}
}Output:
Alice (admin)
The parentheses on new TestUser(...) are calling that constructor. The arguments line up with the constructor's parameters in order, exactly like a normal method call.
The this keyword
Inside the constructor, the parameter name and the field name have the same name. The parameter wins inside the method body — that's called shadowing. To refer to the field, you need this:
public TestUser(String name, String email, String role) {
this.name = name; // this.name = the FIELD; name = the PARAMETER
this.email = email;
this.role = role;
}this is a reference to the current object — the instance being constructed. this.name says "the name field on the object I'm working on right now."
You're not required to use the same names for the field and the parameter — public TestUser(String n, String e, String r) would compile and avoid the shadowing — but the convention is to reuse the field names because the constructor signature reads as documentation: this constructor takes a name, an email, and a role. Your IntelliJ "Generate constructor" command writes them this way too.
this is also useful outside constructors — any time a method needs to refer to the current object explicitly, like return this; in a builder pattern (chapter 5+).
The default constructor — and how you lose it
If your class declares no constructors, Java silently provides a no-argument one:
public class TestUser {
String name;
// ... no constructor written ...
}
TestUser u = new TestUser(); // works — Java provided one for freeThe moment you write any constructor, the freebie disappears:
public class TestUser {
String name;
public TestUser(String name) { this.name = name; }
}
TestUser u = new TestUser(); // ❌ compile error — no zero-arg constructor exists
TestUser u = new TestUser("Alice"); // ✅If you need both shapes, write both. Or use constructor overloading — the topic of the next section.
Constructor overloading and this(...)
You can have multiple constructors with different parameter lists, exactly like method overloading from chapter 3.2:
public class TestUser {
String name;
String email;
String role;
public TestUser(String name, String email, String role) {
this.name = name;
this.email = email;
this.role = role;
}
public TestUser(String name, String email) {
this(name, email, "tester"); // delegate to the 3-arg constructor
}
public TestUser() {
this("Default", "default@test.com", "tester");
}
}this(...) (with parentheses) inside a constructor calls another constructor on the same class. It must be the very first statement; you can't run code before it. Each shorter overload supplies defaults and delegates to the canonical 3-argument version, so the real construction logic lives in one place.
This is the standard Java idiom for "default values" — Java has no syntactic defaults the way Python does. Test framework classes use this pattern constantly: new WebDriverWait(driver, Duration.ofSeconds(10)) and new WebDriverWait(driver) both exist, the shorter one delegates with a default timeout.
A real QA example — TestConfig
A configuration class with three constructors representing common setups:
public class TestConfig {
String env;
String baseUrl;
int timeoutSeconds;
boolean dryRun;
public TestConfig(String env, String baseUrl, int timeoutSeconds, boolean dryRun) {
if (env == null || env.isEmpty()) {
throw new IllegalArgumentException("env must be non-empty");
}
if (timeoutSeconds <= 0) {
throw new IllegalArgumentException("timeoutSeconds must be positive");
}
this.env = env;
this.baseUrl = baseUrl;
this.timeoutSeconds = timeoutSeconds;
this.dryRun = dryRun;
}
public TestConfig() {
this("dev", "http://localhost:3000", 10, false);
}
public TestConfig(String env) {
this(env,
env.equals("production") ? "https://myapp.com" : "https://" + env + ".myapp.com",
10,
false);
}
public static void main(String[] args) {
TestConfig dev = new TestConfig();
TestConfig staging = new TestConfig("staging");
TestConfig custom = new TestConfig("qa", "https://qa.myapp.com", 30, true);
System.out.println(dev.env + " -> " + dev.baseUrl);
System.out.println(staging.env + " -> " + staging.baseUrl);
System.out.println(custom.env + " -> " + custom.baseUrl + " (dry-run=" + custom.dryRun + ")");
}
}Output:
dev -> http://localhost:3000
staging -> https://staging.myapp.com
qa -> https://qa.myapp.com (dry-run=true)
Three constructors, one canonical implementation, validation in one place. The IllegalArgumentException makes it impossible to construct a TestConfig with a missing env or a non-positive timeout — invalid configs fail loudly at construction, not silently five test cases later.
Constructor execution, step by step
Step 1 of 6
new TestUser(...)
The new keyword asks the JVM for memory to hold one TestUser. All fields are initialised to their type defaults (null/0/false).
The whole new TestUser(...) expression is one atomic operation from the caller's point of view, but six things happened internally. Knowing the order helps when validation fails: nothing is half-built — either the constructor finishes and you get a usable object, or it throws and you get nothing.
⚠️ Common mistakes
- Forgetting
this.— and silently doing nothing.name = name;inside a constructor sets the parameter to itself. The field is never touched and staysnull. The compiler doesn't warn about this; tests just mysteriously fail. Always usethis.field = parameter;. - Putting
this(...)somewhere other than the first line.this("default")to delegate to another constructor must be the very first statement. Putting it later (or after asuper(...)call we'll meet in lesson 4) is a compile error. The fix is to keep delegation at the top and any extra logic after it. - Writing a constructor with a return type.
public void TestUser(...)is not a constructor — it's a regular method that happens to have the class's name. Java ignores it duringnewand silently uses the default no-arg constructor. The fix is to dropvoid. The compiler doesn't flag this — it's a silent bug.
🎯 Practice task
Replace your hand-set fields with constructors. 25-30 minutes.
- Open the
TestCaseclass from lesson 1's practice task (or create a fresh one). - Add a 4-arg constructor
public TestCase(String name, String priority, long durationMs, boolean passed)that sets each field withthis.field = parameter;. - Add a 2-arg overload
public TestCase(String name, String priority)that delegates withthis(name, priority, 0L, false);— a "freshly added, not yet run" case. - Add a no-arg constructor that delegates with
this("Untitled", "P3");. (You'll see why this is rare in practice, but the exercise is in writing it.) - Add a guard in the canonical constructor:
if (priority == null || priority.isEmpty()) throw new IllegalArgumentException("priority is required");. Try constructing a case with a null priority and read the exception output. - Update your
CaseRunnerto call each of the three constructor shapes. Confirm all three work and the validation throws as expected. - Stretch: in IntelliJ, delete your hand-written constructor and use Right-click → Generate → Constructor (or Alt+Insert / Cmd+N) to regenerate it. The IDE writes the same
this.field = fieldpattern. Do this often enough and it becomes muscle memory; in real codebases nobody types constructors by hand.
You can now build objects with one line, validate inputs at construction, and provide multiple shapes of "default" via overloads. Lesson 3 introduces private and the encapsulation pattern that turns those constructors into a real safety net.