|
Problem Set 4 Subtyping and Inheritance |
Due: Monday, 11 October (drop off at my office, Olsson 236A, before 5pm) |
Collaboration Policy. For this problem set, you may either work alone and turn in a problem set with just your name on it, or work with one other student of your choice. If you work with a partner, you and your partner should turn in one assignment with both of your names on it.
Regardless of whether you work alone or with a partner, you are encouraged to discuss this assignment with other students in the class and ask and provide help in useful ways. You may consult any outside resources you wish including books, papers, web sites and people, with the important exception of materials from previous cs2220/cs205 courses. If you use resources other than the class materials, indicate what you used along with your answer.
Bertrand Meyer's justification for violating the substitution principle from Static Typing and Other Mysteries of Life
Liskov's Chapter 7 and Meyer's Static Typing and Other Mysteries of Life describe two very different rules for subtypes.
Liskov's substitution principle requires that the subtype specification supports reasoning based on the supertype specification. When we reasoned about a call to a supertype method, we reasoned that if a callsite satisfies the preconditions in the requires clause, it can assume the state after the call satisfies the postconditions in the effects clause. This means the subtype replacement for the method cannot make the precondition stronger since then our reasoning about the callsite may no longer hold (that is, the callsite may not satisfy the stronger precondition). Hence, the type of the return value of the subtype method must be a subtype of the type of the return value for the supertype method; the types of the parameters of the subtype method must be supertypes of the types of the parameters of the supertype method. This is known as contravariant typing.
Bertrand Meyer prefers covariant typing: the subtype replacement method parameter types must be subtypes of the types of the parameters of the supertype method. We will generalize his rules to apply to preconditions and postconditions also: the subtype method preconditions must be stronger than the supertype method precondition (presub => presuper) and the subtype postconditions must be stronger than the supertype postconditions (postsub => postsuper). The => notation indicates logical implication: a => b means that knowing a is true is enough to guarantee that b must be true. Note that unlike the corresponding Liskov substitution rule, (presuper && postsub) => postsuper, there is no need for presuper in the covariant rule since postsub => postsuper.
Consider the minimal Tree class and its BinaryTree subtype, both specified below:
public class Tree // OVERVIEW: A Tree is a mutable tree where the nodes are int values. // A typical Tree is < value, [ children ] > // where value is the int value of the root of the tree // and children is a sequence of zero or more Tree objects // that are the children of this tree node. // A Tree may not contain cycles, and may not contain the same // Tree object as a sub-tree in more than one place. public Tree (int val) // EFFECTS: Creates a tree with value val and no children: < value, [] > public void addChild (Tree t) // REQUIRES: t is not contained in this. // MODIFIES: this // EFFECTS: Adds t to the children of this, as the rightmost child: // this_post = < this_pre.value, children > // where children = [ this_pre.children[0], this_pre.children[1], ..., // this_pre.children[this_pre.children.length - 1], t] // NOTE: the rep is exposed! public Tree getChild (int n) // REQUIRES: 0 <= n < children.length // EFFECTS: Returns the Tree that is the nth leftmost child // of this. // NOTE: the rep is exposed!
public class BinaryTree extends Tree // OVERVIEW: A BinaryTree is a mutable tree where the nodes are int values // and each node has zero, one or two children. // // A typical BinaryTree is < value, [ children ] > // where value is the int value of the root of the tree // and children is a sequence of zero, one or two BinaryTree objects // that are the children of this tree node. // A BinaryTree may not contain cycles, and may not contain the same // BinaryTree object as a sub-tree in more than one place. public BinaryTree (int val) // EFFECTS: Creates a tree with value val and no children: // < value, null, null > @Override public void addChild (BinaryTree t) // REQUIRES: t is not contained in this and this has zero or one children. // MODIFIES: this // EFFECTS (same as supertype): // Adds t to the children of this, as the rightmost child: // this_post = < this_pre.value, children > // where children = [this_pre.children[0], this_pre.children[1], ..., // this_pre.children[this_pre.children.length - 1], t] @Override public BinaryTree getChild (int n) // REQUIRES: 0 <= n < 2 // EFFECTS: If this has at least n children, returns a copy of the BinaryTree // that is the nth leftmost child of this. Otherwise, returns null.
The questions below concern the Liskov substitution principle and Eiffel subtyping rule. Although we are asking them in the context of a Java-like class definition, note that the actual subtyping rules for Java are not the same as either of these. Java does has a signature rule (described below), but doesn't place any constraints on the behavior of methods.
The Java signature rule follows the covariant rule for return types. (This is a relatively recent change to Java — before Java 1.5, the Java signature rule was novariant: the subtype methods return type must match exactly).
For parameters, the actual Java rule is more complex because of overloading. The Java compiler allows a subclass to define a method that has the same name but different parameter types (including subtype) from a method in the superclass, but instead of overriding the superclass method it will overload the inherited method. Thus, the way addChild is declared in the BinaryTree subtype would be allowed by Java, but would not actually override the Tree.addChild(Tree) method. Instead, it would overload that method. This can be quite dangerous since the overloaded methods are resolved based on apparent types, not actual types. For example, try this program:
static public void main (String args[]) {
Tree t = new BinaryTree (3);
BinaryTree bt = new BinaryTree (4);
t.addChild (bt); // Calls the addChild(Tree) method
bt.addChild (new BinaryTree (5)); // Calls the addChild (BinaryTree) method
bt.addChild (new Tree (12)); // Calls the addChild (Tree) method
}
Note that the first call uses the inherited addChild(Tree)
because the apparent type of t is Tree, even though
its actual type is BinaryTree.
For these questions, you should assume a language that does not have overloading, but instead interprets the addChild method as overriding the supertype's method. (We use the @Override annotation to indicate this in the specification. The Java compiler would issue a warning for this, since the method does not actually override a superclass method. This is a good example of the value of using these annotations!)
2. Does the addChild method in BinaryTree satisfy the Liskov substitution principle? Explain why or why not.
3. Does the getChild method in BinaryTree satisfy the Eiffel subtyping rules? Explain why or why not.
4. Does the addChild method in BinaryTree satisfy the Eiffel subtyping rules? Explain why or why not.
To avoid running out of memory, we need to increase the maximum size of the Java VM heap. Do this by selecting the Arguments pane, and adding a VM argument (the bottom window in the pane):
-Xmx512m -eaThe first argument sets the maximum size of the VM heap to 512 megabytes. The second argument turns on assertion checking.
To turn on compiler warnings about missing @Overload annotations, select Project | Properties, then select Java Compiler and Errors/Warnings under it. Uncheck the Enable project specific settings box, and then select Configure Workspace Settings (at the top right). This pops up a dialog you can use to control the warnings given by the Java compiler. Select the Annotations (at the bottom) and for "Missing '@Override' annotation' select Warning.
ImageChop provides a graphical user interface (GUI) for manipulating a set of images by applying filters to generate new images. Every filter must be a subtype of the Filter datatype. The filters provided are shown in the class hierarchy below:
The Filter class is a subtype of java.lang.Object, the ultimate supertype of all Java object types. The Filter class provides methods for examining and manipulating an image including the filter abstract method. Since filter is an abstract method, the Filter abstract class provides no implementation of filter. To make a useful Filter object, we need a subclass of Filter that provides an implementation of the filter method.java.lang.Objectps4.Filter
ps4.PointFilter
ps4.BrightenFilter
ps4.GreyscaleFilter
ps4.BlurFilter
ps4.FlipFilter
ps4.MultiFilter
ps4.AddFilter
ps4.TileFilter
We provide two abstract subtypes of Filter:
For the next two questions, you are to implement new filters. The behavior of these filters is not specified precisely; you can determine in your implemention a good way to provide the effect described.
Turn-in Checklist: You should turn in your answers to questions 1-11 on paper either in class on Thursday, 7 October, or at my office before 5pm on Monday, 11 October. Also, submit the image you produced for question 11 as a JPG and a zip file containing all of your code by email to evans@cs.virginia.edu. You should also post your image (more than one is fine) on the course blog to share with the class. There may be token prizes for the best images created.