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 keywordextends
.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.
Programming demo (duration 1:07:56)