The Six Roles of the Interface

Newcomers to the Java language often experience confusion. Their bafflement is largely due to Java's palette of exotic language features, such as generics and lambdas. However, even simpler features such as interfaces can be puzzling.

Recently, I confronted a question on why Java supports interfaces (via interface and implements keywords). When I began to learn Java in the 1990s, this question was often answered by stating that interfaces get around Java's lack of support for multiple implementation inheritance (child classes inheriting from multiple parent classes). However, interfaces serve as much more than a kludge. In this post, I present the six roles that interfaces play in the Java language.

The interface keyword is overloaded for use in declaring annotation types. For example, Listing 1 presents a simple Stub annotation type.

Listing 1:

Stub describes a category of annotations (annotation type instances) that denote unfinished types and methods. Its declaration begins with a header consisting of @ followed by the interface keyword, followed by its name.

This annotation type declares three elements, which you can think of as method headers:

An element returns whatever value is assigned to it by an annotation. If the element isn't specified, its default value (following the default keyword in the declaration) is returned.

Listing 2 demonstrates Stub in the context of an unfinished ContactMgr class; the class and its solitary method have been annotated with @Stub annotations.

Listing 2:

An annotation type instance begins with @, which is followed by the annotation type name. Here, the first @Stub annotation identifies itself as number 1 with a due date of December 31, 2016. The developer responsible for filling in the stub has not yet been assigned. In contrast, the second @Stub annotation identifies itself as number 2 with a due date of June 31, 2016. The developer responsible for filling in the stub is identified as Marty.

Annotations must be processed to be of any use. (Stub is annotated @Retention(RetentionPolicy.RUNTIME) so that it can be processed.) Listing 3 presents a StubFinder application that reports a class's @Stub annotations.

Listing 3:

Listing 3's main() method uses Java's Reflection API to retrieve all @Stub annotations that prefix a class declaration as well as its method declarations.

Compile Listings 1 through 3, as follows:

Run the resulting application, as follows:

You should observe the following output:

You might argue that annotation types and their annotations have nothing to do with interfaces. After all, class declarations and the implements keyword aren't present. However, I would disagree with this conclusion.

@interface is similar to class in that it introduces a type. Its elements are methods that are implemented (behind the scenes) to return values. Elements with default values return values even when not present in annotations, which are similar to objects. Nondefault elements must always be present in an annotation and must be declared to return a value. Therefore, it's as if a class has been declared and that the class implements an interface's methods.

Different classes may offer a common capability. For example, the java.nio.CharBuffer, javax.swing.text.Segment, java.lang.String, java.lang.StringBuffer, and java.lang.StringBuilder classes provide access to readable sequences of char values.

When classes offer a common capability, an interface to this capability can be extracted for reuse. For example, an interface to the "readable sequence of char values" capability has been extracted into the java.lang.CharSequence interface. CharSequence provides uniform, read-only access to many different kinds of char sequences.

Suppose you were asked to write a small application that counts the number of occurrences of each kind of lowercase letter in CharBuffer, String, and StringBuffer objects. After some thought, you might come up with Listing 4. (I would typically avoid culturally-biased expressions such as ch - 'a', but I want to keep the example simple.)

Listing 4: (version 1)

Listing 4 presents three different analyze methods for recording the number of lowercase letter occurrences and outputting this statistic. Although the String and StringBuffer variants are practically identical (and you might be tempted to create a single method for both), the CharBuffer variant differs more significantly.

Listing 4 reveals a lot of duplicate code, which leads to a larger classfile than is necessary. You could accomplish the same statistical objective by working with the CharSequence interface. Listing 5 presents an alternate version of the frequency application that's based on CharSequence.

Listing 5: (version 2)

Listing 5 reveals a much simpler application, which is due to codifying analyze() to receive a CharSequence argument. Because each of String, StringBuffer, and CharBuffer implements CharSequence, it's legal to pass instances of these types to analyze().

To sum up, the second role of an interface is to describe an implementation-independent capability. By coding to an interface (such as CharSequence) instead of to a class (such as String, StringBuffer, or CharBuffer), you avoid duplicate code and generate smaller classfiles. In this case, I achieved a reduction of more than 50%.

Java 8 introduced us to the extremely useful lambda language feature and Streams API (with a focus on what computation should be performed rather than on how it should be performed). Lambdas and Streams make it much easier for developers to introduce parallelism into their applications. Unfortunately, the Java Collections Framework could not leverage these capabilities without needing an extensive rewrite.

To quickly enhance collections for use as stream sources and destinations, support for default methods (also known as extension methods), which are non-static methods whose headers are prefixed with the default keyword and which supply code bodies, was added to Java's interface feature. Default methods belong to interfaces; they're not implemented (but can be overridden) by classes that implement interfaces. Also, they can be invoked via object references.

Once default methods became part of the language, the following methods were added to the java.util.Collection interface, to provide a bridge between collections and streams:

Suppose you've declared the following java.util.List variable and assignment expression:

You would traditionally iterate over this collection, as follows:

You can replace this external iteration, which focuses on how to perform a computation, with Streams-based internal iteration, which focuses on what computation to perform, as follows:

Here, and innerPlanets.parallelStream() return sequential and parallel streams to the previously created List source. Chained to the returned Stream references is forEach(System.out::println), which iterates over the stream's objects and invokes System.out.println() (identified by the System.out::println method reference) for each object to output its string representation to the standard output stream.

Default methods can make code more readable. For example, the java.util.Collections class declares a <T> void sort(List<T> list, Comparator< super T> c) static method for sorting a list's contents subject to the specified comparator. Java 8 added a default void sort(Comparator< super E> c) method to the List interface so you can write the more readable myList.sort(comparator); instead of Collections.sort(myList, comparator);.

The default method role offered by interfaces has given new life to the Java Collections Framework. You might consider this role for your own legacy interface-based libraries.

Before Java 5 introduced the static imports language feature and enums, interfaces were widely used as constant repositories. A somewhat lazy developer would use an interface instead of a class as the basis for an enumerated type, to avoid having to include a classname prefix when specifying a constant in the class that implements the interface. Consider Listing 6's Directions constant interface.

Listing 6: (version 1)

Now, consider Listing 7's Compass class.

Listing 7: (version 1)

The problem with the constant interface is that it causes maintenance headaches. To preserve binary compatibility, the class must always implement the interface, even when the class no longer requires the constants. Also, the presence of such constants might confuse the class's users -- perhaps the constants are not intended to be seen outside of the class, but they will be seen because any constants declared in an interface are implicitly public (and static and final).

Constant interfaces are a kludge that can be eliminated by using static imports or enums. For example, consider Listing 8's replacement to the Directions interface:

Listing 8: (version 2)

Near the top of the source file that declares Compass, you could specify the following static import statement:

and then refer to NORTH and the other constants without having to include a Directions prefix.

For this example, an enum would be a better choice:

Because the switch statement supports enums, you can rewrite Compass, as shown in Listing 9.

Listing 9: (version 2)

Along with default methods, Java 8 evolved the interface language feature to support static methods. You can now declare static methods in an interface. This capability opens up all kinds of possibilities, such as that shown in Listing 10.

Listing 10:

Listing 10 declares a Fillable interface for use with graphical objects (such as circles or triangles) that can paint their interiors via the fill() method. For convenience, an rgb() static method is declared in Fillable to conveniently convert red/green/blue color components to a 32-bit integer value, which can be passed to fill(). This static method belongs to Fillable and can be called, as follows:

The final role served by interfaces is to tag (or mark) classes for special services. For example, a class implements the java.lang.Cloneable interface to tell the Object.clone() method that it's legal for that method to make a field-for-field copy of that class's instances. Similarly, a class implements the interface to indicate that the class's instances can be serialized and deserialized.

Cloneable and Serializable are empty interfaces. They exist solely to identify to the cloning and serialization mechanisms that instances of their implementing classes can be cloned or serialized.

Java newcomers are often puzzled by interfaces, probably because of their multiple roles. Although I think I've covered all of these roles, let me know in the comments if I've missed a role played by Java's interface language feature.


Jeff Friesen

Zur Startseite