3.9 Abstract Classes and InterfacesIn Example 3.14 we declared our Circle class to be part of a package named shapes. Suppose we plan to implement a number of shape classes: Rectangle, Square, Ellipse, Triangle, and so on. We'll give all of these shape classes our two basic area() and circumference() methods. Now, to make it easy to work with an array of shapes, it would be helpful if all our shape classes have a common superclass, Shape. We want Shape to encapsulate whatever features all our shapes have in common. In this case, they have in common the area() and circumference() methods. But our generic Shape class can't actually implement these methods, since it doesn't represent any actual shape. Java handles this case with abstract methods. Abstract MethodsJava lets us define a method without implementing it by making the method abstract. An abstract method has no body; it simply has a signature definition followed by a semicolon. [13] Here are the rules about abstract methods, and the abstract classes that contain them:
That description of the abstract keyword was pretty abstract! Example 3.15 is more concrete. It shows an abstract Shape class and two non-abstract subclasses of it. Example 3.15: An Abstract Class and Subclasses
public abstract class Shape { public abstract double area(); public abstract double circumference(); } class Circle extends Shape { protected double r; protected static final double PI = 3.14159265358979323846; public Circle() { r = 1.0; } public Circle(double r) { this.r = r; } public double area() { return PI * r * r; } public double circumference() { return 2 * PI * r; } public double getRadius() { return r; } } class Rectangle extends Shape { protected double w, h; public Rectangle() { w = 0.0; h = 0.0; } 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); } public double getWidth() { return w; } public double getHeight() { return h; } } Note that the abstract methods in Shape have a semicolon right after their parentheses. There are no curly braces, and no method body is defined. Using the classes defined in Example 3.15, we can now write code like this:
Shape[] shapes = new Shape[3]; // Create an array to hold shapes. shapes[0] = new Circle(2.0); // Fill in the array... shapes[1] = new Rectangle(1.0, 3.0); shapes[2] = new Rectangle(4.0, 2.0); double total_area = 0; for(int i = 0; i < shapes.length; i++) total_area += shapes[i].area(); // Compute the area of the shapes. There are two important points to notice here:
InterfacesLet's extend our shapes package further. Suppose we now want to implement a number of shapes that can be drawn on the screen. We could define an abstract DrawableShape class, and then implement various subclasses of it, such as DrawableCircle, DrawableRectangle, and so on. This would work fine. But suppose we want our drawable shape types to support the area() and circumference() methods. We don't want to have to re-implement these methods, so we'd like to make DrawableCircle a subclass of Circle, for example, and DrawableRectangle a subclass of Rectangle. But classes in Java can only have one superclass. If DrawableCircle extends Circle, then it cannot also extend DrawableShape! [14]
Java's solution to this problem is called an interface. An interface looks a lot like an abstract class, except that it uses the keyword interface instead of the words abstract and class. Example 3.16 shows an interface named Drawable. Example 3.16: An Interface Definition
public interface Drawable { public void setColor(Color c); public void setPosition(double x, double y); public void draw(DrawWindow dw); } While an abstract class may define some abstract methods and some non-abstract methods, all the methods defined within an interface are implicitly abstract. We've omitted the abstract keyword in this example, but it is perfectly legal to use it if you want to belabor the abstractness of interface methods. A further restriction on interfaces is that any variables declared in an interface must be static and final--that is, they must be constants. So what can we do with an interface? Just as a class extends its superclass, it also optionally implements an interface. implements is a Java keyword that can appear in a class declaration following the extends clause. implements should be followed by the name of the interface that the class implements. In order to implement an interface, a class must first declare the interface in an implements clause, and then it must provide an implementation (i.e., a body) for all of the abstract methods of the interface. [15]
Example 3.17 shows how we can define a DrawableRectangle class that extends our Rectangle class and implements the Drawable interface we defined in Example 3.16. The example assumes that a Color class and a DrawWindow class are defined elsewhere, and that DrawWindow knows how to convert floating-point coordinates to pixel coordinates and knows how to draw primitive shapes. Example 3.17: Implementing an Interface
public class DrawableRectangle extends Rectangle implements Drawable { // New instance variables private Color c; private double x, y; // A constructor public DrawableRectangle(double w, double h) { super(w, h); } // Here are implementations of the Drawable methods. // We also inherit all the public methods of Rectangle. 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); } } Using InterfacesSuppose we implement DrawableCircle and DrawableSquare just as we implemented DrawableRectangle in Example 3.17. As we saw earlier, instances of these classes can be treated as instances of the abstract Shape class. They can also be treated as instances of the Drawable interface. Example 3.18 demonstrates this. Example 3.18: Casting Objects to Their Interface Type
Shape[] shapes = new Shape[3]; // Create an array to hold shapes Drawable[] drawables = new Drawable[3]; // and an array to hold drawables. // Create some drawable shapes. DrawableCircle dc = new DrawableCircle(1.1); DrawableSquare ds = new DrawableSquare(2.5); DrawableRectangle dr = new DrawableRectangle(2.3, 4.5); // The shapes can be assigned to both arrays. shapes[0] = dc; drawables[0] = dc; shapes[1] = ds; drawables[1] = ds; shapes[2] = dr; drawables[2] = dr; // Compute total area and draw the shapes by invoking // the Shape and the Drawable abstract methods. double total_area = 0; for(int i = 0; i < shapes.length; i++) { total_area += shapes[i].area(); // Compute the area of the shapes. drawables[i].setPosition(i*10.0, i*10.0); drawables[i].draw(draw_window); // Assume draw_window defined somewhere. } What this example demonstrates is that interfaces are data types in Java, just as classes are, and that when a class implements an interface, instances of that class can be assigned to variables of the interface type. Don't interpret this example to imply that you must assign a DrawableRectangle object to a Drawable variable before you can invoke the draw() method or that you must assign it to a Shape variable before you can invoke the area() method. DrawableRectangle defines draw() and inherits area() from its Rectangle superclass, and so you can always invoke these methods. Implementing Multiple InterfacesSuppose we wanted shapes that could be scaled to be larger or smaller. One way we could do this is by defining a Scalable interface and implementing subclasses of DrawableRectangle and the other classes. To do this, though, the new subclass would have to implement both the Drawable interface and the Scalable interface. This is no problem. You may specify a list of comma-separated interfaces in the implements clause of any class:
public class DrawableScalableRectangle extends DrawableRectangle implements Drawable, Scalable { // The methods of the Scalable interface must be implemented here. } When a class implements more than one interface, it means simply that it must provide an implementation for all of the abstract methods in all of its interfaces. Constants in InterfacesAs we noted above, constants may appear in interface definitions. What does it mean to implement an interface that contains constants? It simply means that the class that implements the interface "inherits" the constants (in a sense) and can use them as if they were defined directly in the class. There is no need to prefix them with the name of the interface, and there is no need to provide an "implementation" of the constants:
class A { static final int CONSTANT1 = 3; } interface B { static final int CONSTANT2 = 4; } class C implements B { void f() { int i = A.CONSTANT1; // Have to use the class name here. int j = CONSTANT2; // No class name here, because we implement } // the interface that defines this constant. } When you have a set of constants used by more than one class within a package (for example, a port number and other protocol constants used by a client and server), it is convenient to define them in an interface that contains no abstract methods. Then, any class that wants to use those constants needs only to declare that it implements the interface. Extending InterfacesInterfaces can have sub-interfaces, just as classes can have subclasses. A sub-interface inherits all the abstract methods and constants of its super-interface, and may define new abstract methods and constants. Interfaces are different from classes in one very important way, however. An interface can extend more than one interface at a time:
public interface Transformable extends Scalable, Rotateable, Reflectable { } public interface DrawingObject extends Drawable, Transformable { } public class Shape implements DrawingObject { ... } An interface that extends more than one interface inherits all the abstract methods and constants from each of those interfaces, and may define its own additional abstract methods and constants. A class that implements such an interface must implement the abstract methods defined in the interface itself as well as all the abstract methods inherited from all the super-interfaces. Marker InterfacesAnother technique that is sometimes useful is to define an interface that is entirely empty. A class can implement this interface to provide additional information about itself. The Cloneable interface in java.lang is an example of this type of "marker interface." It defines no methods, but serves simply to identify the class as one that will allow its internal state to be cloned by the clone() method of the Object class. In Java 1.1, java.io.Serializable is another such marker interface. You can test whether a class implements a marker interface (or any interface) using the instanceof operator. |
|