The Sensible Use of Method Overriding: Part 2
Design with Java:
QUOIN
1208 Massachusetts Avenue,
Suite 3
Cambridge, Massachusetts
02138
tel: 617.492.6461
fax: 617.492.6461
email: info@[Link]
web: [Link]
Design with Java
The Sensible Use of Method Overriding: Part 2
by David M. Papurt
printed in the Journal of Object-Oriented Programming
Introduction
This is the second installment in a two part series of columns covering the sensible use of
inheritance and method overriding. As stated in Part 1 [1], experience shows that without
concrete guidelines, it is all too easy to misapply inheritance and method overriding.
Automatic up-conversion from descendents into ancestors and polymorphic method
selection - which are central to the sensible use of inheritance and method overriding - can
produce semantic nonsense and errors when the inheritance relationship or overriding
methods are inappropriate, particularly when reuse motivates the introduction of
inheritance.
Part 1 covered three topics: polymorphic method selection was defined, and rationale for
preference of polymorphic criteria over other method selection criteria was presented; final,
non-abstract, and abstract methods were compared and interpreted; and the substitution
principle guideline for the implementation of overriding methods was presented. In
particular, the substitution principle simplifies reasoning about program behavior, but
severely constrains the implementation of overriding methods in descendent classes.
This column presents guidelines for the implementation of overriding methods expressed in
terms of constraints on method pre- and post-conditions. Less restrictive than the
substitution principle and the paradigmatic forms appearing in Part 1, satisfaction of pre-
and post-condition rules is necessary for program correctness.
An overriding method in a descendent class ought to conform to the overridden method in
the ancestor class or interface. The discussion on the substitution principle in Part 1
contained one way to effect such conformance. Pre- and post-condition rules correspond to
a second meaning for conformance of subclass overriding methods to superclass
overridden methods.
Pre- And Post-Conditions
A pre-condition on a method is a condition that should be satisfied prior to a call of the
method. A post-condition on a method is a condition guaranteed satisfied following method
execution, if the method pre-condition was satisfied prior to the method call. All methods
have pre- and post-conditions, whether compiler enforced, run-time checked, stated in
commentary, or otherwise.
The programmer of a method call needs to assure that the pre-condition is satisfied
immediately prior to the method call. The method implementor assures that the post-
condition holds at method termination. In practice, the post-condition of a method called
earlier in some method call sequence contributes to establishing the pre-condition of a
method called later in that sequence.
A typical method pre-condition constrains the state of the object the method is called on. A
second example pre-condition constrains a method actual argument value. Constraint on
method actual argument type at method entry is so important a pre-condition that the Java
language includes strict type safety mechanisms to aid in its enforcement.
A typical method post-condition might constrain method return value in terms of method
argument value. Another might guarantee method return type. One more might guarantee
method side effects. As described in reference [2], the post-condition guarantee that object
state at method exit satisfies the class representation invariant is contingent upon the pre-
condition that object state satisfies the same invariant at method entry.
Guidelines
This section states pre- and post-condition guidelines for the implementation of overriding
methods [3], [4]. Generally, adherence to pre- and post-condition rules is by programmer
convention; for the most part, language mechanisms do not enforce the programming style,
though compile-time or run-time mechanisms can enforce certain constraints. As will be
reasoned, the guidelines ought to be followed; otherwise programs are incorrect and
corrupted.
* A pre-condition on an overriding subclass method (1) must not be tighter and (2)
can be looser than the pre-condition on the overridden superclass method 1 .
* A post-condition on an overriding subclass method (1) must not be looser and (2)
can be tighter than the post-condition on the overridden superclass method.
Consider the pre-condition constraint. Normally, a method is called through a superclass
reference after establishing the superclass method pre-condition, without regard for any
overriding method pre-conditions. If an overriding method pre-condition is tighter than the
superclass method pre-condition, then the tighter pre-condition may not be satisfied when
the overriding method executes at a call through a superclass reference; failure to satisfy the
tighter pre-condition corrupts the program. But if an overriding method pre-condition is
looser than the superclass method pre-condition, then the looser pre-condition is guaranteed
satisfied when the overriding method executes.
Next consider the post-condition constraint. Normally, a caller of a method depends upon a
superclass method post-condition to establish some condition - the pre-conditions of
subsequent expressions or statements - without regard for any overriding method post-
conditions. If an overriding method post-condition is looser than the superclass method
post-condition, then when executed, the overriding method may establish a condition
assumed impossible following the method call and in violation of a subsequent, dependent
pre-condition; failure to satisfy a subsequent pre-condition corrupts the program. But if an
overriding method post-condition is tighter than the superclass method post-condition, then
no unexpected conditions can be established when the overriding method executes, and all
subsequent, dependent pre-conditions are satisfied.
Consequently, failure to adhere to pre- and post-condition constraints admits incorrect
programs. Several examples appear below.
Examples
This section presents examples of overriding methods that both satisfy and violate pre- and
post-condition constraints; both correct and incorrect programs are described.
Post-condition Tightening - Side Effect
The first example involves class Figure hierarchy and abstract method draw() overridden in
descendent class Circle. Below, pre-conditions appear in commentary in Requires: clauses,
and post-conditions appear in Effects: clauses.
The post-condition is a constraint on the side effect generated by method draw(). The
example demonstrates correct post-condition tightening. Pre-condition TRUE on
[Link]() always is satisfied. There is no looser pre-condition, so extended class
overrides cannot modify the pre-condition. Such a broad pre-condition is not uncommon.
The post-condition on [Link](), Draws a Figure to Window, is somewhat broad. The
post-condition on [Link](), Draws a Circle with radius r at center c to Window, is
more specific and tighter than [Link]() post-condition. The former post-condition
subsumes the latter.
Below, method draw() is called through Figure reference s.
The program assumes that the method call draws some sort of Figure to the Window. If s
references a Circle, then the assumption is satisfied, and execution of [Link]() does
not corrupt the program.
In this example, programmer convention must realize the post-conditions within superclass
and extended class method implementations.
Post-condition Tightening - Return Type
The second example involves class Figure hierarchy and abstract method copy() 2
overridden in descendent class Circle.
The post-condition is a constraint on the type of object returned from method copy(). The
example demonstrates correct post-condition tightening.
The post-condition on [Link](), Returns reference to Figure object copy, is somewhat
broad. The post-condition on [Link](), Returns reference to Circle object copy, is
tighter than [Link]() post-condition. The former post-condition subsumes the latter.
Below, method copy() is called through Figure reference s.
The program assumes [Link]() returns a reference to a Figure object; the subsequent
initialization of d exploits that assumption. If s references a Circle then the assumption is
satisfied, and execution of [Link]() does not corrupt the program; treatment of the
Circle object referenced by d as a Figure is correct.
The type system enforces [Link]() post-condition. But programmer convention must
realize the stricter post-condition on [Link]().
Figure method copy() and overrides do not satisfy the substitution principle described in
Part 1 [1] of this column, even though the methods satisfy pre- and post-condition
constraints. This example is representative of the category of methods where overrides do
not return the same value as the overridden method, and therefore cannot satisfy the
substitution principle, but are still correct. Compare method draw() in the previous example
which satisfies the substitution principle as well as pre- and post-condition constraints.
Pre-condition Tightening - Incorrect Program
The next example involves class Time hierarchy and non-abstract method setHour()
overridden in descendent class SomeTime.
The pre-condition is a constraint on the value of integer argument h passed into setHour().
The example demonstrates incorrect pre-condition tightening; execution of setHour()
eventually corrupts any using program.
The pre-condition on [Link](), 0 <= h < 24, is relatively broad. (Post-condition,
Time object hour state is set to h, is implicit.) The pre-condition on override
[Link](), 0 <= h < 12, is relatively narrow. If a caller fails to meet
[Link]() pre-condition, then the SomeTime object and containing program are
corrupted.
Below, if actual argument n satisfies the superclass method pre-condition 0 <= n < 24,
method setHour() is called through Time reference p.
The caller does not verify the overriding method pre-condition. So if p references a
SomeTime object, then tighter overriding method pre-condition 0 <= n < 12 may be
violated, and the program will be corrupted. The origin of corruption is violation of pre-
and post-condition constraints for the implementation of overriding functionality.
An overriding method can loosen a pre-condition without program corruption. The example
below modifies the Time hierarchy, and incorporates a loosened pre-condition.
Now, if the extended class method executes, the program cannot be corrupted.
Language Rules
Several Java language rules can be easily understood in terms of the pre- and post-
condition constraints. For example, when overriding a method, the types listed in the
overriding method throws clause can be a subset of the types listed in the overridden
method throws clause. The sample code below is perfectly legal Java.
Method D1.f() may throw only one of the two exceptions that B.f() may throw, and D2.f()
may throw no exceptions at all.
Disregarding syntax, semantically a type in a method throws clause can be viewed as not
very different than the return type of a method. Above, B.f() can be viewed as "returning"
either a Z, an X1, or an X2 object, depending upon program state at method entry; that is
B.f() "returns" a single object at each method call, but that single object can have differing
type for differing method calls. Eliminating "return" types in overriding methods is clearly
a case of correct post-condition tightening 3 4.
Another language rule that can be understood in terms of the pre- and post-condition
constraints involves method access specifiers: an overriding method can have a looser
access specifier than the overridden method. In order of most restrictive access to least
restrictive access, the method access specifiers are private, package 5 , protected, and
public. The sample code below is perfectly legal.
Method D.g() is accessible from all parts of the program that B.g() is accessible plus D.g()
is accessible from additional parts of the program that B.g() is not accessible. A similar
statement can be made about D.h() and B.h().
The access qualifier that applies to a method is essentially a language enforced pre-
condition that dictates the regions of the program that can call the method; a looser access
qualifier on a method corresponds to additional regions that may call the method and a
looser pre-condition. So loosening access on overriding methods is clearly a case of correct
pre-condition loosening.
Standard Library Design
Unfortunately, many programmers do not consider the pre- and post-condition constraints
when creating class hierarchies. Nonsensical cases can be found even in the Java standard
library. Consider the abbreviated descriptions of superclass [Link] and
extended class [Link] below.
Dictionary is an abstract class that defines an interface for storing key-value pairs where
both the key and value can have arbitrary class type. Hashtable is a particularly useful
concrete implementation of Dictionary that stores key-value pairs using a hash table
strategy; again both the key and value can have arbitrary class type.
Properties is a concrete class for storing key-value pairs, but both the key and value must
be of the fixed type String. As stated in the comment in the class Properties text above and
in the class Properties documentation, when inserting a key-value pair into a Properties
object by calling [Link](Object,Object) (the only way to make an insertion), both
key and value must be of the type String. This constraint is nothing less than a tightening of
the pre-condition on method put(); the potential for error is obvious.
This does not mean that programs cannot be written successfully using a Properties object;
so long as only type String keys and values are inserted by programmer convention, there
will be no errors. However, this is much like saying that in the example above involving
classes Time and SomeTime, that the argument to setHour() should be kept between 0 and
12 by programmer convention. Both practices are inconsistent with the semantics of the
superclass.
Reuse nearly always motivates such designs; it seems that the designers of class Properties
wanted to reuse the functionality of class Hashtable. A better design simply composes
Properties out of Hashtable.
Of course, explicit call through methods must now be written for class Properties.
However, this approach achieves the same level of reuse as the standard library design, and
in this case - and as it should be - by language enforcement it is impossible to use a
Properties object where a Hashtable is expected.
Summary
The pre-condition on an overriding method can be looser than the pre-condition on the
overridden method; the post-condition can be tighter. Pre- and post-condition rules for the
implementation of overriding functionality are less constraining than rules for conformance
to the substitution principle. But reasoning about method behavior under pre- and post-
condition rules is complicated as compared to reasoning about method behavior under the
substitution principle; less constraint makes more complexity.
Ultimately, there is no choice. Though adherence to substitution principle constraints is
optional, pre- and post-condition rules must be followed. Any other policy admits poorly
designed and incorrect programs. Adherence to pre- and post-condition rules for the
implementation of overriding functionality is a necessary - but not sufficient - condition for
program correctness.
Footnotes
1
Here, the term subclass can refer to either an extended class (a subclass of another class)
or an implementing class (a subclass of an interface), and the term superclass can refer to
either a class or an interface.
2
The Java standard library class Object includes method clone() for object copying.
Method clone() is more complicated than method copy() employed here as it uses
exceptions to indicate whether it is supported or not, so is beyond the scope of the present
discussion. Object cloning will be discussed in a future column.
3
Here, when enclosed in quotation marks, the various tenses and forms of the term return
include normal return values and thrown objects.
4
The earlier example involving class Figure hierarchy and method copy() can be viewed as
a case of eliminating "return" types in overriding methods.
5
Of course, there is no explicit package keyword or specifier, and methods indicate
package access by having no access specifier at all. The term is used here for convenience
only.
References
[1] Papurt, D. M. and LeJacq, J. P., "Design With Java: The Sensible Use of Method
Overriding: Part 1", Journal of Object-Oriented Programming, Vol. 10, No. 1, March-
April 1997, pp. 62-65, 71.
[2] Papurt, D. M., Inside the Object Model: The Sensible Use of C++, SIGS Books, New
York, NY, 1995.
[3] LeJacq, J. P., "Semantic Based Design Guidelines for Object-Oriented Programs,"
Journal of Object Oriented Programming, Focus on Analysis and Design, SIGS, 1991, pp.
87-97.
[4] Cardelli, L. A., "A Semantics of Multiple Inheritance," Semantics of Data Types,
Lecture Notes in Computer Science No. 173, Springer-Verlag, 1984, pp. 51-67.
Biographies
David Papurt is founder of David Papurt Associates, Cambridge, MA, a firm specializing
in Java, C++, and object-oriented analysis and design. He is also author of the highly
acclaimed text Inside the Object Model: The Sensible Use of C++ (SIGS Books, 1995). He
may be contacted at 75310.1621@[Link].
------------------------------------------------------------------------
article presented with permission of David Papurt Associates
© Copyright QUOIN, 1997