JAVA / CLASSES, CONSTRUCTORS AND ENCAPSULATION
Constructors and the default constructor trap
Predict when Java generates a no-arg constructor, why writing your own removes it, and how to restore it for callers, subclasses and reflective tools.
What you will learn
- Tell when javac generates a no-arg constructor and when it silently stops
- Restore new X() by declaring the no-arg constructor yourself
- Explain why fields read 0, null or false before a constructor body runs
- Spot the void Foo() typo that leaves a class with a generated empty constructor
Understanding Constructors and the default constructor trap
A constructor looks like a method that carries the class name and has no return type, but it is not a method: you cannot invoke it directly, only new can, and it produces no value. By the time its first statement executes, memory for the object already exists and every field already holds its zero value, meaning 0, 0.0, false or null. The constructor's job is the step right after allocation: moving a zeroed object to one that satisfies the class's invariants. Initializers written next to field declarations are compiled into the constructor and run in source order before its body.
If a class declares no constructors at all, javac inserts one: no parameters, empty body, same access level as the class. That is the default constructor, and it is generated on a count rather than on a name, so a single Foo(int) is enough to stop the generation. Nothing overrides or hides it; it simply never comes into existence, which is why adding your first parameterized constructor turns every existing new Foo() into a compile error in files you never opened.
The no-arg constructor is therefore part of the class's public surface, and its existence should be a decision rather than an accident. Bean-style tools, JPA entity managers and JSON deserializers create objects by calling the no-arg constructor reflectively, and those break at runtime with a complaint about a missing default constructor. Subclasses depend on it too, because any constructor body that does not begin with an explicit super(...) or this(...) begins with an implicit super(). When you add a parameterized constructor, choose consciously between writing Foo() {} back and letting it disappear.
class Counter {
int count;
String label;
boolean active;
// no constructor declared, so javac generates Counter()
}
class Temperature {
double celsius;
Temperature(double celsius) {
this.celsius = celsius;
}
// new Temperature() no longer compiles: Temperature() was never generated
}
class Pressure {
double kpa;
Pressure() { // written back by hand
kpa = 101.325;
}
Pressure(double kpa) {
this.kpa = kpa;
}
}
public class Main {
public static void main(String[] args) {
Counter c = new Counter();
System.out.println(c.count + " " + c.label + " " + c.active);
Temperature t = new Temperature(21.5);
System.out.println(t.celsius);
System.out.println(new Pressure().kpa);
System.out.println(new Pressure(95.0).kpa);
}
}The compiler supplies a no-arg constructor only to a class that declares no constructor at all, so your first constructor silently deletes new X() from the class's API.
Worked examples
Proving the generated constructor exists
Reflection shows the constructor javac wrote for a class that declares none, and shows it missing once one is declared.
import java.lang.reflect.Constructor;
class Plain {
int x;
}
class Configured {
int x;
Configured(int x) {
this.x = x;
}
}
public class Main {
public static void main(String[] args) {
report(Plain.class);
report(Configured.class);
}
static void report(Class<?> type) {
Constructor<?>[] ctors = type.getDeclaredConstructors();
System.out.println(type.getSimpleName() + ": " + ctors.length
+ " constructor, takes " + ctors[0].getParameterCount() + " parameter(s)");
}
}Example explained
Line 1Plain declares nothing, yet reflection finds one constructor: javac wrote Plain() into the class file.
Line 2Configured declares Configured(int) and reflection finds only that one, so the no-arg version was never generated.
Line 3getDeclaredConstructors() reads the compiled class, so it reports what the compiler produced rather than what you typed.
A method that impersonates a constructor
Adding a return type turns the would-be constructor into an ordinary method, so the generated no-arg constructor is still used.
class Row {
int count;
void Row() { // return type present, so this is a method
count = 10;
}
}
public class Main {
public static void main(String[] args) {
Row r = new Row();
System.out.println(r.count);
r.Row();
System.out.println(r.count);
}
}Example explained
Line 1void Row() declares a return type, and a constructor never does, so this member is a plain method.
Line 2Row therefore declares zero constructors and javac supplies an empty Row(), which is what new Row() runs.
Line 3count stays at its default 0 until r.Row() is called explicitly, which is why this typo hides for a long time.
The trap reaching a subclass
Once the parent has only a parameterized constructor, every subclass constructor must call super(...) itself.
class Shape {
String name;
Shape(String name) {
this.name = name;
}
}
class Circle extends Shape {
double radius;
Circle(double radius) {
super("circle");
this.radius = radius;
}
}
public class Main {
public static void main(String[] args) {
Circle c = new Circle(2.0);
System.out.println(c.name + " " + c.radius);
}
}Example explained
Line 1Shape declares Shape(String), so the generated Shape() does not exist for anyone to call.
Line 2A constructor body that does not start with super(...) or this(...) gets an implicit super(), so deleting super("circle") here breaks compilation of Circle, not of Shape.
Line 3The parent constructor runs first, so name is assigned before this.radius = radius.
Important notes
The generated constructor copies the class's own access level, so a package-private class gets a package-private no-arg constructor and code in another package still cannot call it.
Records and enums sit outside this rule: a record's generated constructor mirrors its component list, and enum constructors are implicitly private, so neither gives you a no-arg constructor.
Common mistakes
Adding Foo(int id) and assuming new Foo() still works; the no-arg constructor was never generated, so unrelated files stop compiling.
Writing void Foo() instead of Foo(); it becomes a method nobody calls, the generated empty constructor runs instead, and every field keeps its 0/null/false default.
Leaning on the generated constructor to set up reference fields, then getting a NullPointerException the first time one of those fields is used.
Try it yourself
Change, predict, then run
Write an Account class with fields owner and balance and only the constructor Account(String owner, double balance), then add new Account() in main and read the compiler error. Make it compile by declaring Account() { owner = "unknown"; balance = 0.0; } and print the fields of both objects.
Open the Java workspaceCheck your understanding
A Point class with fields x and y had no constructors and is created as new Point() in forty places and by a JSON library. You now add Point(int x, int y). What keeps the old usages working?
- Declare Point() {} explicitly alongside Point(int x, int y)
- Nothing; the generated no-arg constructor stays available next to constructors you write
- Rely on Java default parameter values so new Point() resolves to Point(0, 0)
- Make Point(int x, int y) public, which restores no-arg construction for other callers
Show answer
The compiler generates the no-arg constructor only for a class that declares none, so it vanished the moment Point(int, int) appeared and must be written back by hand. Option 2 is tempting if you come from Python or Kotlin, but Java has no default parameter values; option 3 confuses visibility, which controls who may call an existing constructor, with existence.