230 likes | 323 Views
COSC3557: Object-Oriented Programming. Haibin Zhu, Ph. D. As sociate Professor of CS, Nipissing University. Lecture 7. Overriding Ref. : Chapter 16. Difference from Overloading. Like overloading, there are two distinct methods with the same name. But there are differences:
E N D
COSC3557: Object-Oriented Programming Haibin Zhu, Ph. D. Associate Professor of CS, Nipissing University
Lecture 7 • Overriding • Ref. : Chapter 16
Difference from Overloading • Like overloading, there are two distinct methods with the same name. But there are differences: • Overriding only occurs in the context of the parent/child relationship • The type signatures must match • Overridden methods are sometimes combined together • Overriding is resolved at run-time, not at compile time.
Overriding Comparisons in Smalltalk • An interesting example of overriding is found in class Magnitude in Smalltalk. <= arg ^ self < arg or: [ self = arg ] >= arg ^ arg <= self < arg ^ self <= arg and: [ self ~= arg ] > arg ^ arg < self = arg ^ self == arg ~= arg ^ (self == arg) not • Notice how these definitions are circular.
Overridden Relations • Child classes need only override one method (for example <) to get effect of all relational. • Overridden in class Integer to mean integer less than. • Overridden in class Char to be ASCII ordering sequence. • Overridden in class String to mean lexical-graphic ordering. • Overridden in class Point to mean lower-left quadrant.
Notating Overriding • Generally, for instances, overriding occurs automatically. • For references (or pointers), in some languages (Smalltalk, Java) overriding occurs automatically when a child class redefines a method with the same name and type signature. In some languages (C++) overriding will only occur if the parent class has declared the method in some special way (example, keyword virtual). • In some languages (Object Pascal) overriding will only occur if the child class declares the method in some special way (example, keyword override). • In some languages (C#, Delphi) overriding will only occur if both the parent and the child class declare the method in some special way. • class Parent { // C# example • public virtual int example (int a) { ... } • } • class Child : Parent { • public override int example (int a) { ... } • }
Replacement and Refinement • There are actually two different ways that overriding can be handled: • A replacement totally and completely replaces the code in the parent class the code in the child class. • A refinement executes the code in the parent class, and adds to it the code in the child class. • Most languages use both types of semantics in different situations. Constructors, for example, almost always use refinement.
Reasons to use Replacement • There are a number of reasons to use replacement of methods. • The method in the parent class is abstract, it must be replaced. • The method in the parent class is a default method, not appropriate for all situations. • The method in the parent can be more efficiently executed in the child.
Overriding a Default Method • Here is another from Smalltalk. Class Number has child classes Integer, Fraction and Float. Method in class Number is defined as follows: • "class Number" • sqrt • ^ self asFloat sqrt • The method in class Float clearly must perform something different, in this case actually computing the square root. The parent class has a method that will work in most, but not all, child classes.
Overriding for Optimization • Here is an example where a child class can do the same action more efficiently than the parent class. Class Boolean has child classes True and False, and defines the following methods: • "class Boolean" • & right • self ifTrue: [ right ifTrue: [ ^ true ] ]. • ^ false • | right • self ifTrue: [ ^ true ]. • right ifTrue: [ ^ true ]. • ^ false • These very general algorithms will work for either true or false values.
More Efficient Versions in class True • In class True we know the left argument is true, and therefore can make more efficient algorithms. • " class True " • & right • ^ right • | right • ^ true • Similar code in class False. These are faster than the code in the parent class, but have the same effect.
Downside of Replacement • The down side of replacement semantics is that there is no guarantee that the child class will have any meaning at all similar to the parent class. • For example, a child class could redefine sqrt to compute the cube root of its argument. • This goes back to the difference between subclasses and subtypes. • A refinement makes this difficult to do, since whatever the parent does is guaranteed to be part of the child. This is why most languages use refinement semantics for constructors.
Simulating Refinement with Replacement • In most languages the most important features of a refinement can be simulated, even if the language uses replacement. • void Parent::example (int a) { • cout << "in parent code\n"; • } • void Child::example (int a) { • Parent::example(12); // do parent code • cout << "in child code\n"; // then child code • }
Constructors use Refinement • In most languages that have constructors, a constructor will always use refinement. • This guarantees that whatever initialization the parent class performs will always be included as part of the initialization of the child class.
Overriding versus Shadowing • It is common in programming languages for one declaration of a variable to shadow a previous variable of the same name: • class Silly { • private int x; // an instance variable named x • public void example (int x) { // x shadows instance variable • int a = x+1; • while (a > 3) { • int x = 1; // local variable shadows parameter • a = a - x; • } • } • }
Shadowing of Instance Variables in Java • Java allows instance variables to be redefined, and uses shadowing. • class Parent • { public int x = 12; • public int get() {return x;}; • } • class Child extends Parent • { public int x = 42; // shadows variable from parent class • public int get() {return x;}; • } • public class override • { public static void main(String args[]) • { Parent p = new Parent(); • Child c= new Child(); • System.out.println("p.x is "+p.x+" and c.x is " +c.x); • p = c; • System.out.println("p.x is "+p.x+" and c.x is " +c.x); • System.out.println("p.x is "+p.get()+" and c.x is " +c.get()); • } • }//override.java
Shadowing Methods • Many of those languages that require the virtual keyword in the parent class will use shadowing if it is omitted: • class Parent { • public: // note, no virtual keyword here • void example () { cout << "Parent" << endl; } • }; • class Child : public Parent { • public: • void example () { cout << "Child" << endl; } • }; • Parent * p = new Parent(); • p->example() //Parent • Child * c = new Child(); • c->example() //Child • p = c; // be careful here! • p->example() //Parent • //override.cpp
Covariance and Contravariance • Frequently it seems like it would be nice if when a method is overridden we could change the argument types or return types. A change that moves down the inheritance hierarchy, making it more specific, is said to be covariant. A change that moves up the inheritance hierarchy is said to be contravariant. • class Parent { • void test (covar : Mammal, contravar : Mammal) : boolean • } • class Child extends Parent { • void test (covar : Cat, contravar : Animal) : boolean • } • While appealing, this idea runs into trouble with the principle of substitution. • Parent aValue = new Child(); • aValue.text(new Dog(), new Mammal()); // is this legal??
Contravariant Return Types • To see how a contravariant change can get you into trouble, consider changing the return types: • class Parent { • Mammal test ( ) { • return new Cat(); • } • } • class Child extends Parent { • Animal test () { • return new Bird(); • } • } • Parent aParent = new Child(); • Mammal result = aParent.test(); // is this legal? • //Covar.java • Most languages subscribe to the novariance rule: no change in type signatures.
A Safe Variance Change • C++ allows the following type of change in signature: • class Parent { • public: • Parent * clone () { return new Parent(); } • }; • class Child : public Parent { • public: • Child * clone () { return new Child(); } • }; • //sig.cpp • No type errors can result from this change.
Summary • An override occurs when a method in the child classes uses the same name and type signature as a method in the parent class. • Unlike overloading, overriding is resolved at run-time. • There are two possible means for an overriding, replacement and refinement. • A name can shadow another name. Some languages permit both shadowing and overriding. Shadowing is resolved at compile time. • A change in the type signature can be covariant or contravariant, if it moves down or up the type hierarchy. The semantics of both types of change can be subtle.