meeting -*- outline -*- * inheritance They should read Reil's book, ch. 5. ** motivation ------------------------------------------ WHY INHERITANCE? Want to avoid duplication of code in Sum, Mult, Diff classes. Want to make it easier to make similar classes (Divide, ...) ------------------------------------------ Avoiding code duplication is an important principle of design. Q: What problems happen if you write the same code twice? more space, more time harder to maintain and keep consistent! ** example Q: What code was duplicated between Sum, Mult, and Diff? ... extends FormulaType /** The left register */ private /*@ spec_public @*/ int left; /** The right register */ private /*@ spec_public @*/ int right; the code in the body of the constructors, and it's specification accesses to left and right in evaluate method Registers.get(left) Registers.get(right) So: make an abstract class, BinaryFormula. BinaryFormula contains - two private instance variables (int left, right) - a constructor (to init the private instance vars) - two methods, that return the values of the left and right registers: leftValue(), rightValue() Then fix up the classes Sum, Mult, and Diff, - make them subclasses of BinaryFormula - remove their instance vars, - put a super call in their constructors - change the evaluate method to use leftValue() and rightValue() Rerun the tests, note they work without change. package calc; /** Binary formulas (like a+b). * @author Gary T. Leavens */ public abstract class BinaryFormula implements FormulaType { /** The left register */ private /*@ spec_public @*/ int left; /** The right register */ private /*@ spec_public @*/ int right; /**Initialize this Binary Formula object to contain the * the given registers. * @param left, the left register's number. * @param right, the right register's number */ //@ requires 0 <= left && left < Registers.NUM_REGS; //@ requires 0 <= right && right < Registers.NUM_REGS; //@ assignable this.left, this.right; //@ ensures this.left == left && this.right == right; public BinaryFormula(int left, int right) { this.left = left; this.right=right; } /** * Return the left register's value. */ protected /*@ pure @*/ double leftValue() { return Registers.get(left); } /** * Return the right register's value. */ protected /*@ pure @*/ double rightValue() { return Registers.get(right); } } package calc; /** Difference formulas. * @author Gary T. Leavens */ public class Diff extends BinaryFormula { /**Initialize this Diff object to calculate the * difference of the given registers. */ //@ requires 0 <= left && left < Registers.NUM_REGS; //@ requires 0 <= right && right < Registers.NUM_REGS; //@ assignable this.left, this.right; //@ ensures this.left == left && this.right == right; public Diff(int left, int right) { super(left, right); } /** * Evaluate the formula and return its value. */ /*@ also @ ensures (* \result is the difference of the value of @ the left and right register's values. *); @*/ public double evaluate() { return leftValue() - rightValue(); } } Be sure to carefully explain the super call in the constructor. Sum and Mult are similar. ** semantics *** What happens when a subclass object is created? ------------------------------------------ OBJECT CREATION FormulaType f = new Diff(0, 1); ------------------------------------------ ... show picture of memory allocation on the heap Stack Heap f [ *-]------> [ class | *-]-----> [ classname | *-]--> "Diff" [ left | 0 ] [ parent | *-]--> [ BinaryFormula ] [ right | 1 ] [ vtable | *-]---> (...) It's important to note that the instance fields (left, right) inherited from the superclass are all *in* the subclass object. There's no extra indirection cost to access inherited instance fields. *** What happens when a subclass method is called? lookup in the object's class's vtable for code, followed by a procedure call ------------------------------------------ METHOD CALLS import calc.*; import java.util.Random; public class Client { private static Random rand = new Random(); public static void main(String [] argv) { BinaryFormula d = new Diff(0,1); BinaryFormula s = new Sum(2,3); Registers.put(0, 3.14); Registers.put(1, 10); Registers.put(2, 0); Registers.put(3, -100.0); System.out.println(d.evaluate()); System.out.println(s.evaluate()); BinaryFormula f; if (rand.nextBoolean()) { f = new Mult(0,3); } else { f = new Sum(1,2); } System.out.println(f.evaluate()); } } ------------------------------------------ Note that there is no way to tell before run-time the exact class of f for the last method call, f.evaluate(), above. Talk about the last method call above, and show on the pictures of the objects and classes and their vtables, and how the language finds the appropriate method. Semantics, to run o.m() 1. find the class of the object denoted by o, call this C, 2. look for m in C's vtable, if C's vtable has a method for f, go to step 3 to run it. else if C is not Object, then set C to C's superclass, and go to step 2, else if C is Object, then the method is not found (error). 3. set "this" to the o, assign the actuals to the formals, and then run the code. *** subtype polymorphism ------------------------------------------ SUBTYPE POLYMORPHISM The ability to call instance methods of a type on objects of its subtypes is called *(subtype) polymorphism*. For example: BinaryFormula f; ... f.evaluate() Also: Object x; ... x.toString(); x.equals(y); x.hashCode(); ------------------------------------------ Q: How does subtype polymorphism help in our example? ** two aspects of inheritance ------------------------------------------ TWO ASPECTS OF INHERITANCE I. Subclassing (deriving) of Code Implementations of non-private instance declarations are: - available in subclass by default - methods can be overridden - fields can be shadowed Code for a subclass is derived from its superclass. II. Behavioral Subtyping of ADTs Specifications of non-private instance declarations are: - imposed on subclasses - methods specifications can be more restrictive (refined) Objects of a subtype can be act like objects of their supertype. ------------------------------------------ Synonyms (C++ terms): superclass = base class subclass = derived class Note that the specification of evaluate in FormulaType, and hence in BinaryFormula is underspecifed, because it doesn't describe the exact function performed. Reil calls subclassing "generalization" (i.e., making a superclass for several subclasses), and the second "specialization", but those terms aren't standard and are confusing. *** these are distinct A type is (to a first approximation) a set of objects. So a subtype is a subset of objects. This is because every object of the subtype *is* an object of the supertype (the subsumption rule). Subtyping and subclassing are independent and distinct. E.g. (Alan Snyder, OOPSLA 1986): Dequeue vs. Stack can build Stack from Dequeue i.e., Stack as subclass of Dequeue by overriding some operations to throw exceptions (NOP them), but can't use a Stack when a Dequeue is expected Stack is not a behavioral subtype of Dequeue Can treat a Dequeue as a Stack Dequeue is a behavioral subtype of Stack But can't efficiently implement a Dequeue from a Stack Dequeue isn't a good subclass of Stack E.g. (Leavens's thesis): IntSet vs. Interval Can treat an Interval as an IntSet, but don't want its representation. (want subtype but not subclass) In Java, use an interface for sets to avoid this. E.g. (Cook's OOPSLA paper): Can program Bags from Set (change insert) or vice versa but Bag isn't a behavioral subtype of set or vice versa (want subclass but not subtype) *** it's best to make subclasses be behavioral subtypes It's probably best (according to Reil) to have both relationships at the same time: - less confusion - since it's mostly done that way, people tend to assume it Reil's heuristic 5.17: A derived class should never NOP a method of its base class. *** add new abstract classes to avoid NOPing methods In Java, an interface imposes obligations on the class that implements it and so gives a subtype relationship. ------------------------------------------ REFACTORING TO PRESERVE BEHAVIORAL SUBTYPING Wrong: public class Dequeue { public void addFront(Object o) {...} public void addRear(Object o) {...} /* ... */ } public class Stack extends Dequeue { public void addRear(Object o) { throw new ...Exception(); } } Refactoring 1: public abstract class List { public void addFront(Object o) {...} /* ... */ } public class Dequeue extends List { public void addRear(Object o) {...} /* ... */ } public class Stack extends List { /* ... */ } ------------------------------------------ *** use interfaces to get subtyping without inheritance ------------------------------------------ ANOTHER REFACTORING public interface ListType { public void addFront(Object o); /* ... */ } public class Dequeue implements ListType { public void addFront(Object o) {...} public void addRear(Object o) {...} /* ... */ } public class Stack implements ListType { public void addFront(Object o) {...} public void addRear(Object o) {...} /* ... */ } ------------------------------------------ ** bad example to show how inheritance should not be used ------------------------------------------ package calc; /** Negation of a register * @author Gary T. Leavens */ public class Negate extends BinaryFormula { /** Initialize this negation formula * to refer to the given register. */ public Negate(int reg) { super(reg, 0); } /** * @see calc.FormulaType#evaluate() */ public double evaluate() { return - leftValue(); } } ------------------------------------------ Q: Why is this bad? negate just has one register, so wastes space negation isn't binary, so confusing Q: How could we fix it? don't use inheritance... ------------------------------------------ BETTER package calc; /** Negation of a register * @author Gary T. Leavens */ public class Negate implements FormulaType { private /*@ spec_public @*/ int reg; /** Initialize this negation formula to refer * to the given register. */ /*@ requires 0 <= reg && reg < Registers.NUM_REGS; @ ensures this.reg == reg; @*/ public Negate(int reg) { this.reg = reg; } /** * @see calc.FormulaType#evaluate() */ public double evaluate() { return - Registers.get(reg); } } ------------------------------------------ Q: Is there still any repeated code? Yes, we're still declaring a register and using Registers.get. We could fix that by making another superclass. (but let's not do that here in class...) ** Method inheritance (super) and abstract methods *** motivation ------------------------------------------ ADDING 3 REGISTERS What code is duplicated below? package calc; /** Sum three registers * @author Gary T. Leavens */ public class Sum3 extends Sum { /** The third register to sum. */ private /*@ spec_public @*/ int reg3; /** * Initialize this Sum object to calculate * the sum of the given registers. */ //@ requires 0 <= reg1 && reg1 < Registers.NUM_REGS; //@ requires 0 <= reg2 && reg2 < Registers.NUM_REGS; //@ requires 0 <= reg3 && reg3 < Registers.NUM_REGS; //@ assignable this.left, this.right, this.reg3; //@ ensures this.left == reg1 && this.right == reg2; //@ ensures this.reg3 == reg3; public Sum3(int reg1, int reg2, int reg3) { super(reg1, reg2); this.reg3 = reg3; } /** * Evaluate the formula and return its value. */ /*@ also @ ensures (* \result is the sum of the value of @ all the register's values. *); @*/ public double evaluate() { return leftValue() + rightValue() + Registers.get(reg3); } } ------------------------------------------ ... it's the leftValue() + rightValue() that is duplicated. ------------------------------------------ USE SUPER TO ELIMINATE THE DUPLICATION package calc; /** Sum three registers * @author Gary T. Leavens */ public class Sum3 extends Sum { /** The third register to sum. */ private /*@ spec_public @*/ int reg3; /** * Initialize this Sum object to calculate * the sum of the given registers. */ //@ requires 0 <= reg1 && reg1 < Registers.NUM_REGS; //@ requires 0 <= reg2 && reg2 < Registers.NUM_REGS; //@ requires 0 <= reg3 && reg3 < Registers.NUM_REGS; //@ assignable this.left, this.right, this.reg3; //@ ensures this.left == reg1 && this.right == reg2; //@ ensures this.reg3 == reg3; public Sum3(int reg1, int reg2, int reg3) { super(reg1, reg2); this.reg3 = reg3; } /** * Evaluate the formula and return its value. */ /*@ also @ ensures (* \result is the sum of the value of @ all the register's values. *); @*/ public double evaluate() { return super.evaluate() + Registers.get(reg3); } } ------------------------------------------ To fix that, write: return super.evaluate() + Registers.get(reg3); Explain how this works, on the picture [ class | *-]-----> [ classname | *-]--> "Sum3" [ left | 0 ] [ parent | *-]----------> [ classname |*-]->"Sum" [ right | 1 ] [ vtable | *-]---> (...) [ parent |*-]->... [ reg3 | 2 ] [ vtable |*-]-| | v [ evaluate | *-]->... Q: What would happen if you wrote "this.evaluate()" in evaluate()? recursive call, in this case it loops forever. Note that "evaluate()" is short for "this.evaluate()"! Q: toString() is defined in Object, which is the superclass of BinaryFormula. How does a call super.toString() work from inside Sum3? *** abstract methods ------------------------------------------ ABSTRACT METHODS, CLASSES Def: an *abstract method* Def: an an *abstract class* ------------------------------------------ ... is a method that has only a signature, but no implementation. Q: how do you write an abstract method declaration in C++? use = 0 instead of a body Q: In Java? Use "abstract" as a modifier on the declaration and a semicolon (;) instead of the body Or, implement an interface and don't define one of the methods in the interface (which is then abstract). - all methods of an interface are abstract Q: Does BinaryFormula have any abstract methods? yes, evaluate() ... is a class that has some abstract methods, or otherwise should not be instantiated. In Java these are declared using the "abstract" modifier on the class. Q: What classes have we seen that are abstract? BinaryFormula An interface is like an abstract class in which *all* methods are abstract, and there are no constructors, and no (instance) fields. ------------------------------------------ INTERFACES AS EXTREMES OF ABSTRACT CLASSES package calc; /** Operations on formulas. * @author Gary T. Leavens */ public interface FormulaType { /** * Evaluate the formula and return its value. */ //@ ensures (* \result is the value of the formula. *); /*@ pure @*/ double evaluate(); /** * Does this formula have a positive value? */ //@ ensures \result <==> evaluate() > 0.0; boolean isPositive(); } ------------------------------------------ Q: If we change FormulaType by adding the isPositive method, without adding an implementation in BinaryFormula, what does that do to BinaryFormula? *** down calls (skip if already discussed) Q: Can we write an implementation of isPositive() in BinaryFormula? ------------------------------------------ DOWN CALLS (TEMPLATE METHOD PATTERN) Def: A method call is a *down call* if it may call a method of a subclass. package calc; /** Binary formulas (like a+b). * @author Gary T. Leavens */ public abstract class BinaryFormula implements FormulaType { /* ... no evaluate() here ... */ /** * Does this formula have a * positive value? */ public boolean isPositive() { return this.evaluate() > 0; } } ------------------------------------------ This is an example of the template method pattern, which is important in frameworks (sets of classes designed to be reused by subclassing). *** method overriding Def: a method declared in a subclass that is also declared in the superclass is an *override* of the superclass's method. ------------------------------------------ METHOD OVERRIDING package calc; /** Binary formulas (like a+b). * @author Gary T. Leavens */ public abstract class BinaryFormula implements FormulaType { /* ... */ // documentation comment and specification inherited public int hashCode() { return super.hashCode() + right; } // documentation comment inherited //@ also //@ ensures \result ==> o instanceof BinaryFormula; //@ ensures \result ==> ((BinaryFormula)o).right == this.right; public boolean equals(Object o) { return super.equals(o) && o instanceof BinaryFormula && ((BinaryFormula)o).right == this.right; } public String toString() { return "r" + left + " " + this.opString() + " r" + right; } /** A string that represents the operation. * This is used in decoding. * @see #toString() */ //@ ensures \result != null; protected abstract String opString(); } ------------------------------------------ Q: Where do we have to define opString? Q: What happens when we call new Diff(1, 2).toString()? Note that toString is called implicitly when a String is needed and an object is present. ** privacy in subclasses ------------------------------------------ PROTECTED PRIVACY LEVEL protected = ------------------------------------------ ... accessible only from (i) code in a class C if C is a subclass of the class where the declaration appears and the reference is to an object of static type C (ii) from classes declared in the same package as the declaration. ------------------------------------------ EXAMPLES package examples; public class Privacy { protected int prot = 2; public final int pub = 3; protected void protM() {} public void pubM() {} } ------------------------------------------ ------------------------------------------ PROTECTED ACCESS Access depends both on the class where the call occurs, and the class of the object. For example... import examples.*; public class Client2 extends Privacy { protected void protM() { super.protM(); prot = 3; } public void pubM() { super.pubM(); } public void testPrivacy() { Client2 o = new Client2(); int i = 3; i = o.prot; o.prot = i; i = o.pub; o.protM(); o.pubM(); // Note: some below is illegal Privacy p = new Privacy(); i = p.prot; p.prot = i; i = p.pub; p.protM(); p.pubM(); } } ------------------------------------------ Q: which parts are illegal? Reil recommends only using protected for methods, never for (writable) data fields. (Heuristic 5.3) ** inheritance vs. composition See Reil's book, ch. 5 ------------------------------------------ COMPOSITION VS. INHERITANCE Def: *composition* means ------------------------------------------ ... building an object from a reference to another object (in a data field). *** when to use inheritance Q: Given that both inheritance and composition allow objects in the subclass to hold information from another type's objects, why use inheritance at all? Without using inheritance: - one doesn't get the behavior (methods) inherited (you have to write code to delegate explicitly) - in many languages one can't use subtype polymorphism (although in Java you can use interfaces to get subtyping without inheritance) ------------------------------------------ WHEN TO USE INHERITANCE ------------------------------------------ ... - to avoid code duplication (by factoring out common declarations and behavior) - to model a specialization hierarchy - to get subtype polymorphism - to avoid explicit case analysis on object types (Heuristic 5.12) or object attributes (Heuristic 5.13) - to abstract from representation details (to hide information) - to construct reusable frameworks (Heuristic 5.19) Q: Which of these have we followed? most except we haven't hidden rep details *** when not to use inheritance ------------------------------------------ WHEN NOT TO USE INHERITANCE ------------------------------------------ ... - when you need to NOP an operation of the superclass (i.e., when you can't make a behavioral subtype, as in our bad example of negation, above) - when there is only one instance of the subclass (Heuristic 5.15) (in that case, just make an object of the class) - to model types that need to change at runtime (instead use objects with attributes that encapsulate the changing state) (Heuristic 5.16) - when objects have optional components (Heuristic 5.18)