8:
Interfaces & Inner Classes
Interfaces and inner classes provide more sophisticated ways to organize and control the objects in your system.
C++, for example, does not contain such mechanisms, although the clever programmer may simulate them. The fact that they exist in Java indicates that they were considered important enough to provide direct support through language keywords.
In Chapter 7 you learned about the abstract keyword, which allows you to create one or more methods in a class that have no definitions—you provide part of the interface without providing a corresponding implementation, which is created by inheritors. The interface keyword produces a completely abstract class, one that provides no implementation at all. You’ll learn that the interface is more than just an abstract class taken to the extreme, since it allows you to perform a variation on C++’s “multiple inheritance” by creating a class that can be upcast to more than one base type.
At first, inner classes look like a simple code-hiding mechanism: you place classes inside other classes. You’ll learn, however, that the inner class does more than that—it knows about and can communicate with the surrounding class—and that the kind of code you can write with inner classes is more elegant and clear, although it is a new concept to most. It takes some time to become comfortable with design using inner classes.
Interfaces
The interface keyword takes the abstract concept one step further. You could think of it as a “pure” abstract class. It allows the creator to establish the form for a class: method names, argument lists, and return types, but no method bodies. An interface can also contain fields, but these are implicitly static and final. An interface provides only a form, but no implementation.
An interface says, “This is what all classes that implement this particular interface will look like.” Thus, any code that uses a particular interface knows what methods might be called for that interface, and that’s all. So the interface is used to establish a “protocol” between classes. (Some object-oriented programming languages have a keyword called protocol to do the same thing.)
To create an interface, use the interface keyword instead of the class keyword. Like a class, you can add the public keyword before the interface keyword (but only if that interface is defined in a file of the same name) or leave it off to give package access, so that it is only usable within the same package.
To make a class that conforms to a particular interface (or group of interfaces), use the implements keyword, which says, “The interface is what it looks like, but now I’m going to say how it works.” Other than that, it looks like inheritance. The diagram for the instrument example shows this:
You can see from the Woodwind and Brass classes that once you’ve implemented an interface, that implementation becomes an ordinary class that can be extended in the regular way.
You can choose to explicitly declare the method declarations in an interface as public, but they are public even if you don’t say it. So when you implement an interface, the methods from the interface must be defined as public. Otherwise, they would default to package access, and you’d be reducing the accessibility of a method during inheritance, which is not allowed by the Java compiler.
You can see this in the modified version of the Instrument example. Note that every method in the interface is strictly a declaration, which is the only thing the compiler allows. In addition, none of the methods in Instrument are declared as public, but they’re automatically public anyway:
//: c08:music5:Music5.java
// Interfaces.
package c08.music5;
import com.bruceeckel.simpletest.*;
import c07.music.Note;
interface Instrument {
// Compile-time constant:
int I = 5; // static & final
// Cannot have method definitions:
void play(Note n); // Automatically public
String what();
void adjust();
}
class Wind implements Instrument {
public void play(Note n) {
System.out.println("Wind.play() " + n);
}
public String what() { return "Wind"; }
public void adjust() {}
}
class Percussion implements Instrument {
public void play(Note n) {
System.out.println("Percussion.play() " + n);
}
public String what() { return "Percussion"; }
public void adjust() {}
}
class Stringed implements Instrument {
public void play(Note n) {
System.out.println("Stringed.play() " + n);
}
public String what() { return "Stringed"; }
public void adjust() {}
}
class Brass extends Wind {
public void play(Note n) {
System.out.println("Brass.play() " + n);
}
public void adjust() {
System.out.println("Brass.adjust()");
}
}
class Woodwind extends Wind {
public void play(Note n) {
System.out.println("Woodwind.play() " + n);
}
public String what() { return "Woodwind"; }
}
public class Music5 {
private static Test monitor = new Test();
// Doesn't care about type, so new types
// added to the system still work right:
static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
static void tuneAll(Instrument[] e) {
for(int i = 0; i < e.length; i++)
tune(e[i]);
}
public static void main(String[] args) {
// Upcasting during addition to the array:
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
monitor.expect(new String[] {
"Wind.play() Middle C",
"Percussion.play() Middle C",
"Stringed.play() Middle C",
"Brass.play() Middle C",
"Woodwind.play() Middle C"
});
}
} ///:~
The rest of the code works the same. It doesn’t matter if you are upcasting to a “regular” class called Instrument, an abstract class called Instrument, or to an interface called Instrument. The behavior is the same. In fact, you can see in the tune( ) method that there isn’t any evidence about whether Instrument is a “regular” class, an abstract class, or an interface. This is the intent: Each approach gives the programmer different control over the way objects are created and used.