DEV Community

Sergiy Yevtushenko
Sergiy Yevtushenko

Posted on • Updated on

Interface-only programming in Java

Combination of default methods in Java interface together with Monad pattern enables writing 'interface only' classes. There classes don't have separate implementation classes, just interfaces. Well, of course, technically they have implementation, but that implementation is an inline tiny anonymous class.

I'll demonstrate the idea by writing (incomplete) implementation of Maybe monad, sometimes called Optional or Option. This container is used to represent values which might be present or missing. For example Map.get(key) can use this container to represent result of the lookup. If value is found then Some value is returned, otherwise None (empty container) is returned.

Java 8 has Optional which implements such a behavior, so we can look at it's API and implement something similar:

public interface Option<T> {
    <U> Option<U> map(final Function<T, U> mapper);

    <U> Option<U> flatMap(final Function<T, Option<U>> mapper);

    Option<T> ifPresent(final Consumer<T> consumer);

    Option<T> ifEmpty(final Runnable consumer);

    T otherwise(final T replacement);
}
Enter fullscreen mode Exit fullscreen mode

If we try to implement this interface straightforward, we should create a class which will hold value. Then in every method we'll check if that value is null and if yes, do one action and if not - do other action. Looks like code smell - repeating logic. Let's extract this logic into method, which will accept two functions and will call them depending on the Option state (i.e. value is present or not):

   <U> U map(final Function<T, U> presentMapper, final Supplier<U> emptyMapper);

Enter fullscreen mode Exit fullscreen mode

Now we can represent all remaining methods using this new map() method:

    default <U> Option<U> map(final Function<T, U> mapper) {
        return Option.option(map(mapper, () -> null));
    }

    default <U> Option<U> flatMap(final Function<T, Option<U>> mapper) {
        return map(mapper, Option::empty);
    }

    default Option<T> ifPresent(final Consumer<T> consumer) {
        map(v -> {consumer.accept(v); return null;}, () -> null);
        return this;
    }

    default Option<T> ifEmpty(final Runnable consumer) {
        map(v -> v, () -> { consumer.run(); return null;} );
        return this;
    }

    default T otherwise(final T replacement) {
        return map(v -> v, () -> replacement);
    }

Enter fullscreen mode Exit fullscreen mode

Now all we need is two static factory methods - one for creating empty container and one for container with value. Since for all functionality we need only one method, we can use anonymous classes:

    static <T> Option<T> option(final T value) {
        return (value == null) ? empty() : new Option<T>() {
            @Override
            public <U> U map(final Function<T, U> presentMapper, final Supplier<U> emptyMapper) {
                return presentMapper.apply(value);
            }
        };
    }

    static <T> Option<T> empty() {
        return new Option<T>() {
            @Override
            public <U> U map(final Function<T, U> presentMapper, final Supplier<U> emptyMapper) {
                return emptyMapper.get();
            }
        };
    }

Enter fullscreen mode Exit fullscreen mode

That's it, all necessary functionality is implemented inside interface.

Interestingly enough is that there is only one conditional operator in the whole code. It is necessary just because we followed Java convention to recognize null as missing value. This is not strictly necessary and this implementation may hold null values and still will be able to distinguish present and missing values.
Some other interesting observations:

  • Empty instance actually holds no value, there is no even field for it (unlike Java 8 Optional). We can optimize implementation and return same instance for empty container every time.
  • There is no explicit instance variable for stored value, it is implicitly stored by Java compiler while creating non-empty instance.
  • Lack of branching inside code should help achieve better performance at run time. In this case gain will be negligible, but the technique is general and might be used in cases where gain might be significant.

Top comments (0)