Blog-Archiv

Sonntag, 14. März 2021

Java Interface as Function Pointer

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).

Why Would We Need That?

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.

Example

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".

Interfaces as Object Aspects

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.

Functions since Java 1.8

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 is get(), 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, whereby default and static 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.

Built-in Functional Interfaces

Basic built-in functional interfaces (with their single functions) are:

  • T Supplier.get()
    T = return-type

  • void Consumer.accept(T parameter)
    T = parameter-type

  • R Function.apply(T parameter)
    R = return-type, T = parameter-type

  • boolean Predicate.test(T parameter)
    always returns boolean, T = parameter-type

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.

Conclusion

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.




Keine Kommentare: