What does the Liskov substitution principle require of a subclass, and how do you recognise a violation?
The principle says a subclass must be usable anywhere its parent is expected, without the caller needing to know or care. Formally, if code works correctly with the parent type, it must work correctly when given the child type instead. That is what makes polymorphic dispatch safe rather than a trap.
public class Main {
public static void main(String[] args) {
java.util.List<Account> accounts = new java.util.ArrayList<>();
accounts.add(new SavingsAccount(100.0));
accounts.add(new FixedDepositAccount(100.0));
for (Account a : accounts) {
try {
System.out.println(a.type() + " withdraw -> " + a.withdraw(50.0));
} catch (RuntimeException e) {
System.out.println(a.type() + " refused: " + e.getMessage());
}
}
}
}
class Account {
protected double balance;
Account(double balance) { this.balance = balance; }
String type() { return "account"; }
double withdraw(double amount) {
if (amount > balance) { throw new IllegalStateException("insufficient funds"); }
balance = balance - amount;
return balance;
}
}
class SavingsAccount extends Account {
SavingsAccount(double balance) { super(balance); }
String type() { return "savings"; }
}
class FixedDepositAccount extends Account {
FixedDepositAccount(double balance) { super(balance); }
String type() { return "fixed deposit"; }
double withdraw(double amount) {
throw new UnsupportedOperationException("fixed deposits cannot be withdrawn early");
}
}The example shows a violation. The fixed deposit account inherits withdraw and then refuses to honour it, so a caller holding an Account reference cannot rely on the call succeeding. The output shows the savings account working normally while the fixed deposit is refused, and the caller had no way to predict which would happen from the type alone.
There are several ways to break the contract, and the exception is the most obvious. Others include narrowing the visibility of an inherited method, returning a narrower type than the parent promised, changing the meaning or units of a parameter or a return value, and adding new checked exceptions to an overridden method.
The fix is to make the hierarchy match the behaviour rather than the vocabulary. If a fixed deposit genuinely cannot be withdrawn, then withdraw should not be part of the contract that all accounts promise. Move it to a narrower interface that only the accounts that support it implement, and let the caller that needs to withdraw depend on that narrower type. The class named account then becomes an honest abstraction again.