Constructors

Write constructors that make every object valid from the start, chain them with this(...), and avoid the shadowing and default-constructor traps.

SyntaxClassName(Type a, Type b) { this.a = a; this.b = b; } ClassName(Type a) { this(a, defaultB); }

A constructor runs once, when new creates the object. It has the same name as the class and no return type. Its job is to leave the object in a valid state, so the rest of the code never has to check whether a field was set.

class Product {
    final String sku;
    final int priceCents;

    Product(String sku, int priceCents) {
        this.sku = sku;
        this.priceCents = priceCents;
    }
}

The default constructor

If a class declares no constructor at all, the compiler adds a no-argument one. As soon as you write any constructor, that free one disappears. Code that did new Product() stops compiling, and libraries that create objects by reflection, such as some JSON mappers and JPA, fail at runtime with an error about a missing no-argument constructor. Records and Jackson's constructor support avoid this, but it is worth knowing why the error appears.

Validate in one place

Check arguments in the constructor and throw IllegalArgumentException (or NullPointerException via Objects.requireNonNull) when they are wrong. An object that cannot exist in an invalid state removes a whole class of bugs further down, for example a negative price reaching the payment provider. When you need several ways to build an object, have the shorter constructors delegate with this(...) to one main constructor, so validation lives once. Before Java 25, this(...) had to be the first statement; Java 25 lets checks run before it (next lesson).

Mistakes people make

  • Writing name = name; when the parameter has the same name as the field. That assigns the parameter to itself and leaves the field null. Use this.name = name;.
  • Adding a return type, as in void Product() {}. That declares an ordinary method that happens to share the class name; it never runs on new.
  • Doing heavy work in a constructor, such as opening network connections. It makes objects slow and hard to test; pass ready dependencies in instead.

Constructor injection

Services receive their collaborators through the constructor: a repository, a clock, an HTTP client. The dependencies are explicit, can be final, and a test can pass a fake. Spring uses exactly this when a bean has a single constructor, so the habit carries straight over.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Reject a negative price or blank SKU when a product object is built from a request body.
  • Pass a repository and a clock into a service constructor so tests can supply fakes.
  • Offer a short constructor with sensible defaults that delegates to the full one with this(...).

More examples

The shadowing mistake

Inside the constructor the bare name refers to the parameter, so the field is never touched. Making the field final turns this mistake into a compile error, because a final field that is never assigned does not compile.

Example · java
class Broken {
    String name;

    Broken(String name) {
        name = name;   // assigns the parameter to itself
    }
}

class Fixed {
    String name;

    Fixed(String name) {
        this.name = name;
    }
}

void main() {
    IO.println(new Broken("orders").name);
    IO.println(new Fixed("orders").name);
}
// Output:
// null
// orders

Constructor injection for a service

The service cannot be created without its repository, and the test passes a lambda instead of a real database. This is the same shape Spring fills in when a bean has one constructor.

Example · java
import java.util.Map;
import java.util.Objects;

interface PriceRepository {
    int priceCents(String sku);
}

class CheckoutService {
    private final PriceRepository prices;
    private final int taxPercent;

    CheckoutService(PriceRepository prices, int taxPercent) {
        this.prices = Objects.requireNonNull(prices, "prices");
        this.taxPercent = taxPercent;
    }

    int totalCents(String sku, int quantity) {
        int net = prices.priceCents(sku) * quantity;
        return net + net * taxPercent / 100;
    }
}

void main() {
    Map<String, Integer> table = Map.of("mug", 1000, "pen", 250);
    PriceRepository fake = sku -> table.get(sku);

    CheckoutService service = new CheckoutService(fake, 10);
    IO.println(service.totalCents("mug", 2));
    IO.println(service.totalCents("pen", 4));

    try {
        new CheckoutService(null, 10);
    } catch (NullPointerException e) {
        IO.println("Missing dependency: " + e.getMessage());
    }
}
// Output:
// 2200
// 1100
// Missing dependency: prices

A return type turns it into a method

Because of the void, the compiler treats this as an ordinary method and supplies a default constructor. Nothing runs on new, so the setup code is silently skipped until someone calls it by name.

Example · java
class Widget {
    String label = "default";

    void Widget() {        // a method named Widget, not a constructor
        label = "configured";
    }
}

void main() {
    Widget w = new Widget();
    IO.println(w.label);
    w.Widget();
    IO.println(w.label);
}
// Output:
// default
// configured

Discussion

  • Be the first to comment on this lesson.