The six roles of the interface

Discover the six uses for Java's interface language feature

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.

Role 1: Declaring annotation types

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

Listing 1. Stub.java

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Stub
{
   int id(); // A semicolon terminates an element declaration.
   String dueDate();
   String developer() default "unassigned";
}

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:

  • id() returns an integer-based identifier for the stub
  • dueDate() identifies the date by which the stub must be filled in with code
  • developer() identifies the developer responsible for filling in the stub

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. ContactMgr.java

@Stub
(
   id = 1,
   dueDate = "12/31/2016"
)
public class ContactMgr
{
   @Stub
   (
      id = 2,
      dueDate = "06/31/2016",
      developer = "Marty"
   )
   public void addContact(String contactID) 
   {
   }
}

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. StubFinder.java

import java.lang.reflect.Method;

public class StubFinder
{
   public static void main(String[] args) throws Exception
   {
      if (args.length != 1)
      {
         System.err.println("usage: java StubFinder classfile");
         return;
      }
      Class<?> clazz = Class.forName(args[0]);
      if (clazz.isAnnotationPresent(Stub.class))
      {
         Stub stub = clazz.getAnnotation(Stub.class);
         System.out.println("Stub ID = " + stub.id());
         System.out.println("Stub Date = " + stub.dueDate());
         System.out.println("Stub Developer = " + stub.developer());
         System.out.println();
      }
      Method[] methods = clazz.getMethods();
      for (int i = 0; i < methods.length; i++)
         if (methods[i].isAnnotationPresent(Stub.class))
         {
            Stub stub = methods[i].getAnnotation(Stub.class);
            System.out.println("Stub ID = " + stub.id());
            System.out.println("Stub Date = " + stub.dueDate());
            System.out.println("Stub Developer = " + stub.developer());
            System.out.println();
         }
   }
}

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:

javac *.java

Run the resulting application, as follows:

java StubFinder ContactMgr

You should observe the following output:

Stub ID = 1
Stub Date = 12/31/2016
Stub Developer = unassigned

Stub ID = 2
Stub Date = 06/31/2016
Stub Developer = Marty

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.

Role 2: Describing implementation-independent capabilities

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. Freq.java (version 1)

import java.nio.CharBuffer;

public class Freq
{
   public static void main(String[] args)
   {
      if (args.length != 1)
      {
         System.err.println("usage: java Freq text");
         return;
      }
      analyzeS(args[0]);
      analyzeSB(new StringBuffer(args[0]));
      analyzeCB(CharBuffer.wrap(args[0]));
   }

   static void analyzeCB(CharBuffer cb)
   {
      int counts[] = new int[26];
      while (cb.hasRemaining())
      {
         char ch = cb.get();
         if (ch >= 'a' && ch <= 'z')
            counts[ch - 'a']++;
      }
      for (int i = 0; i < counts.length; i++)
         System.out.printf("Count of %c is %d%n", (i + 'a'), counts[i]);
      System.out.println();
   }

   static void analyzeS(String s)
   {
      int counts[] = new int[26];
      for (int i = 0; i < s.length(); i++)
      {
         char ch = s.charAt(i);
         if (ch >= 'a' && ch <= 'z')
            counts[ch - 'a']++;
      }
      for (int i = 0; i < counts.length; i++)
         System.out.printf("Count of %c is %d%n", (i + 'a'), counts[i]);
      System.out.println();
   }

   static void analyzeSB(StringBuffer sb)
   {
      int counts[] = new int[26];
      for (int i = 0; i < sb.length(); i++)
      {
         char ch = sb.charAt(i);
         if (ch >= 'a' && ch <= 'z')
            counts[ch - 'a']++;
      }
      for (int i = 0; i < counts.length; i++)
         System.out.printf("Count of %c is %d%n", (i + 'a'), counts[i]);
      System.out.println();
   }
}

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. Freq.java (version 2)

import java.nio.CharBuffer;

public class Freq
{
   public static void main(String[] args)
   {
      if (args.length != 1)
      {
         System.err.println("usage: java Freq text");
         return;
      }
      analyze(args[0]);
      analyze(new StringBuffer(args[0]));
      analyze(CharBuffer.wrap(args[0]));
   }

   static void analyze(CharSequence cs)
   {
      int counts[] = new int[26];
      for (int i = 0; i < cs.length(); i++)
      {
         char ch = cs.charAt(i);
         if (ch >= 'a' && ch <= 'z')
            counts[ch - 'a']++;
      }
      for (int i = 0; i < counts.length; i++)
         System.out.printf("Count of %c is %d%n", (i + 'a'), counts[i]);
      System.out.println();
   }
}

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

Role 3: Facilitating library evolution

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:

  • default Stream<E> parallelStream(): Return a (possibly) parallel java.util.stream.Stream object with this collection as its source.
  • default Stream<E> stream(): Return a sequential Stream object with this collection as its source.

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

List<String> innerPlanets = Arrays.asList("Mercury", "Venus", "Earth", "Mars");

You would traditionally iterate over this collection, as follows:

for (String innerPlanet: innerPlanets)
   System.out.println(innerPlanet);

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:

innerPlanets.stream().forEach(System.out::println);
innerPlanets.parallelStream().forEach(System.out::println);

Here, innerPlanets.stream() 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.

Related:
1 2 Page 1
Page 1 of 2
InfoWorld Technology of the Year Awards 2023. Now open for entries!