Interfaces in Java

Learn the difference between classes and interfaces, then get started declaring, implementing, and extending interfaces in your Java programs

1 2 3 Page 2
Page 2 of 3

When you implement an interface method (by overriding the interface's method header), remember that all of the methods whose headers are declared in the interface are implicitly declared public. If you forget to include public in the implemented method's declaration, the compiler will report an error informing you that you're attempting to assign weaker access to the implemented method.

Implementing multiple interfaces

Earlier, I mentioned that a class can implement multiple interfaces. Each interface's name is specified as part of a comma-separated list of names that follows the implements keyword. Listing 6 presents a simple example where class C implements interfaces A and B.

Listing 6. Implementing multiple interfaces

interface A
{
   // appropriate constants and/or method headers
}
interface B
{
   // appropriate constants and/or method headers
}
class C implements A, B
{
   // override A's and B's method headers
}

Beware of the potential for name collisions when implementing multiple interfaces. This occurs when the same constant name appears in each interface, possibly with different type and/or other information, and is accessed in the class. When a name collision occurs, the compiler will report an error, which is demonstrated in Listing 7.

Listing 7. Demonstrating colliding constants

interface A
{
   int CONSTANT = 2;
   void method();
}
interface B
{
   int CONSTANT = 3;
   int method(int x);
}
class C implements A, B
{
   int x = CONSTANT;
   @Override
   public void method()
   {
   }
   @Override
   public int method(int x)
   {
      return x;
   }
}

Here, class C is inheriting two different constants named CONSTANT that are initialized to two different values. The Java compiler cannot determine which constant should be inherited by C (the same problem would occur if each constant was assigned the same value) and reports the following error message:

C.java:15: error: reference to CONSTANT is ambiguous
   int x = CONSTANT;
           ^
  both variable CONSTANT in A and variable CONSTANT in B match
1 error

Extending Java interfaces

A class that implements an interface reveals interface inheritance. The class inherits the interface's constants and method headers, which it overrides. For example, each of Circle and Rectangle inherits Drawable's five integer constants and draw() method header.

Interface inheritance is also demonstrated when an interface extends another interface. Just as a subclass can extend a superclass via reserved word extends, you can use this reserved word to have a subinterface extend a superinterface. Listing 8 demonstrates.

Listing 8. Declaring a subinterface that extends a superinterface


interface Fillable extends Drawable
{
   void fill(int color);
}

Fillable extends Drawable, inheriting its color constants and draw() method header. Fillable also declares the header for a fill() method that must be called with one of these constants to specify the color used to fill an interior. (Fillable extends Drawable to support drawing an outline as well as filling an interior.)

You could retrofit the previous Circle and Rectangle classes to support Fillable by performing the following steps:

  1. Change implements Drawable to implements Fillable. There is no need to specify either implements Drawable, Fillable or implements Fillable, Drawable because Fillable includes all of Drawable by extension.
  2. Override the fill() method header in the same manner as overriding the draw() method header.

Listing 9 presents an equivalent Fill application that demonstrates the Fill interface.

Listing 9. Aliasing Circle and Rectangle objects as Fillables

class Fill
{
   public static void main(String[] args)
   {
      Fillable[] fillables = new Fillable[] { new Circle(10, 20, 15), 
                                              new Circle(30, 20, 10),
                                              new Rectangle(5, 8, 8, 9) };
      for (int i = 0; i < fillables.length; i++)
      {
         fillables[i].draw(Drawable.RED);
         fillables[i].fill(Fillable.BLACK);
      }
   }
}

Circle and Rectangle implement Fillable, giving Circle and Rectangle objects a Fillable type in addition to their class types. Therefore, it's legal to store each object's reference in an array of Fillables. A loop iterates over this array, invoking each Fillable's inherited draw() and non-inherited fill() methods to draw and fill a circle or a rectangle.

If Listing 2 is stored in Drawable.java, which is in the same directory as Circle.java, Rectangle.java, Fillable.java, and Fill.java (respectively storing Listing 3 and Listing 4, with updates, Listing 8, and a source file that stores Listing 9) you can compile these source files using either of the following command lines:

javac Fill.java
javac *.java

Run the Fill application as follows:

java Fill

You should observe the following output:

Circle drawn at (10.0, 20.0), with radius 15.0, and color 1
Circle filled at (10.0, 20.0), with radius 15.0, and color 4
Circle drawn at (30.0, 20.0), with radius 10.0, and color 1
Circle filled at (30.0, 20.0), with radius 10.0, and color 4
Rectangle drawn with upper-left corner at (5.0, 8.0) and lower-right corner at (8.0, 9.0), and color 1
Rectangle filled with upper-left corner at (5.0, 8.0) and lower-right corner at (8.0, 9.0), and color 4

You can upcast the interface type of an object from a subinterface to a superinterface because a subinterface is a kind of superinterface. For example, you could assign a Fillable reference to a Drawable variable and then invoke Drawable's draw() method on the variable:

Drawable d = fillables[0];
d.draw(Drawable.GREEN);

Extending multiple interfaces

As with interface implementation, you can extend multiple interfaces. Each interface's name is specified as part of a comma-separated list of names that follows the extends keyword. Listing 10 presents a simple example where interface C extends interfaces A and B.

Listing 10. Extending multiple interfaces

interface A
{
   // appropriate constants and/or method headers
}
interface B
{
   // appropriate constants and/or method headers
}
interface C extends A, B
{
   // appropriate constants and/or method headers
}

Beware of the potential for name collisions when extending multiple interfaces. This occurs when the same constant name appears in each superinterface, possibly with different type and/or other information, and is accessed in the subinterface. When a name collision occurs, the compiler will report an error, which is demonstrated in Listing 11.

Listing 11. Demonstrating colliding constants

interface A
{
   int CONSTANT = 2;
   void method();
}
interface B
{
   int CONSTANT = 3;
   int method(int x);
}
interface C extends A, B
{
   int CONSTANT2 = CONSTANT;
}

Here, interface C is inheriting two different constants named CONSTANT that are initialized to two different values. The Java compiler cannot determine which constant should be inherited by C (the same problem would occur if each constant was assigned the same value) and reports the following error message:

C.java:15: error: reference to CONSTANT is ambiguous
   int CONSTANT2 = CONSTANT;
                   ^
  both variable CONSTANT in A and variable CONSTANT in B match
1 error

Evolving the interface in Java 8: Default and static methods

Java SE 8 introduced two significant enhancements to interfaces: default and static methods. These enhancements have their uses, but make interfaces more like classes--to the point where you can base some applications on interfaces instead of classes.

Default methods

A default method is a concrete instance method that's defined in an interface and whose method header is prefixed with the default keyword. An example of a default method in Java's standard class library is java.util.Comparator<T>'s default Comparator<T> reversed() method.

To understand the usefulness of interface-based default methods, suppose you've created Listing 12's Drivable interface, which describes any kind of drivable in terms of its basic operations. (I could have called this interface Vehicle but would a horse qualify as a vehicle?)

Listing 12. Declaring a Drivable interface

public interface Drivable
{
   public void drive(int numUnits);
   public void start();
   public void stop();
   public void turnLeft();
   public void turnRight();
}

Being satisfied with Drivable, you make this interface available to other developers who use it extensively in their applications. For example, Listing 13 shows a simple application that introduces a Car class, which implements Drivable, along with a DMDemo demonstration class.

Listing 13. Implementing Drivable in a Car class

class Car implements Drivable
{
   @Override
   public void drive(int numUnits)
   {
      System.out.printf("Driving car %d kilometers%n", numUnits);
   }
   @Override
   public void start()
   {
      System.out.println("Starting car");
   }
   @Override
   public void stop()
   {
      System.out.println("Stopping car");
   }
   @Override
   public void turnLeft()
   {
      System.out.println("Turning car left");
   }
   @Override
   public void turnRight()
   {
      System.out.println("Turning car right");
   }
}
public class DMDemo
{
   public static void main(String[] args)
   {
      Car car = new Car();
      car.start();
      car.drive(20);
      car.turnLeft();
      car.drive(10);
      car.turnRight();
      car.drive(8);
      car.stop();
   }
}

Note that @Override is an annotation (metadata) telling the compiler to ensure that the superclass actually declares a method that the annotated method overrides, and report an error when this isn't the case. I'll introduce annotations later in this series.

Compile Listing 13 and Listing 12 (javac DMDemo.java) and run the application (java DMDemo). You'll observe the following output:

Starting car
Driving car 20 kilometers
Turning car left
Driving car 10 kilometers
Turning car right
Driving car 8 kilometers
Stopping car

Suppose your manager now wants you to add a demo() method to Drivable. Adding a method to an interface breaks binary compatibility, so every class implementing Drivable must be retrofitted to also implement demo(). To avoid this aggravation, you add demo() to Drivable as a default method, as shown in Listing 14.

Listing 14. Declaring a Drivable interface with a default method

public interface Drivable
{
   public void drive(int numUnits);
   public void start();
   public void stop();
   public void turnLeft();
   public void turnRight();
   public default void demo()
   {
      start();
      drive(20);
      turnLeft();
      drive(10);
      turnRight();
      drive(8);
      stop();
   }
}

demo() exists as an instance method with a default implementation. Because it doesn't need to be implemented by classes that implement Drivable, older code won't break when executed with a binary version of this interface. Listing 15 proves that binary compatibility isn't compromised and shows how to invoke demo().

Listing 15. Invoking Drivable's default method via a Car object reference

class Car implements Drivable
{
   @Override
   public void drive(int numUnits)
   {
      System.out.printf("Driving car %d kilometers%n", numUnits);
   }
   @Override
   public void start()
   {
      System.out.println("Starting car");
   }
   @Override
   public void stop()
   {
      System.out.println("Stopping car");
   }
   @Override
   public void turnLeft()
   {
      System.out.println("Turning car left");
   }
   @Override
   public void turnRight()
   {
      System.out.println("Turning car right");
   }
}
public class DMDemo
{
   public static void main(String[] args)
   {
      Car car = new Car();
      car.demo();
   }
}

Listing 15 shows that the Car class hasn't been modified; it doesn't have to implement demo(). It also shows that demo() is invoked like any other instance method that's a member of the Car class. If you run this application, you'll observe the same output as shown previously.

Java 8 introduced default methods to evolve the Java Collections Framework, so that it could support the new Streams API without breaking backward compatibility. New default Stream<E> parallelStream() and default Stream<E> stream() methods were added to the java.util.Collection<E> interface to bridge between Collections and Streams. Default methods can also be used to better organize library methods. For example, now that default void sort(Comparator<? super E> c) has been added to java.util.List<E>, you can more naturally write myList.sort(null); instead of Collections.sort(myList);.

Mastering default methods

If you want to master default methods, there are a few more tricks you should know. First, you can override a default method when necessary. Because default methods are implicitly public, don't assign a weaker access privilege when you override one. If you do that you'll receive a compiler error. Listing 16 shows how to override demo().

Listing 16. Excerpting Car from a third version of the DMDemo application

class Car implements Drivable
{
   @Override
   public void demo()
   {
      start();
      drive(20);
      stop();
   }
   @Override
   public void drive(int numUnits)
   {
      System.out.printf("Driving car %d kilometers%n", numUnits);
   }
   @Override
   public void start()
   {
      System.out.println("Starting car");
   }
   @Override
   public void stop()
   {
      System.out.println("Stopping car");
   }
   @Override
   public void turnLeft()
   {
      System.out.println("Turning car left");
   }
   @Override
   public void turnRight()
   {
      System.out.println("Turning car right");
   }
}
1 2 3 Page 2
Page 2 of 3