Java 101: Foundations

Java 101: Interfaces in Java

Extract class interfaces into reusable Java interfaces

Become An Insider

Sign up now and get FREE access to hundreds of Insider articles, guides, reviews, interviews, blogs, and other premium content. Learn more.

Java interfaces are different from classes, and it's important to know how to use their special properties in your programs. This article introduces the difference between classes and interfaces, then guides you through short examples demonstrating how to declare, implement, and extend Java interfaces. I also demonstrate how the interface has evolved in Java 8, with the addition of default and static methods. These additions make interfaces more useful to experienced developers, but they also blur the lines between classes and interfaces, making interface programming even more confusing to Java beginners.

What is an interface?

An interface is a point where two systems meet and interact. For example, a vending machine interface is a mechanism that allows users to select an item, pay for it, and receive the desired food or drink. From a programming perspective, an interface sits between software components. Consider that a method header (method name, parameter list, and so on) interface sits between external code that calls the method and the code within the method that will be executed as a result of the call. Here is an example:


System.out.println(average(10, 15));
double average(double x, double y) // interface between average(10, 15) call and return (x + y) / 2;
{
   return (x + y) / 2;
}

What's often confusing to Java beginners is that classes also have interfaces. As I explained in Java 101: Classes and objects in Java, the interface is the part of the class that is accessible to code located outside of it. A class's interface consists of some combination of methods, fields, constructors, and other entities. Consider Listing 1.

Listing 1. Declaring an Account class


class Account
{
   private String name;
   private long amount;
   Account(String name, long amount)
   {
      this.name = name;
      setAmount(amount);
   }
   void deposit(long amount)
   {
      this.amount += amount;
   }
   String getName()
   {
      return name;
   }
   long getAmount()
   {
      return amount;
   }
   void setAmount(long amount)
   {
      this.amount = amount;
   }
}

The Account(String name, long amount) constructor and the void deposit(long amount), String getName(), long getAmount(), and void setAmount(long amount) methods form the Account class's interface: they are accessible to external code. The private String name; and private long amount; fields are inaccessible.

The code that supports a method's or a class's interface (such as a class's private fields) is known as implementation code. Implementation code should be hidden from external code so that it can be changed to meet evolving requirements.

Exposed implementation code can lead to unwanted interdependencies between software components. For example, method code might come to rely on external variables, or a class's users could become dependent on fields that should have been hidden. This coupling might not be an issue for the early iterations of the software, but it can lead to problems when an implementation must evolve.

Java developers use the interface language feature to abstract class interfaces, thus decoupling classes from their users. By focusing on Java interfaces instead of classes, you can minimize the number of references to class names in your source code. This facilitates changing from one class to another (perhaps to improve performance) as your software matures. Here is an example:


List names = new ArrayList<>()
void print(List names)
{
 // ...
}

This simple program declares and initializes a names field that stores a list of string names. The program also declares a print() method for printing out the contents of a list of strings, perhaps one string per line. For brevity, I've omitted the method's implementation.

List is a Java interface that describes a sequential collection of objects. ArrayList is a class that describes an array-based implementation of the List Java interface. A new instance of the ArrayList class is obtained and assigned to List variable names. (List and ArrayList are stored in the standard class library's java.util package.)

When client code interacts with names, it will invoke those methods that are declared by List, and which are implemented by ArrayList. The client code will not interact directly with ArrayList. As a result, the client code will not break when a different implementation class, such as LinkedList, is required:


List names = new LinkedList<>()
// ...
void print(List names)
{
 // ...
}

Because the print() method parameter type is List, this method's implementation doesn't have to change. However, if the type had been ArrayList, the type would have to be changed to LinkedList. If both classes were to declare their own unique methods, you might need to significantly change print()'s implementation.

Decoupling List from ArrayList and LinkedList lets you write code that's immune to class-implementation changes. By using Java interfaces, you can avoid problems that could arise from relying on implementation classes. This decoupling is the main reason for using Java interfaces.

Interface declaration

You declare an interface by adhering to a class-like syntax that consists of a header followed by a body. At minimum, the header consists of keyword interface followed by a name that identifies the interface. The body starts with an open-brace character and ends with a close-brace. Between these delimiters are constant and method header declarations:


interface identifier
{
   // interface body
}

By convention, the first letter of an interface's name is uppercased and subsequent characters are lowercased (for example, Drawable). If a name consists of multiple words, the first letter of each word is uppercased (such as DrawableAndFillable). This naming convention is known as CamelCasing.

Listing 2 declares an interface named Drawable.

Listing 2. Declaring a Drawable interface


interface Drawable
{
   int RED = 1;
   int GREEN = 2;
   int BLUE = 3;
   int BLACK = 4;
   int WHITE = 5;
   void draw(int color);
}

Drawable declares five fields that identify color constants. This interface also declares the header for a draw() method that must be called with one of these constants to specify the color used to draw an outline. (Using integer constants isn't a good idea because any integer value could be passed to draw(). However, they suffice in a simple example.)

Drawable identifies a reference type that specifies what to do (draw something) but not how to do it. Implementation details are consigned to classes that implement this interface. Instances of such classes are known as drawables because they know how to draw themselves.

Implementing interfaces

A class implements an interface by appending Java's implements keyword followed by a comma-separated list of interface names to the class header, and by coding each interface method in the class. Listing 3 presents a class that implements Listing 2's Drawable interface.

Listing 3. Circle implementing the Drawable interface


class Circle implements Drawable
{
   private double x, y, radius;
   Circle(double x, double y, double radius)
   {
      this.x = x;
      this.y = y;
      this.radius = radius;
   }
   @Override
   public void draw(int color)
   {
      System.out.println("Circle drawn at (" + x + ", " + y + 
                         "), with radius " + radius + ", and color " + color);
   }
   double getRadius()
   {
      return radius;
   }
   double getX()
   {
      return x;
   }
   double getY()
   {
      return y;
   }
}

Listing 3's Circle class describes a circle as a center point and a radius. As well as providing a constructor and suitable getter methods, Circle implements the Drawable interface by appending implements Drawable to the Circle header, and by overriding (as indicated by the @Override annotation) Drawable's draw() method header.

Listing 4 presents a second example: a Rectangle class that also implements Drawable.

Listing 4. Implementing the Drawable interface in a Rectangle context


class Rectangle implements Drawable
{
   private double x1, y1, x2, y2;
   Rectangle(double x1, double y1, double x2, double y2)
   {
      this.x1 = x1;
      this.y1 = y1;
      this.x2 = x2;
      this.y2 = y2;
   }
   @Override
   public void draw(int color)
   {
      System.out.println("Rectangle drawn with upper-left corner at (" + x1 + 
                         ", " + y1 + ") and lower-right corner at (" + x2 +
                         ", " + y2 + "), and color " + color);
   }
   double getX1()
   {
      return x1;
   }
   double getX2()
   {
      return x2;
   }
   double getY1()
   {
      return y1;
   }
   double getY2()
   {
      return y2;
   }
}

Listing 4's Rectangle class describes a rectangle as a pair of points denoting the upper-left and lower-right corners of this shape. As with Circle, Rectangle provides a constructor and suitable getter methods, and also implements the Drawable interface.

An interface type's data values are the objects whose classes implement the interface and whose behaviors are those specified by the interface's method headers. This fact implies that you can assign an object's reference to a variable of the interface type, provided that the object's class implements the interface. Listing 5 demonstrates.

Listing 5. Aliasing Circle and Rectangle objects as Drawables


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

Because Circle and Rectangle implement Drawable, Circle and Rectangle objects have Drawable type in addition to their class types. Therefore, it's legal to store each object's reference in an array of Drawables. A loop iterates over this array, invoking each Drawable object's draw() method to draw a circle or a rectangle.

Assuming that Listing 2 is stored in a Drawable.java source file, which is in the same directory as the Circle.java, Rectangle.java, and Draw.java source files (which respectively store Listing 3, Listing 4, and Listing 5), compile these source files via either of the following command lines:


javac Draw.java
javac *.java

Run the Draw application as follows:


java Draw

You should observe the following output:


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

Note that you could also generate the same output by specifying the following main() method:


public static void main(String[] args)
{
   Circle c = new Circle(10, 20, 15);
   c.draw(Drawable.RED);
   c = new Circle(30, 20, 10);
   c.draw(Drawable.RED);
   Rectangle r = new Rectangle(5, 8, 8, 9);
   r.draw(Drawable.RED);
}

As you can see, it's tedious to repeatedly invoke each object's draw() method. Furthermore, doing so adds extra bytecode to Draw's class file. By thinking of Circle and Rectangle as Drawables, you can leverage an array and a simple loop to simplify the code. This is an additional benefit from designing code to prefer interfaces over classes.

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 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 Fillable subinterface that extends the Drawable 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

Java 8 introduced two significant enhancements to interfaces: default methods 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. I'll conclude my Java 101 introduction to interfaces with a first look at these features and why they exist.

To continue reading this article register now