Index

Design by Contract and Defensive Programming

Java Object-Oriented Design

13.1 Preconditions, Postconditions, Invariants

In the pursuit of building reliable and maintainable software, Design by Contract (DbC) stands out as a powerful methodology to ensure program correctness. Introduced by Bertrand Meyer with the Eiffel programming language, Design by Contract establishes formal agreements—or contracts—between software components, specifying their mutual obligations and benefits. This methodology focuses on clearly defining what a method expects, guarantees, and maintains, reducing ambiguity and minimizing bugs.

At the core of Design by Contract are three fundamental concepts: preconditions, postconditions, and invariants. These act as rules that must be respected by both the caller of a method and the method implementation itself.

Preconditions: What Must Be True Before Execution

A precondition is a condition or requirement that must hold before a method executes. It defines the responsibilities of the caller—essentially what the caller guarantees when invoking the method. If a precondition is violated, the method is free from any obligation to behave correctly, meaning the caller is misusing the API.

Example: Consider a method that calculates the square root of a number.

public double sqrt(double value) {
    if (value < 0) {
        throw new IllegalArgumentException("Value must be non-negative");
    }
    return Math.sqrt(value);
}

Here, the precondition is that value must be non-negative. The caller must ensure this before calling sqrt(). If this precondition is violated, the method throws an exception indicating improper use.

Postconditions: What Must Be True After Execution

A postcondition defines what the method guarantees to be true after it finishes execution, provided that the preconditions were met. It specifies the expected state or output that the caller can rely on.

Example: Continuing with the sqrt method, a postcondition might be that the returned result is always non-negative.

public double sqrt(double value) {
    if (value < 0) {
        throw new IllegalArgumentException("Value must be non-negative");
    }
    double result = Math.sqrt(value);
    assert result >= 0 : "Postcondition violated: result is negative";
    return result;
}

This postcondition asserts that the returned value will be greater than or equal to zero. While Java doesn’t enforce contracts natively, developers can use assertions or explicit checks to verify postconditions during testing.

Invariants: Conditions That Always Hold True

An invariant is a condition that must always be true for a class’s instance—before and after any public method executes. Invariants represent the consistent state of an object, ensuring its internal data remains valid throughout its lifecycle.

Example: Imagine a simple BankAccount class:

public class BankAccount {
    private double balance;

    public BankAccount(double initialBalance) {
        if (initialBalance < 0) {
            throw new IllegalArgumentException("Initial balance cannot be negative");
        }
        this.balance = initialBalance;
    }

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit must be positive");
        }
        balance += amount;
        assert balance >= 0 : "Invariant violated: balance negative";
    }

    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Withdrawal must be positive");
        }
        if (amount > balance) {
            throw new IllegalArgumentException("Insufficient funds");
        }
        balance -= amount;
        assert balance >= 0 : "Invariant violated: balance negative";
    }

    public double getBalance() {
        return balance;
    }
}

Here, the invariant is that balance must never be negative. This condition is checked after each operation that modifies the state, ensuring the object remains valid.

Benefits of Using Contracts in APIs

By clearly defining preconditions, postconditions, and invariants, APIs become more transparent and robust:

When contracts are violated, exceptions or assertion failures provide immediate feedback, preventing obscure bugs that may only appear much later in the system.

Contracts and Formal Specifications

Design by Contract moves beyond informal comments and verbal agreements by establishing formal specifications that can be enforced programmatically or at least systematically tested. While Java does not natively enforce contracts, tools and libraries (such as the Java Modeling Language (JML) or runtime assertions) can help integrate DbC practices.

Formal contracts encourage developers to think rigorously about every method’s obligations, promoting disciplined and predictable designs. They are especially valuable in large codebases or team environments, where clear interfaces reduce integration errors.

Summary

Together, these contracts form a foundation for correctness, clarity, and reliability in Java software design. Embracing these principles leads to APIs that are easier to use, debug, and maintain—helping you write code that you and others can trust.

If you’ve ever wondered how to ensure your methods and classes behave exactly as intended, Design by Contract provides a systematic approach to formalize those expectations. It’s a powerful mindset that transforms vague assumptions into explicit agreements, helping prevent bugs before they happen.

Index

13.2 Assertions

In Java programming, assertions serve as a built-in mechanism for testing assumptions during development. They play a key role in Design by Contract (DbC) by allowing developers to formally verify preconditions, postconditions, and invariants—the essential agreements between a method and its caller. Although assertions are not intended for handling user or runtime errors, they are a valuable tool for improving software correctness and robustness during development and testing phases.

What Are Assertions?

An assertion is a statement in code that asserts a condition must be true at a specific point of execution. If the assertion fails—meaning the condition evaluates to false—the Java Virtual Machine (JVM) throws an AssertionError. Assertions help detect logical flaws early by surfacing broken assumptions as soon as they occur.

Introduced in Java 1.4, the assert keyword is not enabled by default in production environments. It is mainly a development-time feature that helps verify the internal consistency of your program.

Syntax and Usage of the assert Keyword

Java’s assert statement comes in two forms:

assert condition;
assert condition : errorMessage;

The second form allows you to provide a custom error message that helps identify the problem when the assertion fails.

Example:

int result = divide(10, 2);
assert result == 5 : "Expected result to be 5";

To enable assertions during runtime, use the -ea flag with the java command:

java -ea MyApp

By default, assertions are disabled unless explicitly enabled this way.

Using Assertions for Contracts

Assertions align closely with the Design by Contract model and can be used to enforce:

Preconditions What must be true before a method executes.

public int divide(int a, int b) {
    assert b != 0 : "Precondition failed: denominator must not be zero";
    return a / b;
}

Postconditions What must be true after a method executes.

public int increment(int x) {
    int result = x + 1;
    assert result > x : "Postcondition failed: result should be greater than input";
    return result;
}

Invariants Conditions that must always hold true for an objects state.

public class BankAccount {
    private double balance;

    public void deposit(double amount) {
        balance += amount;
        assert balance >= 0 : "Invariant violated: balance should not be negative";
    }
}

Assertions help make these expectations explicit, improving code readability and maintainability.

Assertions vs. Exceptions

While both assertions and exceptions signal problems, they serve different purposes:

Feature Assertions Exceptions
Purpose Detect programming errors Handle expected or unexpected runtime issues
Use in contracts Precondition/postcondition checks Input validation, recovery strategies
Enabled in prod Typically disabled Always active
Recoverable? No – indicates bugs Yes – can often recover gracefully

Use assertions to check for conditions that should never occur if the code is correct. Use exceptions to handle recoverable conditions, especially those caused by user input or external factors (e.g., file not found, invalid data).

Benefits and Limitations

Benefits:

Limitations:

Summary

Assertions are a lightweight yet powerful tool to reinforce the contractual logic in your programs. By expressing assumptions and verifying them during development, you can catch subtle bugs before they manifest in production. Although assertions are not a runtime safety net, they greatly aid in writing correct, self-validating code—especially when used to reinforce design principles like preconditions, postconditions, and invariants.

When used wisely, assertions make your codebase not just more robust, but more expressive, readable, and aligned with your design intentions.

Index

13.3 Defensive Copies

In Java, defensive copying is a programming technique used to protect the internal state of an object from unintended or malicious modification. It is especially important when exposing mutable objects (like arrays, List, Map, or custom objects) through public methods. By returning or accepting copies instead of references, you ensure that external code cannot alter the internal data of your class, maintaining encapsulation and object integrity.

Why Defensive Copies Matter

When an object exposes a direct reference to a mutable field, external code can change it, potentially violating class invariants or expected behavior. This breaks the contract between a class and its clients and can lead to hard-to-diagnose bugs.

Example without defensive copy:

public class Person {
    private Date birthDate;

    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }

    public Date getBirthDate() {
        return birthDate; // Dangerous: exposes internal state
    }
}
Person p = new Person(new Date());
Date d = p.getBirthDate();
d.setTime(0); // Modifies the Person’s internal birthDate!

Here, the internal birthDate of Person is unintentionally modified from outside the class.

Implementing Defensive Copies

To avoid this, return a copy of the mutable object:

public class Person {
    private Date birthDate;

    public Person(Date birthDate) {
        this.birthDate = new Date(birthDate.getTime()); // Defensive copy on input
    }

    public Date getBirthDate() {
        return new Date(birthDate.getTime()); // Defensive copy on output
    }
}

This ensures that changes to the original Date or to the returned Date do not affect the internal state of the Person object.

With arrays:

public class DataHolder {
    private int[] data;

    public DataHolder(int[] data) {
        this.data = Arrays.copyOf(data, data.length); // Defensive copy
    }

    public int[] getData() {
        return Arrays.copyOf(data, data.length); // Defensive copy
    }
}

With collections:

public class Student {
    private List<String> courses = new ArrayList<>();

    public void setCourses(List<String> courses) {
        this.courses = new ArrayList<>(courses); // Defensive copy
    }

    public List<String> getCourses() {
        return new ArrayList<>(courses); // Defensive copy
    }
}

Trade-Offs of Defensive Copying

Benefits:

Costs:

Therefore, defensive copying is most valuable in situations where data integrity outweighs performance concerns, such as APIs, libraries, or critical systems.

Conclusion

Defensive copying is a foundational defensive programming technique in Java. When designing classes that deal with mutable objects, you should assume that callers might inadvertently—or intentionally—modify what they receive or pass in. By defensively copying, you uphold object boundaries and enforce contracts reliably, ensuring your objects remain safe, predictable, and easy to reason about.

Index