Reputation: 157
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
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
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
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