Java 101: Object-oriented language basics, Part 6: Interfaces and multiple inheritance

how-to
Sep 7, 200119 mins

Use interfaces for safe multiple inheritance and a great deal more

Java’s interface language feature confuses many Java newbies. Many mistakenly assume that interfaces only sidestep Java’s failure to support multiple implementation inheritance. In reality, that workaround is only a side-effect of interface usage, as you will discover in this article.

If you follow the Java 101 column, you know that we are currently touring Java’s object-oriented language basics. So far, this series has covered class declaration, object creation, field/method (including constructor method) declaration and access, object composition, object layering, multiple implementation inheritance, and the root of all classes. In addition, I have introduced the specialized topics of enumerated types and singletons. Part 6 of this series presents a deeper understanding of interfaces.

Interfaces

The word interface conjures up the image of a place where two independent systems meet for communication. For example, a human being and a computer communicate through a keyboard, a mouse, and/or some other input device. The input device serves as an interface between the computer and the human being. From a software perspective, an interface represents a service contract between a library that implements those services and code that calls upon that library’s services. For example, a Java program obtains file I/O (input/output) services by creating objects from various file-related classes in Java’s class library and calling their methods. Those classes and methods form an interface between code wanting file I/O and the actual file I/O code.

We can take the interface concept one step further: from Java’s perspective, an interface is the introduction (but not an implementation) of a type at the source code level. To understand that concept, you first must grasp the notion of type, an abstraction that identifies a set of data items — that share some commonality — and a set of operations that manipulate those data items. For example, the integer type identifies a set of numbers that do not have fractional parts and a set of operations that manipulate those numbers (such as addition and subtraction). To support types, a computer language provides a facility that allows a developer to introduce (and possibly implement) a type in source code.

Developers introduce primitive types into source code through keywords (such as int or boolean) and operators (such as + or !). Primitive types have no implementation at the source code level: the compiler and the JVM provide an implementation. Developers introduce reference types into source code through class and interface declarations. Classes implement reference types (at the source code level) by declaring read/write field variables (to identify a set of data items) and method signatures with code bodies (to identify a set of operations). Unlike classes, interfaces do not implement reference types in source code.

There is a reason why interfaces do not implement reference types: flexibility. Consider a starting and stopping reference type. You can use that reference type to indicate that you wish to start/stop a vehicle, an electric saw, a lawn mower, a washing machine, court proceedings, an expedition, a criminal investigation, and so on. If you implement that reference type as a class, you assign an implementation to the reference type. Once done, you can only use that reference type in the context of its implementation. In other words, you lose the flexibility of the starting and stopping reference type when you implement that type as a class. To preserve the generality of the starting and stopping reference type, you need to use an interface.

Declaring interfaces

When you want to introduce, but not implement, a reference type in source code, you must declare an interface with the following syntax:

[ 'public' ] [ 'abstract' ] 'interface' interface_name
'{'
     // constants and method signature declarations
'}'

An interface declaration minimally consists of keyword interface followed by an identifer — interface_name — that names the interface (and must not be a reserved word). By convention, an interface name begins with a capital letter, and you capitalize the first letter of all subsequent words that compose that name. To make interface_name accessible to all classes in all packages (a concept I will discuss in a future column), precede keyword interface with keyword public. Also, to emphasize the abstractness of an interface, prefix keyword interface with keyword abstract. (Because specifying abstract is redundant, you can leave it out of your code.) A brace-delimited block follows the declaration header. Use that block to present an interface’s members: constants and method signatures. For your first taste of interfaces, examine Listing 1:

Listing 1. StartStop.java

// StartStop.java
interface StartStop
{
   void start ();
   void stop ();
}

The StartStop interface declaration introduces a reference type into source code. That reference type identifies two operations: starting, by way of the start() method signature, and stopping, by way of the stop()method signature. As with the interface header, you can include keyword abstract in each method signature, but it is redundant to do so. For example, you could declare the StartStop interface as follows:

abstract interface StartStop
{
   abstract void start ();
   abstract void stop ();
}

Extending interfaces

Just as classes extend other classes, interfaces can extend other interfaces (but not classes). Interface extension permits type specialization through additional method signatures. To extend an interface, append the following extends-clause syntax to an interface declaration header:

'extends' interface_name [ ','  interface_name ... ]

An extends clause begins with keyword extends and continues with a comma-delimited list of identifiers that name interfaces. You can think of the interface performing the extension as a subinterface and each named interface in the extends clause as a superinterface. Listing 2’s DiagnoseStartStop source code demonstrates the use of interface extension to declare a subinterface of Listing 1’s StartStop interface:

Listing 2. DiagnoseStartStop.java

// DiagnoseStartStop.java
interface DiagnoseStartStop extends StartStop
{
   boolean NOT_STARTED = false;
   boolean STARTED = true;
   boolean isStarted ();
} 

The DiagnoseStartStop interface extends the StartStop interface by introducing a diagnostic capability. (Why might you want such a capability? Consider situations in which a lawn mower does not start because it has no gas and when a conference fails to begin because the guest speaker has not arrived.) The diagnostic capability takes the form of an isStarted() method and a pair of constants: NOT_STARTED and STARTED. (Note: Even though you cannot see keywords public, final, and static, NOT_STARTED and STARTED are implicitly public, static, and final. You can explicitly specify those keywords in an interface’s constant declaration, but it is redundant to do so.)

Tagging classes with empty interfaces

Java’s standard class library contains several empty interfaces that declare no constants or method signatures. Java uses empty interfaces to tag, that is, mark object classes that are to be singled out for special treatment. Empty interface examples include Cloneable, Serializable, and UIResource.

An interface is typically left empty to notify various APIs that they can perform certain operations on objects whose classes implement those interfaces. For example, a class implements Serializable to tell the Serializable API, which I will explore in a future column, to save and restore the data items in that class’s objects.

Implementing interfaces

Once you declare an interface, that interface accomplishes nothing until you specify an implementation for its operations in a class. Use the following syntax to implement an interface in a class:

'class' class_name [ 'extends' superclass_name ] 
                            [ 'implements' interface_name [ ',' interface_name ... ] ]

The syntax specifies an implements clause that you append to a class header. An implements clause begins with keyword implements and continues with a comma-delimited list of interface_name identifiers, where each identifier identifies an interface. The implements clause forces a class to inherit all constants and/or method signatures in that clause’s interfaces, a form of inheritance commonly known as interface inheritance.

To implement an interface, it is not sufficient to specify only an implements clause. If you do, you end up with an abstract class — a concept I will explore in next month’s column. In addition to identifying an implements clause, you must attach code bodies to the interface’s method signatures. Listing 3 demonstrates interface implementation, by implementing the StartStop interface in a Vehicle class:

Listing 3. UseStartStop.java

// UseStartStop.java
class Vehicle implements StartStop
{
   private String name;
   Vehicle (String name)
   {
      this.name = name;
   }
   String getName ()
   {
      return name;
   }
   public void start ()
   {
   }
   public void stop ()
   {
   }
}
class Car extends Vehicle
{
   Car (String name)
   {
      super (name);
   }
   public void start ()
   {
      System.out.println ("Insert key into ignition and turn.");
   }
   public void stop ()
   {
      System.out.println ("Turn key and remove from ignition.");
   }
   public String toString ()
   {
      return getName ();
   }
}
class UseStartStop
{
   public static void main (String [] args)
   {
      Car c = new Car ("???");
      c.start ();
      c.stop ();
      System.out.println (c.toString ());
      StartStop ss = c;
      ss.start ();
      ss.stop ();
      System.out.println (ss.toString ());
   }
}

For the most part, UseStartStop‘s source code should be easy to understand. However, three items deserve mention. First, notice the public keyword that prefixes the start() and stop() method signatures in classes Vehicle and Car. That keyword is not part of the start() and stop() method signatures in the earlier StartStop interface. What is going on? Java considers all method signatures that appear in an interface to represent public methods whether or not the public keyword is present. Java does not allow more restrictive method access in a class that inherits from either a class or an interface. Also, a public method is the most accessible method. Because of those two facts, keyword public must appear in a method declaration when a class either extends a class that declares that public method (and chooses to override the method), or implements an interface that specifies that method’s signature.

Second, why implement StartStop in Vehicle when its methods are empty? Couldn’t you just implement StartStop in Car? You could, but suppose all types of vehicles express some commonality during startup. Rather than repeat that commonality in each subclass, express it once in the superclass. Also, by factoring StartStop‘s methods into Vehicle, you take advantage of polymorphism — a concept I will discuss next month.

Finally, what does StartStop ss = c; mean? ss is a reference variable that has the StartStop type. That variable can hold a reference to an object whose class implements the StartStop interface. Because c contains a reference to a Car object, and because Car implements StartStop, you can legally assign c‘s reference to variable ss. That capability has implications though. For example, when a Car object reference assigns to a Car variable, you have full access to that object’s nonprivate members. However, when a Car object reference assigns to a StartStop variable, your access narrows to only those members that StartStop declares as well as all Object methods inherited and/or overridden in the class. For example, using ss, it is legal to call start() and toString(), but not getName().

Multiple interface inheritance

Java allows classes to implement multiple interfaces — a capability known as multiple interface inheritance. Suppose you want to write a program that counts the number of times you have failed to start a washing machine. You would like to use that counting capability together with the previous starting and stopping (with diagnosis) capability.

One way to introduce the counting capability is to specify it inside an appropriate class that represents your washing machine. However, because counting is a generic concept, you probably do not want to tie it to a specific implementation. Instead, you could introduce the counting capability as an interface that extends DiagnoseStartStop. But doing so indicates that counting is a kind of diagnostic starting and stopping capability, which has implications. For example, if you include counting with starting and stopping, you say that you can also start and stop geese in addition to counting them as they fly overhead — I assume you do not have a gun. Also, you say that you can start and stop goals scored at a hockey game (in addition to tracking the goals your team scored). To separately deal with counting, you must introduce an independent interface that represents that capability, as shown in Listing 4’s Counter interface:

Listing 4. Counter.java

// Counter.java
interface Counter
{
   void increment ();
   int getCount ();
}

The Counter interface introduces a counter type into source code. When a class implements Counter, its objects can be thought of as counters: somewhere inside a counter object is a field that keeps track of a count. The increment operation advances the counter by some increment (which is usually one, but could be some other value, such as six — to account for a touchdown in a football simulation), and the get operation retrieves the current counter value.

Now that you have the DiagnoseStartStop and Counter interfaces, you can develop a program that tracks how many times you fail at starting a washing machine. Listing 5 does just that:

Listing 5. FaultyAppliance.java

// FaultyAppliance.java
class Appliance implements Counter, DiagnoseStartStop
{
   private int count;
   private String brand;
   protected boolean started;
   Appliance (String brand)
   {
      this.brand = brand;
   }
   String getBrand ()
   {
      return brand;
   }
   public void increment ()
   {
      count++;
   }
   public int getCount ()
   {
      return count;
   }
   public void start ()
   {
      increment ();
   }
   public void stop ()
   {
   }
   public boolean isStarted ()
   {
      return started;
   }
}
class WashingMachine extends Appliance
{
   WashingMachine (String brand)
   {
      super (brand);
   }
   public void start ()
   {
      if (isStarted ())
      {
          System.out.println ("Washing machine already running.");
          return;
      }
      if (rnd (4) > 1)
      {
          System.out.println ("Trying to start washing machine.");
          super.start ();
      }
      else
      {
          started = true;
          System.out.println ("Washing machine is running.");
      }
   }
   public void stop ()
   {
      if (started)
      {
          started = false;
          System.out.println ("Washing machine is stopped.");
      }
      else
          System.out.println ("Washing machine already stopped."); 
   }
   // Return a random number between 0 and limit - 1 (inclusive).
   int rnd (int limit)
   {
      return (int) (Math.random () * limit);
   }
}
class FaultyAppliance
{
   public static void main (String [] args)
   {
      WashingMachine wm = new WashingMachine ("Maytag");
      for (int i = 0; i < 10; i++)
           wm.start ();
      if (wm.isStarted ())
          wm.stop ();
      System.out.println ("Number of faulty start attempts: " +
                          wm.getCount ());
   }
}

FaultyAppliance‘s main() method creates a WashingMachine object and tries to start the washing machine 10 times. If the washing machine starts, it must stop. If the first attempt to start the washing machine succeeds, getCount() returns zero. Otherwise, that method returns the number of faulty start attempts. To accomplish its simulation, FaultyAppliance uses Math‘s random() method, which returns a random number that ranges from 0.0 to (almost) 1.0. By multiplying random()‘s return value by some limit, and by converting the result to an integer, it is possible to generate an integral random number greater than or equal to zero and less than some limit. (I will discuss Math and its methods in a future column.)

The Appliance class inherits from both the Counter and DiagnoseStartStop interfaces. In contrast to the general use of multiple implementation inheritance, Appliance‘s multiple interface inheritance is safe. To see why, let’s compare both inheritance approaches. We begin with a look at read/write variables and conclude with a look at methods.

What is wrong with multiple implementation inheritance in the context of read/write variables? To answer that question, keep in mind that the compiler must generate instructions that the JVM executes to allocate memory for a variable. The amount of memory the JVM allocates depends on the variable’s type. Suppose two superclasses declare a read/write variable with the same name but different types and/or initial values. If Java permitted a subclass to inherit from both superclasses, the compiler would face a dilemma: should it assume the first type (and, possibly, initial value) or the second type (and, possibly, initial value)? A subclass’s type implementation is fixed and cannot accommodate both types, thus, the dilemma. For example, suppose the subclass contains x = 2;, and one superclass declares x to be of integer type, whereas the other superclass declares x to be of double-precision floating-point type. Does the compiler generate instructions that assign a 32-bit twos-complement bit sequence — representing an integer — to a 32-bit memory location? Or does it generate instructions that assign a 64-bit IEEE-754 format floating-point bit sequence — representing a double-precision floating-point value — to a 64-bit memory location? That problem exacerbates if each superclass contains a method that modifies its same-named variable according to the variable’s type. Assuming a subclass inherits both methods, the compiler must deal with two different type implementations for the same variable.

Multiple interface inheritance solves the problem of read/write variables with two different types (and/or initial values) by requiring interfaces to declare only constants (that is, read-only variables). Because each variable is read-only, the compiler does not generate bytecode instructions to allocate memory for that variable — the read-only variable never changes so it does not require its own memory. The compiler only needs to generate bytecode instructions that retrieve the constant’s value from its class. If two different interfaces declare a constant with the same name, but with a different type and/or a different initial value, and if a class implements both interfaces but does not access either constant, the compiler does nothing; there is no problem. However, if a constant name appears in the class, the compiler requires the interface’s name (followed by a period character) to prefix that name. Problem solved!

Two issues arise in multiple implementation inheritance in the context of methods:

  1. Each method can have the same name with the same parameter list and return type but can have a different code body. Which code body does the subclass inherit?
  2. Also, if two superclasses each declare methods with signatures that are identical except for their return types, the compiler, by examining a method call expression, cannot determine which method to call. As such, those methods are incorrectly overloaded.

To solve the first problem, multiple interface inheritance prohibits interfaces from specifying code bodies. Instead, the class that implements the interface supplies the code body.

In regards to the second issue, interfaces do not shoulder the blame for incorrectly overloaded methods, developers do. The following code fragment solves the same-named constant problem, but fails to solve the incorrectly overloaded method problem:

interface A
{
   char c = 'A';
   char m ();
}
interface B
{
   boolean c = true;
   void m ();
}
class C implements A, B
{
   char m ()
   {
       return A.c;
   }
   void m ()
   {
      System.out.println (B.c);
   }
}

The above code fragment will not compile because it unsuccessfully overloads m(). The solution: avoid incorrectly overloaded methods through careful class and interface design.

The power of the interface

Though interfaces are a potent language feature, many Java newcomers fail to understand why they are so powerful. Interfaces don’t just provide a workaround to Java’s lack of support for multiple implementation inheritance. Interfaces factor out commonality across diverse class hierarchies, resulting in compact and understandable source code. For a taste of interface power, examine Listing 6:

Listing 6. InventoryDemo.java

// InventoryDemo.java
interface Product
{
   String getName ();
   double getCost ();
}
class Vehicle implements Product
{
   private String name;
   private double cost;
   Vehicle (String name, double cost)
   {
      this.name = name;
      this.cost = cost;
   }
   public String getName ()
   {
      return name;
   }
   public double getCost ()
   {
      return cost;
   }
}
class Car extends Vehicle
{
   Car (String name, double unitCost)
   {
      super (name, unitCost);
   }
}
class Truck extends Vehicle
{
   Truck (String name, double unitCost)
   {
      super (name, unitCost);
   }
}
class Tool implements Product
{
   private String name;
   private double cost;
   Tool (String name, double cost)
   {
      this.name = name;
      this.cost = cost;
   }
   public String getName ()
   {
      return name;
   }
   public double getCost ()
   {
      return cost;
   }
}
class InventoryDemo
{
   public static void main (String [] args)
   {
      Product [] products =
      {
         new Car ("Jaguar", 100000.00),
         new Tool ("JigSaw", 150.23),
         new Car ("Neon", 17000.00),
         new Tool ("JigSaw", 149.18),
         new Car ("Jaguar", 110000.00),
         new Car ("Neon", 17500.00),
         new Car ("Neon", 17875.32),
         new Truck ("RAM", 35700.00)
      };
      takeInventory ("JigSaw", products);
      takeInventory ("Neon", products);
      takeInventory ("Jaguar", products);
      takeInventory ("RAM", products);
   }
   static void takeInventory (String item, Product [] p)
   {
      int quantity = 0;
      double totalCost = 0.00;
      for (int i = 0; i < p.length; i++)
           if (p [i].getName ().equals (item))
           {
               quantity++;
               totalCost += p [i].getCost ();
           }
      System.out.println (item + ": Quantity = " + quantity +
                                 ", Total cost = " + totalCost);
   }
}

InventoryDemo declares two class hierarchies: a vehicle hierarchy and a tool hierarchy. What is common to both class hierarchies? Objects you create from appropriate classes (such as Car and Tool) in each class hierarchy are products. The Product interface factors that commonality into its operations. As you can see, those operations make it easy to write compact code that takes inventory. And that is the power of the interface.

Review

Interfaces make it possible to introduce types into source code without fixing those types to a specific implementation. In this article, you learned how to declare and extend interfaces. You then examined the usefulness of empty interfaces. In addition, you learned how to implement interfaces and why multiple interface inheritance is safe. Finally, you witnessed the power of the interface.

I encourage you to email me with any questions you may have involving interfaces, or any other topic covered in this article or previous Java 101 articles. (Please, keep such questions relevant to material discussed in this column’s articles.) In future articles, I plan to introduce a question-and-answer section, where I publish your questions along with my answers.

Next month’s article will conclude this series by focusing on polymorphism and abstract classes. Also, that article presents the second Java 101 quiz. You might want to start studying!

Jeff Friesen has been involved with computers for the past 20 years. He holds a degree in computer science and has worked with many computer languages. Jeff has also taught introductory Java programming at the college level. In addition to writing for JavaWorld, he wrote his own Java book for beginners — Java 2 By Example (QUE, 2000) — and helped write a second Java book, Special Edition Using Java 2 Platform (QUE, 2001). Jeff goes by the nickname Java Jeff (or JavaJeff). To see what he’s working on, check out his Website at http://www.javajeff.com.