Transcript chap07

7: Polymorphism
•
•
•
•
•
•
•
•
•
•
Upcasting revisited
Forgetting the object type
The twist
Method-call binding
Producing the right behavior
Extensibility
Overriding vs. overloading
Abstract classes and methods
Constructors and polymorphism
Designing with inheritance
• Exercises
7: Polymorphism
• Polymorphism is the third essential feature of
an object-oriented programming language,
after data abstraction and inheritance.
7: Polymorphism
class Note {
private int value;
private Note(int val) { value = val; }
public static final
Note
MIDDLE_C = new Note(0),
C_SHARP = new Note(1),
B_FLAT = new Note(2);
} // Etc.
class Instrument {
public void play(Note n) {
System.out.println("Instrument
.play()");
}
}
// Wind objects are instruments
// because they have the same interface:
class Wind extends Instrument {
// Redefine interface method:
public void play(Note n) {
System.out.println("Wind.play()");
}
}
public class Music {
public static void tune(Instrument i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // Upcasting
}
}
The twist
The difficulty with Music.java can be seen by running
the program. The output is Wind.play( ). This is
clearly the desired output, but it doesn’t seem to
make sense that it would work that way. Look at the
tune( ) method:
public static void tune(Instrument i) { // ...
i.play(Note.MIDDLE_C);
}
It receives an Instrument reference. So how can the
compiler possibly know that this Instrument
reference points to a Wind in this case and not a
Brass or Stringed? The compiler can’t.
Method-call binding
Connecting a method call to a method body is called binding.
• early binding: When binding is performed before the program
is run (by the compiler and linker)
– You might not have heard the term before because it has never
been an option with procedural languages.
– C compilers have only one kind of method call, and that’s early
binding.
late binding: means that the binding occurs at run-time based on
the type of object.
– Late binding is also called dynamic binding or run-time binding.
– There must be some mechanism to determine the type of the object
at run-time and to call the appropriate method.
– The compiler still doesn’t know the object type.
Producing the right behavior
• The classic example in OOP is the “shape” example.
This is commonly used because it is easy to visualize.
class Shape {
void draw() {}
void erase() {}
}
class Circle extends Shape {
void draw()
{ System.out.println("Circle.draw()"); }
void erase()
{ System.out.println("Circle.erase()"); }
}
class Square extends Shape {
void draw()
{ System.out.println("Square.draw()"); }
void erase()
{ System.out.println("Square.erase()"); }
}
class Triangle extends Shape {
void draw()
{ System.out.println("Triangle.draw()");
} void erase()
{ System.out.println("Triangle.erase()");
}
}
public class Shapes {
public static Shape randShape() {
// Upcasting
switch((int)(Math.random() * 3)) {
default: case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
public static void main(String[] args) {
Shape[] s = new Shape[9];
// Fill up the array with shapes:
for(int i = 0; i < s.length; i++)
s[i] = randShape();
// Make polymorphic method calls:
for(int i = 0; i < s.length; i++)
s[i].draw();
}
} ///:~
Circle.draw()
Triangle.draw()
Circle.draw()
Circle.draw()
Circle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Square.draw()
Exercise
class Shape {
void draw() {}
void erase() {}
void flash() { draw(); erase(); draw(); }
}
class Circle extends Shape {
void draw() { System.out.println("Circle.draw()"); }
void erase() { System.out.println("Circle.erase()"); }
}
class Square extends Shape {
void draw() { System.out.println("Square.draw()"); }
void erase() { System.out.println("Square.erase()"); }
}
class Triangle extends Shape {
void draw() { System.out.println("Triangle.draw()"); }
void erase() { System.out.println("Triangle.erase()"); }
}
========= public class Shapes main
for(int i = 0; i < s.length; i++) s[i]. flash();
Extensibility
•
•
Because of polymorphism, you can add as many
new types as you want to the system without
changing the tune( ) method.
All these new classes work correctly with the old,
unchanged tune( ) method.
import java.util.*;
class Instrument {
public void play()
{ System.out.println("Instrument.play()"); }
public String what() { return "Instrument"; }
public void adjust() {}
}
=====================
class Wind extends Instrument {
public void play()
{ System.out.println("Wind.play()"); }
public String what() { return "Wind"; }
public void adjust() {}
}
class Percussion extends Instrument {
public void play()
{ System.out.println("Percussion.play()"); }
public String what() { return "Percussion"; }
public void adjust() {}
}
class Stringed extends Instrument { ……}
class Woodwind extends Wind { ……}
class Brass extends Wind { ……}
==========================
public class Music3 {
// Doesn't care about type, so new types
static void tune(Instrument i) { // ...
i.play(); }
static void tuneAll(Instrument[] e) {
for(int i = 0; i < e.length; i++)
tune(e[i]);
}
public static void main(String[] args) {
Instrument[] orchestra = new
Instrument[5];
int i = 0;
// Upcasting during addition to the array:
orchestra[i++] = new Wind();
orchestra[i++] = new Percussion();
orchestra[i++] = new Stringed();
orchestra[i++] = new Brass();
orchestra[i++] = new Woodwind();
tuneAll(orchestra);
}
} ///:~
Overriding vs. overloading
class NoteX {
public static final int MIDDLE_C = 0, C_SHARP = 1, C_FLAT = 2;
}
class InstrumentX {
public void play(int NoteX) { System.out.println("InstrumentX.play()");
}
}
class WindX extends InstrumentX {
// OOPS! Changes the method interface:
public void play(NoteX n) { System.out.println("WindX.play(NoteX n)"); }
}
public class WindError {
public static void tune(InstrumentX i) { i.play(NoteX.MIDDLE_C); }
public static void main(String[] args) {
WindX flute = new WindX();
tune(flute); // Not the desired behavior!
}
} ///:~
Abstract classes and methods
•
In above examples, the methods in the base class
Instrument were always “dummy” methods.
–
–
•
Java provides a mechanism for doing this called the abstract
method. A class containing abstract method is a abstract class.
–
–
•
•
•
If these methods are ever called, you’ve done something wrong.
The intent of Instrument is to create a common interface for all
the classes derived from it.
abstract method is a method that is incomplete; it has only a
declaration and no method body.
abstract void f();
It cannot create an object of an abstract class
If you inherit from an abstract class and you want to make
objects of the new type, you must override (redefine) all the
abstract methods in the base class.
If you don’t (and you may choose not to), then the derived
class is also abstract.
Abstract classes and methods
import java.util.*;
abstract class Instrument {
int i; // storage allocated for each
public abstract void play();
public String what() { return "Instrument"; }
public abstract void adjust();
}
class Wind extends Instrument {
public void play() {
System.out.println("Wind.play()");
}
public String what() { return "Wind"; }
public void adjust() {}
}
Designing with inheritance
•
•
•
Once you learn about polymorphism, it can seem
that everything ought to be inherited because
polymorphism is such a clever tool. This can
burden your designs;
A better approach is to choose composition first,
when it’s not obvious which one you should use.
A Stage object contains a reference to an Actor,
which is initialized to a HappyActor object. At
run-time, a reference for a SadActor object can be
substituted in a and then the behavior produced by
go( ) changes.
–
Thus you gain dynamic flexibility at run-time. (This is also
called the State Pattern. )
Designing with inheritance
// Dynamically changing the behavior of // an object via composition.
abstract class Actor {
abstract void act();
}
class HappyActor extends Actor {
public void act() { System.out.println("HappyActor"); }
}
class SadActor extends Actor {
public void act() { System.out.println("SadActor"); }
}
class Stage {
Actor a = new HappyActor();
void change() { a = new SadActor(); }
void go() { a.act(); }
}
public class Transmogrify {
public static void main(String[] args) {
Stage s = new Stage();
s.go(); // Prints "HappyActor"
s.change(); s.go(); // Prints "SadActor"
}
} ///:~
Downcasting and run-time type identification
While upcasting is a useful and sensible approach (depending on
the situation) it has a drawback. The extended part of the
interface in the derived class is not available from the base
class, so once you upcast you can’t call the new methods:
Downcasting and run-time type identification
import java.util.*;
class Useful {
public void f() {}
public void g() {}
}
class MoreUseful
extends Useful {
public void f() {}
public void g() {}
public void u() {}
public void v() {}
public void w() {}
}
import java.util.*;
public class RTTI {
public static void main(String[] args) {
Useful[] x = { new Useful(), new
MoreUseful() };
x[0].f();
x[1].g();
// Compile-time: method not found
in Useful:
//! x[1].u();
// Downcast/RTTI
((MoreUseful)x[1]).u();
// Exception thrown
((MoreUseful)x[0]).u();
} } ///:~
Downcasting and run-time type identification
•
Since you lose the specific type information via an upcast
(moving up the inheritance hierarchy), you can use a
downcast to retrieve the type information (moving down the
inheritance hierarchy )
•
An upcast is always safe;
–
•
the base class cannot have a bigger interface than the derived
class, therefore every message you send through the base class
interface is guaranteed to be accepted.
But a downcast is unsafe,
–
•
A shape (for example) may be a circle or a triangle or a square
or some other type.
In Java every cast is checked!
–
–
At run-time this cast is checked to ensure that it is in fact the
type you think it is.
If it isn’t, you get a ClassCastException. This act of checking
types at run-time is called run-time type identification (RTTI).