Search code examples
javainterfacejava-8functional-interface

Using (empty) default method to make FunctionalInterface


In Java 8, default methods for an interface were introduced for adding methods to existing interface without breaking backward compatibility.

Since default methods are non-abstract, they can be used to make a FunctionalInterface with multiple overridable methods.

Say, a StringTransformer interface has two methods, transform, which transforms given String, and end to free resources:

interface StringTransformer {
    String transform(String s);

    void end();
}

But some implementations may not have resources to free, so we can provide empty default method for end and use lambda function and method reference for StringTransformer:

interface StringTransformer {
    String transform(String s);

    default void end() {
    }
}

StringTransformer x = String::trim;
StringTransformer y = (x -> x + x);

Is this a valid/best practice, or an anti-pattern and abuse of default methods?


Solution

  • As said in this answer, allowing to create interfaces with more than one method still being functional interfaces, is one of the purposes of default methods. As also mentioned there, you will find examples within the Java API itself, say Comparator, Predicate, or Function, having default methods and intentionally being functional interfaces.

    It doesn’t matter whether the default implementation is doing nothing or not, the more important question is, how natural is this default implementation. Does it feel like a kludge, just to make lambdas possible, or is it indeed what some or even most implementations would use any way (regardless of how they are implemented)?

    Not needing a special clean up action might be indeed a common case, even if you follow the suggestion made in a comment, to let your interface extend AutoCloseable and name the method close instead of end. Note that likewise, Stream implements AutoCloseable and its default behavior is to do nothing on close(). You could even follow the pattern to allow specifying the cleanup action as separate Runnable, similar to Stream.onClose(Runnable):

    public interface StringTransformer extends UnaryOperator<String>, AutoCloseable {
        static StringTransformer transformer(Function<String,String> f) {
            return f::apply;
        }
        String transform(String s);
        @Override default String apply(String s) { return transform(s); }
        @Override default void close() {}
        default StringTransformer onClose(Runnable r) {
            return new StringTransformer() {
                @Override public String transform(String s) {
                    return StringTransformer.this.transform(s);
                }
                @Override public void close() {
                    try(StringTransformer.this) { r.run(); }
                }
            };
        }
    }
    

    This allows registering a cleanup action via onClose, so the following works:

    try(StringTransformer t = 
            StringTransformer.transformer(String::toUpperCase)
                             .onClose(()->System.out.println("close"))) {
        System.out.println(t.apply("some text"));
    }
    

    resp.

    try(StringTransformer t = transformer(String::toUpperCase)
                             .onClose(()->System.out.println("close 1"))) {
        System.out.println(t.apply("some text"));
    }
    

    if you use import static. It also ensures safe closing if you chain multiple actions like

    try(StringTransformer t = transformer(String::toUpperCase)
                             .onClose(()->System.out.println("close 1"))
                             .onClose(()->{ throw new IllegalStateException(); })) {
        System.out.println(t.apply("some text"));
    }
    

    or

    try(StringTransformer t = transformer(String::toUpperCase)
                             .onClose(()->{ throw new IllegalStateException("outer fail"); })
                             .onClose(()->{ throw new IllegalStateException("inner fail"); })){
        System.out.println(t.apply("some text"));
    }
    

    Note that try(StringTransformer.this) { r.run(); } is Java 9 syntax. For Java 8, you would need try(StringTransformer toClose = StringTransformer.this) { r.run(); }.