|
While a C++ class can have more than one superclass, a Java class can have exactly one superclass. As we will discuss in the next chapter, multiple superclasses can be a powerful tool in the hands of experienced programmers. But, they can also be a source of a lot of programming difficulties for the unwary.
While the "exactly one superclass" rule of Java does avoid the pitfalls associated with multiple inheritance, it can be excessively restrictive. Java gets around this limitation by using interfaces. An interface is a class that consists solely of abstract methods. Since an interface is not allowed to contain any implementation code, any implementation code inherited by a derived Java class can only come from its sole superclass. This is referred to as Java has a single inheritance of implementation. On the other hand, a Java class can implement an arbitrary number of interfaces. This is referred to as a Java class has multiple inheritance for interfaces.
To illustrate the idea of interfaces, we will provide the Shape hierarchy of the previous section with an interface so that the shapes can be drawn on a terminal screen. If we did not have to observe the "exactly one superclass" rule of Java, we could create a new abstract class Drawable where we could place the headers of all the methods that would be needed for drawing a shape in a window on a screen. We could then define a DrawableRectangle class that could inherit from the Rectangle and the Drawable classes, with the idea that the objects of type DrawableRectangle would know how to draw themselves (upon the invocation of a suitable "draw" method) on the screen. We can do this in C++, but not in Java on account of the quote> rule.
So in Java, we resort to declaring an interface Drawable by[15]
interface Drawable { public void setColor(Color c); public void setPosition(double x, double y); public void draw(DrawWindow dw); }
All the methods declared in an interface are implicitly abstract. We have omitted the keyword abstract in this example, but it would be legal to use it to make explicit the abstract nature of these method declarations. We can now define a class DrawableRectangle in the following manner:
class DrawableRectangle extends Rectangle implements Drawable { private Color c; private double x, y; public DrawableRectangle (double w, double h) { super( w, h); } //here are the implementations of the methods //inherited from the interface Drawable: public void setColor(Color c) { this.c = c; } public void setPosition(double x, double y) { this.x = x; this.y = y; } public void draw(DrawWindow dw) { dw.drawRect( x, y, w, h, c); } }
We can define a class DrawableCircle in a similar manner. The resulting class hierarchy will look like what's shown in Figure 15.12 where the inheritance from classes is shown by solid lines and that from interfaces by dashed lines.
What's interesting is that DrawableRectangle objects are not only instances of the class Shape, but also instances of the interface Drawable. In other words, a DrawableRectangle IsA Shape object, and, at the same time, IsA Drawable object. This implies that the hierarchy shown will display polymorphic behavior not only with respect to the methods declared in the Shape class, but also the methods declared in the Drawable interface. To illustrate this notion, let's now declare two arrays, one of type Shape and other of type Drawable:
Figure 15.12
Shape[] shapes = new Shape[3]; Drawable[] drawables = new Drawable[3];
and then let's create the following three objects
DrawableCircle dc = new DrawableCircle(1.1); DrawableRectangle ds = new DrawableRectangle(2.5, 3.2); DrawableRectangle dr = new DrawableRectangle(2.3, 4.5);
These objects can be assigned to either of the two arrays simply because each of the objects is of the two types declared for the array elements:
shapes[0] = dc; shapes[1] = ds; shapes[2] = dr; drawables[0] = dc; drawables[1] = ds; drawables[2] = dr;
Polymorphism with respect to the methods of both the Shape class and the Drawable interface may now be demonstrated by a code fragment like the following:
double total_area = 0; for (int i = 0; i < shapes.length; i++) { total_area += shapes [i].area(); drawables[i].setPosition(i*10.0, i*10.0); drawables[i].draw(draw_window); }
The above example shows that an interface defines a new type in Java, just as a class does, and that when a class implements an interface, instances of that class can be assigned to variables of the interface type. The following source code illustrates the these concepts:
//MultiPolymorphism. Java abstract class Shape { abstract protected double area(); abstract protected double circumference (); } class Circle extends Shape { protected double r; protected static double PI = 3.14159; public Circle (double r) { this.r = r; } public double area() { return PI*r*r;} public double circumference () { return 2 * PI * r; } } class Rectangle extends Shape { double w, h; public Rectangle (double w, double h) { this.w = w; this.h = h; } public double area() { return w * h; } public double circumference () { return 2 * (w + h); } } interface Drawable { public void setColor(Color c); public void setPosition(double x, double y); public void draw(DrawWindow dw); } class DrawableRectangle extends Rectangle implements Drawable { private Color c; private double x, y; public DrawableRectangle (double w, double h) { super(w, h); } // Implementations of the methods inherited from the interface: public void setColor(Color c) { this.c = c; } public void setPosition(double x, double y) { this.x = x; this.y = y; } public void draw(DrawWindow dw) { dw.drawRect( x, y, w, h, c); } } class DrawableCircle extends Circle implements Drawable { private Color c; private double x, y; public DrawableCircle(double rad) { super(rad); } public void setColor(Color c) { this.c = c; } public void setPosition( double x, double y) { this.x = x; this.y = y; } public void draw(DrawWindow dw) { dw.drawCircle(x, y, r, c); } } class Color { int R, G, B; } class DrawWindow { public DrawWindow() {}; public void drawRect(double x, double y, double width, double height, Color col) { System.out.println( "Code for drawing a rect needs to be invoked"); //(A) } public void drawCircle( double x, double y, double radius, Color col) { System.out.println( "Code for drawing a circle needs to be invoked"); //(B) } } class Test { public static void main(String[] args) { Shape[] shapes = new Shape[3]; Drawable[] drawables = new Drawable[3]; DrawableCircle dc = new DrawableCircle(1.1); DrawableRectangle dr1 = new DrawableRectangle( 2.5, 3.5); DrawableRectangle dr2 = new DrawableRectangle( 2.3, 4.5); shapes[0] = dc; shapes [1] = dr1; shapes[2] = dr2; drawables[0] = dc; drawables[1] = dr1; drawables[2] = dr2; int total_area = 0; DrawWindow dw = new DrawWindow(); for (int i = 0; i < shapes.length; i++) { total_area += shapes[i].area(); drawables[i].setPosition(i*10.0, i*10.0); //(C) drawables[i],draw(dw); //(D) } System.out.println("Total area = " + total_area); //(E) } }
The output produced by the statements in lines (A), (B), and (E) of this program is:
Code for drawing a circle needs to be invoked Code for drawing a rect needs to be invoked Code for drawing a rect needs to be invoked Total area = 21
As mentioned earlier, a class can implement more than one interface. Continuing with the Shape example, suppose we also want to scale the shapes to make them larger or smaller prior to drawing them. We could construct another interface, Scalable, for representing this specialized behavior:
interface Scalable { public Shape scaleTransform(); }
We could now define a class DrawScalableRectangle by extending Rectangle and implementing the Drawable and Scalable interfaces:
class DrawableScalableRectangle extends Rectangle implements Drawable, Scalable { // the methods of the Drawable and Scalable interfaces // must be implemented here }
When a class implements more than one interface, it must provide implementations for all the abstract methods declared in all of its interfaces.
Just as a class can extend another class, an interface can also extend another interface. In the Shape hierarchy example we presented earlier, we could extend the Drawable interface in the following manner:
interface Drawable { public void setColor(Color c); public void setPosition(double x, double y); public void draw(DrawWindow dw); } interface DrawScalable extends Drawable { public void drawScaledShape(int scaleFactor, DrawWindow dw); }
Now we can create a class DrawScalableRectangle by
class DrawScalableRectangle extends Rectangle implements DrawScalable { private Color c; private double x, y; public DrawScalableRectangle (double w, double h) { super(w, h); } // Implementations of the methods inherited // from the interface: public void setColor(Color c) { this.c = c; } public void setPosition(double x, double y) { this.x = x; this.y = y; } public void draw (DrawWindow dw) {dw.drawRect(x, y, w, h, c);} public void drawScaledShape (int scaleFactor, DrawWindow dw){ dw.drawScaledRect(x, y, w, h, c, scaleFactor); } }
This class must evidently implement all the abstracts methods of DrawScalable, some of which will be the inherited abstract methods of Drawable. In the following source code, we have shown this example more fully:
//ExtendedInterface.java abstract class Shape { abstract protected double area(); abstract protected double circumference (); } class Circle extends Shape { protected double r; protected static double PI = 3.14159; public Circle (double r) { this.r = r; } public double area() { return PI*r*r; } public double circumference () { return 2 * PI * r; } } class Rectangle extends Shape { double w, h; public Rectangle(double w, double h) { this.w = w; this.h = h; } public double area() { return w * h; } public double circumference() { return 2 * (w + h); } } interface Drawable { public void setColor(Color c); public void setPosition(double x, double y); public void draw(DrawWindow dw); } interface DrawScalable extends Drawable { public void drawScaledShape(int scaleFactor, DrawWindow dw); } class DrawScalableRectangle extends Rectangle implements DrawScalable { private Color c; private double x, y; public DrawScalableRectangle(double w, double h) { super(w, h); } //Implementations of the methods inherited from the interface: public void setColor( Color c) { this.c = c; } public void setPosition( double x, double y) { this.x = x; this.y = y; } public void draw(DrawWindow dw) { dw.drawRect(x, y, w, h, c); } public void drawScaledShape( int scaleFactor, DrawWindow dw) { dw.drawScaledRect(x, y, w, h, c, scaleFactor); } } class DrawScalableCircle extends Circle implements DrawScalable { private Color c; private double x, y; public DrawScalableCircle ( double rad ) { super( rad ); } public void setColor(Color c) {this.c = c;} public void setPosition( double x, double y ) { this.x = x; this.y = y; } public void draw( DrawWindow dw ) { dw.drawCircle( x, y, r, c ); } public void drawScaledShape( int scaleFactor, DrawWindow dw ) { dw.drawScaledCircle( x, y, r, c, scaleFactor ); } } class Color { int R, G, B; } class DrawWindow { public DrawWindow() {}; public void drawRect( double x, double y, double width, double height, Color col ) { System.out.println( //(A) "Code for drawing a rect needs to be invoked" ); } public void drawScaledRect( double x, double y, double width, double height, Color col, int scale ){ System.out.println( //(B) "Code for drawing a scaled rect needs to be invoked" ); } public void drawCircle( double x, double y, double radius, Color col ) { System.out.println( //(C) "Code for drawing a circle needs to be invoked" ); } public void drawScaledCircle( double x, double y, double radius, Color col, int scale ){ System.out.println( //(D) "Code for drawing a scaled circle needs to be invoked" ); } } class Test { public static void main( String[] args ) { Shape[] shapes = new Shape[3]; DrawScalable[] drawScalables = new DrawScalable[3]; DrawScalableCircle dc = new DrawScalableCircle( 1.1 ); DrawScalableRectangle dr1 = new DrawScalableRectangle( 2.5, 3.5 ); DrawScalableRectangle dr2 = new DrawScalableRectangle( 2.3, 4.5 ); shapes[0] = dc; shapes[1] = dr1; shapes[2] = dr2; drawScalables[0] = dc; drawScalables[1] = dr1; drawScalables[2] = dr2; int total_area = 0; DrawWindow dw = new DrawWindow(); for (int i = 0; i < shapes.length; i++ ) { total_area += shapes[i].area(); drawScalables[i].setPosition( i*10.0, i*10.0 ); drawScalables[i].drawScaledShape( 2, dw ); } System.out.println("Total area = " + total_area); //(E) } }
The output of this program, produced by the statements in lines (B), (D), and (E), is
Code for drawing a scaled circle needs to be invoked Code for drawing a scaled rect needs to be invoked Code for drawing a scaled rect needs to be invoked Total area = 21
It is not infrequently the case that a program needs a set of constants for its execution. Java allows for convenient packaging of related constants in interfaces. When a class implements such an interface, the constants become locally available in that class, as if they were defined right there. Constants being constants must be declared "static final." Declaring them "static" makes them available to all the objects of that type and declaring them "final" causes them to be read only.
Instead of placing constants in interfaces, we could also place them inside classes, but then we would have to use the name of that class as a prefix to access the constants. For example, we could package the constant PI inside a class by saying:
class A { public static final double PI = 3.14159; }
In order to access this constant inside another class, we would have to use the class name A as a prefix as in
class Test { //.... void foo() { double x = A. PI; //.... } }
On the other hand, if we packaged the constant in an interface as shown below:
interface A { public static final double PI = 3.14159; }
now a class that implements A can treat the constant PI as if it is locally defined:
class Test implements A { //.... void foo() { double x = PI; } }
[15]The parameter names, such as c, x, y, and so on, in the example shown must be specified even though they do not serve any real purpose except for making the code more readable.
|