Java interfaces can mark component boundaries, but their core capability is separation. Thus they can also isolate functions, which, in OO terms, are methods of an object. That means, through interfaces we can separate a method from all other methods of its object context and pass it around like any other object, without losing strong typing.
This Blog is about how you can simulate C function pointers in Java (before and after version 1.8).
Imagine a class that exposes setter-methods, but you want to hide these setters from a related class the object is given to as parameter. In no case the related class should be able to call one of the setters, you want to expose just a getter.
This is called information hiding, an important tool to reduce complexity.
Following example is about having Person
objects in memory that should be checked for uniqueness using their getId()
method.
public class Person
{
private final String id;
private String name;
public Person(String id) {
this.id = id;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Mind that the id
field is immutable to guarantee that it never changes in the lifetime of a Person
object.
Here is the UniqueRegistration
class that checks for uniqueness:
public class UniqueRegistration
{
private final Set<String> registry = new HashSet<>();
public void register(Person person) {
final String key = person.getId();
if (key == null)
throw new IllegalArgumentException("No id available: "+person);
if (registry.contains(key))
throw new IllegalArgumentException("Already registered: "+person);
registry.add(key);
}
}
Unfortunately this registry has full access to all methods of Person
, it could call setName()
or getName()
or any other public
method.
Example application:
final Person john = new Person("John");
final Person jill = new Person("Jill");
final UniqueRegistration uniqueRegistration = new UniqueRegistration();
uniqueRegistration.register(john);
uniqueRegistration.register(jill);
Now it is about hiding person.setName()
and getName()
from uniqueRegistration.register()
, just exposing the getId
"function pointer".
We create an abstraction of Person
that focuses on its identity and nothing else:
public interface Identifiable
{
String getId();
}
Let Person
implement that interface:
public class Person implements Identifiable
{
// ... implementation same as above
}
And rewrite UniqueRegistration
to use the Identifiable
interface instead of Person
:
public class UniqueRegistration
{
// ...
public void register(Identifiable identifiable) {
final String key = identifiable.getId();
// ... implementation same as above
}
}
That's it! We separated the getId()
method from the Person
class, but we did not tear the two apart. Still the method will be connected to its object and read the correct id
field at runtime. Nevertheless UniqueRegistration
will not see anything else than getId
.
In Java 1.8, function pointers have been integrated into the language. Here is an implementation of UniqueRegistration
that uses a built-in functional interface instead of Identifiable
:
import java.util.function.Supplier;
public class UniqueRegistration
{
// ...
public void register(Supplier<String> identifiable) {
final String key = identifiable.get();
// ... implementation same as above
}
}
The
Supplier
interface is a simple functional interface contained in the Java runtime library since 1.8. Its one-and-only function isget()
, without parameters:package java.util.function;
@FunctionalInterface
public interface Supplier<T>
{
T get();
}The
@FunctionalInterface
annotation is there to ensure that just one method (the "function") is inside the interface. The compiler would report an error when you put this annotation onto an interface with more than one method, wherebydefault
andstatic
methods do not count.
The example application now accesses the function through a Java syntax extension added in 1.8 (context::function
) like here:
final Person john = new Person("John");
final Person jill = new Person("Jill");
final UniqueRegistration uniqueRegistration = new UniqueRegistration();
uniqueRegistration.register(john::getId);
uniqueRegistration.register(jill::getId);
Mind that Person
doesn't need to implement Identifiable
now any more. We can remove that interface.
Basic built-in functional interfaces (with their single functions) are:
T Supplier.get()
void Consumer.accept(T parameter)
R Function.apply(T parameter)
boolean Predicate.test(T parameter)
These four functional interfaces represent archetypical method signatures, and they are generically typed to be reusable. The Java runtime library contains many more variants of these four. In case none satifies your needs, simply write your own @FunctionalInterface
.
Java interfaces are a powerful technique, well usable in separation concerns. Since Java 1.8, interfaces have become even better than abstract classes, because they also allow implementations.
Anyway we shouldn't hide any class behind an interface. Introducing interfaces just to be able to write class-oriented mock tests goes too far in my opinion. Such tests see any class as component, but in reality it isn't. There are not many classes that can exist without related classes, and only such a graph of classes should be regarded as component.
ɔ⃝ Fritz Ritzberger, 2021-03-14