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 Drawable
s. 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 Drawable
s, 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:
- Change
implements Drawable
toimplements Fillable
. There is no need to specify eitherimplements Drawable, Fillable
orimplements Fillable, Drawable
becauseFillable
includes all ofDrawable
by extension. - Override the
fill()
method header in the same manner as overriding thedraw()
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 Fillable
s. 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.