5

The functional interfaces in java.util.function cover the vast majority of common strategies one might want to apply, but it's also possible to define your own @FunctionalInterface instead. In cases where your desired interface coincides with an existing interface in java.util.function should you prefer defining a custom interface over reuse? Why or why not?

As a concrete example (though I'm looking for answers to the general question), should a function that expects a sorting operation take a Consumer<List<T>> and stipulate that it sort the input, or should a separate interface be defined that specifies a sort(List<T>) method?

2
  • Old dependency injection containers do not support generic types safely, in that case it makes sense to extend a standard interface with your own.
    – Basilevs
    Commented Apr 25, 2017 at 3:50
  • That doesn't seem like a great rationale - generics have been part of the language for 10+ years; use a different framework.
    – dimo414
    Commented Apr 25, 2017 at 3:59

1 Answer 1

7

First off this is largely a matter of style and API design, because callers can always convert from one equivalent functional interface to another via method references. So if you have a Consumer<T> instance and need to pass it to a method that expects a MyConsumer<T>, you can do so as:

myFunction(someConsumer::accept);

Since it's a matter of style reasonable people can disagree, but here's what I would suggest considering:

  • Your goal as the API author should be to minimize opportunities for misuse.
  • The interfaces defined in java.util.function are general-purpose, meaning their API contracts are very weak. They essentially stipulate nothing more than their method signatures imply, which can lead to errors if you actually have stronger requirements.
  • An interface you create can define a stricter contract, which can aid your users when they attempt to use your code. As Jules points out, simply defining a more precisely-named interface can significantly improve the readability and usability of your API.
  • That said, users become obligated to read your interface's contract in order to ensure they are following it correctly, which is an added burden and can also be a source of errors.

With those thoughts in mind, I would suggest defining your own interface any time your code requires more of its callers than the general-purpose interfaces imply.

For example a method that expects a sorting strategy would fail to behave correctly if passed a consumer that doesn't actually sort the inputs, meaning that accepting arbitrary Consumer<List<T>> implementations isn't desirable, and specifying your own Sorter interface makes your API clearer and less likely to accidentally be misused.

On the other hand many functional/strategy operations are fully compatible with the contracts of the general-purpose interfaces, in which case you shouldn't hesitate to reuse them. Even if some implementations aren't "correct" per se, as long as your code still behaves appropriately when passed such implementations there's no need to make the contract more complex by defining your own interface.

As another example consider Stream.map() - the intent of this function is to apply some operation to the input and return the transformed result, but there's nothing stopping a caller from passing in an operation that doesn't actually do that, such as one that just returns an arbitrary result, disregarding the input:

myIntStream.map(i -> random.nextInt())

This isn't in keeping with the spirit of map() (and it's an odd way to generate n random ints...), but it doesn't break anything - .map() still behaves correctly and as expected the stream now contains some number of random integers. Therefore it's reasonable that .map() takes an arbitrary Function and not some more strictly defined interface.

2
  • 1
    Naming is another important factor. While you can convey a lot of meaning in the variable name that stores the functional object, if its type or particular the name of its method conflicts with that meaning, the result can be confusing to read. The sorting example illustrates this; both Consumer and its method "accept" imply passivity, and don't really suit well with an operation that modifies its argument. This may indicate you should create a Sorter interface, or maybe simply a Mutator interface with a generic but clearly active method name like "execute".
    – Jules
    Commented Apr 23, 2017 at 11:27
  • Great point @Jules, I'd meant to mention that.
    – dimo414
    Commented Apr 23, 2017 at 16:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.