As shown in this answer, you can implement your interface by lambda expressions processing the varargs parameters, e.g. with a Stream. But you can also design an interface for associative operations providing default methods in addition to the two argument version:
interface Calculator<T> {
T operation(T n1, T n2);
default T operation(T first, T... args) {
return Stream.concat(Stream.of(first), Arrays.stream(args))
.reduce(this::operation).orElseThrow();
}
}
Note that the method signature operation(T first, T... args)
enforces that there is at least one element, which allows to use reduce
without an identity element or fallback value for empty streams.
You can use it like
Calculator<Integer> addition = (n1, n2) -> n1 + n2;
Integer sum = addition.operation(1, 2, 3, 4);
But the operation must be associative, like addition, multiplication, or string concatenation, etc. If you want to support non-associative operations, those operations would have to override operation(T first, T... args)
with an appropriate implementation. Overriding it doesn’t work with lambda expressions. The alternative would be to have an interface for arbitrary calculations and a specialization for associative operations:
interface Calculator<T> {
T operation(T first, T... args);
}
interface AssociativeOp<T> extends Calculator<T> {
T operation(T n1, T n2);
@Override
default T operation(T first, T... args) {
return Stream.concat(Stream.of(first), Arrays.stream(args))
.reduce(this::operation).orElseThrow();
}
}
Then, non-associative operations can implement Calculator<T>
via a lambda expression handling all arguments while associative operations can be implemented like:
Calculator<Integer> addition = (AssociativeOp<Integer>)(n1, n2) -> n1 + n2;
The necessity for a type cast could be eliminated by a factory method, using type inference for its parameter. However, when we are at this point, we could remember that there is already a functional interface type for operations with two arguments which the examples are even using, implicitly. So another alternative would be:
interface Calculator<T> {
T operation(T first, T... args);
static <U> Calculator<U> associative(BinaryOperator<U> op) {
return (first,other) -> Stream.concat(Stream.of(first), Arrays.stream(other))
.reduce(op).orElseThrow();
}
}
This still allows arbitrary implementations via lambda expression while associative operations can be implemented with a simplified:
Calculator<Integer> addition = Calculator.associative((n1, n2) -> n1 + n2);
(Or Calculator.associative(Integer::sum)
)