Java interfaces, part 1

An interface resembles a class but is not meant for defining a completely functional type; an interface describes the features of a type (usually) without implementation details. A type defined by an interface can be used in usual manner as a reference variable type, but no actual objects of an interface can exist; an object that is compatible with an interface type must be an object of some class that implements the interface. The preceding word “implements” has a similar meaning as the word “inherits” with classes. For example the class ArrayList implements the interface List.

An interface definition has a similar structure as a class definition, but the keyword interface is used instead of class. Below are as an example the definitions of two simple interfaces AutoCloseable and Readable from Java’s class library (they should be in separate files AutoCloseable.java and Readable.java, and include also some import statements):

public interface AutoCloseable {
  void close() throws Exception;
}

public interface Readable {
  int read(CharBuffer cb) throws IOException;
}

Here the interface AutoCloseable defines a type that has a public member function void close(), and the interface Readable a type that has a public member function int read(CharBuffer cb). These functions are only declared without a function body: this type of functions without implementation details are called abstract member functions. A class that implements an interface needs to provide implementations for all abstract member functions declared by the interface. The preceding interfaces did not explicitly define their member functions as public: members of an interface are by default public (but can be explicitly defined as private).

A class can implement several interfaces simultaneosly. A class definition lists the interfaces the class implements by adding the keyword implements after the class name and then listing the implemented interfaces, separated by commas. If the class also inherits some superclass, the keyword extends and the interface list is placed after the name of the inherited superclass.

Below is (a completely artificial) example that shows classes A, B ja C that implement the interfaces AutoCloseable and/or Readable mentioned above.

class A implements AutoCloseable {  // A implements the interface AutoCloseable.
  // @Override signals that we override a function "inherited" from an interface.
  @Override
  public void close() throws Exception {   // A must implement function close of AutoCloseable.
    System.out.println("Closing A...");
  }
}

class B implements AutoCloseable, Readable { // B implements AutoCloseable and Readable.
  @Override
  public void close() throws Exception {        // Also B must implement function close.
    System.out.println("Closing A...");
  }

  @Override
  public int read(CharBuffer cb) throws IOException {  // B must implement function read.
    System.out.println("B is reading...");
    return 0;
  }
}

class C implements Readable {  // C implements the interface Readable.
  @Override
  public int read(CharBuffer cb) throws IOException { // Must implement function read.
    System.out.println("C is reading...");
    return 0;
  }
}

public class InterfaceTest {
  public static void main(String[] args) throws Exception {
    // A and B implement the interface AutoCloseable so they can be handled as
    // AutoCloseable objects. Put A and B objects into an AutoCloseable array.
    AutoCloseable[] acs = {new A(), new B()};
    for(AutoCloseable ac : acs) {
      ac.close();  // We can refer to the member close because AutoCloseable declares it.
    }

    // B and C implement the interface Readable so they can be handled as
    // Readable objects. Put B and C objects into a Readable array.
    Readable[] rs = {new B(), new C()};
    CharBuffer cb = CharBuffer.allocate(0); // An empty CharBuffer just so that we can call read.
    for(Readable r : rs) {
      r.read(cb);  // We can refer to the member read because Reader declares it.
    }
  }
}

To be more precise, a Java interface definition can contain the following parts:

  • Memmer variables that must be public static constants that are initialized directly at the variable definition.

    • The compiler automatically assumes the modifiers public static final, but they can be given also explicitly.

    • Interfaces cannot have other kinds (e.g. private or non-static) of member variables.

  • Public member functions (a function of an interface is by default public).

    • Abstract non-static member functions.

      • Abstract = the function is declared without an implementation (only the header; no function body).

      • An implementing class should provide an actual function implementation.

      • These are probably the most important and widely used aspect of interfaces. They define what kind of member functions, and thus functionality, a class that implements the interface must offer.

    • Non-static member functions with a default implementation.

      • The function header is prepended with the modifier default to declare it as a default function.

      • A full function implementation is given, in similar manner as normal class member functions. But note that such default functions are in practice limited by the fact that interfaces cannot have non-static member variables. A default function thus cannot manipulate non-static member variables directly; only through member functions that would manipulate them (in an implementing class).

      • A class that implements the interface will inherit the default function implementation and thus does not need to define its own implementation.

    • Static member functions (that have full implementations; these cannot be abstract).

      • Implemented in similar manner as static member functions of a class.

  • Private member functions (have the explicit modifier private).

    • Role: internal utility functions of the interface (will probably need rarely).

    • Can be non-static or static and cannot be abstract: a full implementation must be defined.

We will from now on concentrate mostly on the most important part of interfaces: abstract member functions.

The relationship between a class and an interface it implements resembles the relationship of a class and its superclass: implementing an interface means that the implementing class has all properties declared by the interface. Therefore implementing class objects can be handled as if they were objects of the implemented interface. But as stated before, it is not possible to create concrete interface objects. Handling objects as interface objects means that references to an object can have the interface type, and it is legal to refer to members of that interface via the reference. E.g. since the interface AutoCloseable declares a member function close, it is legal to call member function close via a reference of type AutoCloseable.

An interface can also inherit other interfaces. This is done in similar manner as class inheritance, using the keyword extends, but an interface can simultaneously inherit several interfaces. The inheriting interface inherits all public members of its superinterfaces. If an interface inherits one or more other interfaces, then a class that implements the interface must provide implementations for all abstract member functions of all the inherited interfaces.

One example of interface inheritance is the interface Closeable that inherits the interface

AutoCloseable.

public interface Closeable extends AutoCloseable {
  void close() throws IOException;
}

The interface Closeable declares, and overrides an inherited, member function close. The only difference between this and the inherited version is that the interface Closeable refines the exception specification of close to use the exception type IOException instead of Exception. An interface does not need to implement inherited member functions and e.g. here the function close still remains an abstract member function that must be implemented by a class that implements the interface.

Abstract classes

Java allows to define incomplete types also as so-called abstract classes. A class definition can declare the class to be abstract by adding the modifier abstract in front of the keyword class. An abstract class is in some way similar to an interface: concrete abstract class objects cannot be created (must inherit the class and create objects of that), and an abstract class can have abstract member functions (declared without a function body). An abstract class is otherwise an ordinary class. Some of the main differences between interfaces and abstract classes are:

  • An abstract member function of an interface does not need to be declared as such, but each abstract member function of an abstract class must be declared as abstract by adding the modifier abstract in front of the function definition.

  • An interface is implemented using the keyword implements, and an abstract class is inherited using the keyword extends.

  • A class can simultaneosly implement several interfaces but inherit only one abstarct class.

    • Inheriting an abstract class follows normal rules of class inheritance: multiple inheritance is not allowed. The main difference between inheriting a normal and an abstract class is that a class that inherits an abstract class must provide implementations for the inherited abstract functions (unless also the inheriting class is abstract).

  • An abstract class can define all types of members whereas an interface is restricted (e.g. cannot define non-static member variables).

Below is an example abstract class AbstractArray that is meant to be used as a superclass of array containers, and an inheriting class Array. AbstractArray expects that an inheriting class takes responsibility for creating an array and implementing the member functions get, set and size. AbstractArray itself contains, as an example, one function reverse that relies on the three preceding functions implemented by an inheriting class. These classes handle items with Object references and can thus handle all types of objects. The example also shows an example of thworing an exception if the size or an index is illegal. The used exception types are readily available (they belong to the package java.lang).

public abstract class AbstractArray {
  public abstract Object get(int i);               // Gets item at index i.
  public abstract void set(int i, Object item);    // Sets item to index i.
  public abstract int size();                      // Returns the array size.

  public void reverse() {  // Sets the array items into reversed order.
    for(int start = 0, end = size() - 1; start < end; start++, end--) {
      Object tmp = get(start);
      set(start, get(end));
      set(end, tmp);
    }
  }
}

public class Array extends AbstractArray {  // Inherit AbstractArray.
  private Object[] items;        // Object array for storing items.
  private int size;              // Array size.

  public Array(int size) {       // Constructor initializes the array with the specified size.
    if(size < 0) {
      throw new NegativeArraySizeException(String.format("Illegal size: %d", size));
    }
    items = new Object[size];
    this.size = size;
  }

  public Object get(int i) {
    if(i < 0 || i >= size) {
      throw new IndexOutOfBoundsException(String.format("Illegal index: %d", i));
    }
    return items[i];
  }

  public void set(int i, Object val) {
    if(i < 0 || i >= size) {
      throw new IndexOutOfBoundsException(String.format("Illegal index: %d", i));
    }
    items[i] = val;
  }

  public int size() {
    return size;
  }
}

An Array array could be used e.g. as follows:

Array arr = new Array(3);
arr.set(0, "One");
arr.set(1, "Two");
arr.set(2, "Three");
System.out.format("%s %s %s%n", arr.get(0), arr.get(1), arr.get(2));
arr.reverse();
System.out.format("%s %s %s%n", arr.get(0), arr.get(1), arr.get(2));

The code would output:

One Two Three
Three Two One

Abstract classes are used mostly as superclasses of classes that share similar functionality, in which case the abstract class can provide some common functionality (e.g. member functions that the inheriting classes can use). The Java class library e.g. contains an abstract class AbstractList that is meant to be used as a superclass of array based list containers. It provides ready functionality for many basic operations of such containers. For example ArrayList inherits AbstractList. The basic ideas illustrated in the preceding example code are in some sense similar to these classes. To be more precise, ArrayList and AbstractList are generic classes. Generics will be discussed soon.

Type cast and inspecting object type

At this point it should be clear that a subtype object can always be referred to by a supertype reference, as the subtype inherits / implements the properties of the supertype. Since the set of members that can be referred to is defined by the type of the reference, only members of the supertype can be referred to via a supertype reference. For example the following is illegal because the supertype Object does not have a member function length:

Object o = "I am a string";
o.length();      // Compiler error: cannot find symbol o.length().

If we want in this kind of a situation to refer to a member of the objects own subtype (that the supertype does not have), we must use a reference whose type corresponds to the object’s subtype. This requires an explicit type cast because a conversion from a supertype to a subtype is not guaranteed to be legal. For example below the first attempt to refer to o as String would produce a compiler error but the second attempt is legal:

Object o = "I am a string";
String s = o; // Compiler error: Object cannot be converted to String.
String s = (String) o;     // Legal because o really is a String.

Java will check the legality of a type conversion between reference types during runtime. A ClassCastException exception will be thrown if the cast object is not compatible with the cast target type. For example below the illegal attempt to cast a String object o into Integer results in an exception.

Object o = "I am a string";
try {
  Integer i = (Integer) o;     // Illegal conversion String -> Integer.
}
catch(ClassCastException e) {
  System.out.println("An illegal cast brought me here...");
}

It is important to note that the legality of the type conversion depends only on the actual type of the object; the type of the previous reference to that object does not matter. It is also important to note that a type conversion between reference types does not affect the “converted” object in any way: it only modifies the type of the reference. The object’s own actual type and features remain untouched.

The actual type of an object can be inspected during runtime by e.g. using the instanceof operator. An operation of form o instanceof T produces a truth value that tells whether the object o is compatible with the type T. This is thus not limited to checkin only the actual own type of the object: the result is true also if T os some supertype of o. It is legal to convert the object o to type T if and only if o instanceof T is true.

Let us as an example inspect types compatible with a Float object. The class Float has superclasses Number and Object and it also implements e.g. the interface Constable (which is an interface related to how a Java virtual machine represents constants). The code below inspects the compatibility of an Float object with these types and the class Double. The format specifier %b used in printing the output corresponds to a truth value.

Object o = 3.14F;
System.out.format("o instanceof Object: %b%n", o instanceof Object);
System.out.format("o instanceof Number: %b%n", o instanceof Number);
System.out.format("o instanceof Float: %b%n", o instanceof Float);
System.out.format("o instanceof Constable: %b%n", o instanceof Constable);
System.out.format("o instanceof Double: %b%n", o instanceof Double);

The code would output:

o instanceof Object: true
o instanceof Number: true
o instanceof Float: true
o instanceof Constable: true
o instanceof Double: false

The last line reflects how type conversions between reference types are based on type hierarchy and not some kind of feasible value compatibility. Therefore e.g. below the first conversion fails (as f instanceof Double is false), but the second conversion is ok due to how it is done indirectly, via boxing conversions, as a conversion between primitive values.

Float f = 3.14F;
Double d = (Double) f;  // Compiler error: Float cannot be converted to Double.
Double d = (double) f;  // Ok: Float -> float -> double -> Double.

Which of the following hold in Java: