ylem
ylem

Reputation: 157

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?

Upvotes: 3

Views: 3483

Answers (3)

Holger
Holger

Reputation: 298153

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(); }.

Upvotes: 1

user9588409
user9588409

Reputation:

To my mind it’s not a good idea to transform a regular interface that can be referenced as data type to functional interface.

If you do this then user can implement the StringTransform where they need actually this interface as a type and where they create lambdas. That reduces readability and maintainability.

Upvotes: 0

Neel Patel
Neel Patel

Reputation: 368

Theoretically there is nothing wrong wit h it. You can use this type of interfaces. I would like to start with the conditions where you need this type of construct.

Backward capability

As you mentioned, default methods are introduced to maintain backward capability, so usage of default methods to make interface backward compatible is justified.

Optional implementation

Default empty methods can be used to make implementation of any method optional. However, you can make a abstract adaptor class of such interface with a default implementation. This strategy has been used for long time and it is better than default methods in interface as with abstract class, we can define complex default implementation. Moreover, we can start with a simple empty method at the beginning and change it later to have complex default implementation while in case of default methods in interface it is not possible as certain functionalities of class is not available.

In addition, Java 8 has also introduced FunctionalInterface to mark any interface as a functional interface. According to official guide, usage of lambda & method reference should be avoided in case of interfaces having one method but not annotated with FunctionalInterface.

Upvotes: 0

Related Questions