Preconditions cannot be strengthened in a subtype.
The preconditions are assumptions we are making on the state/parameters at the time a method is invoked. Here's a simple example using a setter.
class Super {
private int myInt;
// Precondition: newValue can be anything.
public void setMyInt(int newValue) {
myInt = newValue;
}
}
class Sub extends Super {
// Stronger precondition: newValue must be > 0.
@Override
public void setMyInt(int newValue) {
if (newValue <= 0) throw new RuntimeException("Must be positive");
super.setMyInt(newValue);
}
}
This breaks Liskov substitution because we can't guarantee anymore that superInstance.setMyValue(-3);
won't throw an exception (i.e. if superInstance
is of actual type Sub
).
Edit per Christophe's comment: this is only a violation of LSP if the contract of super.setMyValue(someNum)
is broken in a derived class. The contract is what it sounds like: an agreement between the caller and the implementer about how a method will behave. In our example, if the contract allows for setMyValue(int newValue)
to throw exceptions, then there's no LSP violation even if, coincidentally, the base method does not throw, while the overridden method does.
Some languages allow us to specify the possible exceptions that can be thrown in the method declaration. For others, you may just have to read the docs to find out whether a method is allowed to throw exceptions, and which kind.
Postconditions cannot be weakened in a subtype.
The postconditions are assumptions we are making on the state/return value after a method is invoked. Here's another example using a getter.
class Super {
private String myString;
// Postcondition: myString is always nonull.
public String getMyString() {
return myString == null ? "" : myString;
}
}
class Sub extends Super {
private String myBetterString;
// Weaker postcondition: myString can be null.
@Override
public String getMyString() {
return myBetterString;
}
}
Invariants of the supertype must be preserved in a subtype.
Invariants are certain assumptions that we are allowed to make about the state.
class Super {
// Invariant: numInstances is an increasing sequence.
protected static int numInstances;
public static int getNumInstances() {
return numInstances;
}
public Super() {
numInstances++;
}
}
class Sub extends Super {
// Invariant violated: creating a new Sub decrements the sequence.
public Sub() {
numInstances--;
}
}
This breaks Liskov substitution because we can't guarantee anymore that Super.getNumInstances();
is always increasing (or even always positive) once we start creating Sub
instances. Yeah, I know we could fix this by making numInstances private instead. A real invariant might be more subtle.
History constraint (the "history rule"). Objects are regarded as being modifiable only through their methods (encapsulation). Since subtypes may introduce methods that are not present in the supertype, the introduction of these methods may allow state changes in the subtype that are not permissible in the supertype. The history constraint prohibits this.
I hadn't heard of the history constraint before. As I come to understand it, the basic idea is that derived methods shouldn't change the state of the superclass in a way that it couldn't have been changed with just the superclass' public methods. This page gives a good example of a history constraint violation:
A violation of this constraint can be exemplified by defining a mutable point as a subtype of an immutable point. This is a violation of the history constraint, because in the history of the immutable point, the state is always the same after creation, so it cannot include the history of a mutable point in general.