Java in a Nutshell

Previous Chapter 3
Classes and Objects in Java
Next
 

3.9 Abstract Classes and Interfaces

In 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 Methods

Java 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:

[13] An abstract method in Java is something like a "pure virtual function" in C++ (i.e., a virtual function that is declared = 0). In C++, a class that contains a pure virtual function is called an "abstract class" and may not be instantiated. The same is true of Java classes that contain abstract methods.

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:

Interfaces

Let'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]

[14] C++ allows classes to have more than one superclass, using a technique known as "multiple inheritance." Multiple inheritance opens up a can of worms, so Java replaces it with what many believe is a more elegant solution.

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]

[15] This is the real difference between multiple inheritance in C++ and interfaces in Java. In C++, a class can inherit method implementations from more than one superclass. In Java, a class can inherit actual implementations only from one superclass. It can inherit additional abstract methods from interfaces, but it must provide its own implementation of those methods. It is rare, however, to actually be able to use C++ multiple inheritance to inherit useful, non-trivial implementations from more than one class. The elegance and simplicity of Java's interface more than compensate for the inability to inherit implementations from more than one class.

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 Interfaces

Suppose 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 Interfaces

Suppose 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 Interfaces

As 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 Interfaces

Interfaces 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 Interfaces

Another 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.


Previous Home Next
Data Hiding and Encapsulation Book Index C++ Features Not Found in Java

Java in a Nutshell Java Language Reference Java AWT Java Fundamental Classes Exploring Java
This HTML Help has been published using the chm2web software.