Search code examples
javafunctional-programmingjava-8functional-interface

How is a simple setter interpreted as Consumer<T>?


First off, please bear with me. Most of the time I am working in Scala (and only sometimes on the JVM side) or other languages, so my Java (8) knowledge is a bit limited!

Code I have to refactor is littered with null checks. I wanted to make the setting/overriding of some pojo's attributes a bit nicer and got excited being able to use Java 8 for the job.

So I created this:

private <T> void setOnlyIfNotNull(final T newValue, Consumer<T> setter) {
    if(newValue != null) {
        setter.accept(newValue);
    }
}

And used it like this:

setOnlyIfNotNull(newUserName, user::setName);

The junit4-test looks like this:

@Test
public void userName_isOnlyUpdated_ifProvided() {
   User user = new User("oldUserName");

   UserUpdateRequest request = new UserUpdateRequest().withUserName("newUserName");   
service.updateUser(user, request); // This calls setOnlyIfNotNull behind the curtain. And it does invoke the setter ONLY once!

 assertThat(user.getUserName()).isEqualTo("newUserName");
}

And this works. I was quite content with myself until I asked a colleague for a code review. Upon explaining what I did he elaborated that he thought this could not work, as functions are still no first class citizens in Java AND the User-pojo did not extend a FunctionalInterface. Also interfaces are provided on class level, not function level.

Now I wonder, why does the test work and am I misusing something here? Naive me simply imagined that the Java compiler knew that a setter's T setT(T value) signature was the same as for a Consumer<T>.

edit: To elaborate a bit more: If I change the test to fail e.g. with assertThat(user.getUserName()).isEqualTo("Something"); it fails with a comparisonFailure as expected!


Solution

  • The review is wrong on several counts:

    1. FunctionalInterface is not required for an interface to be functional. It's just a compiler hint to flag an error when something marked with this annotation doesn't meet the criteria (being an interface with only one abstract method)

      If a type is annotated with this annotation type, compilers are required to generate an error message unless:

      • The type is an interface type and not an annotation type, enum, or class.
      • The annotated type satisfies the requirements of a functional interface.

      However, the compiler will treat any interface meeting the definition of a functional interface as a functional interface regardless of whether or not a FunctionalInterface annotation is present on the interface declaration.

    2. In any case, it isn't User that should be functional, it's the Consumer interface, which is functional.
    3. While functions indeed may not be first-class values (although see here for more about this), user::setFoo is not a "raw" function, it's a construct that creates an object that implements Consumer (in this case), and calls user.setFoo() with whatever argument is passed in. The mechanism is superficially similar to how anonymous inner classes declare a class and create an instance straight away. (But there are crucial differences in the mechanism behind it.)

    But the strongest argument is that your code demonstrably works, using only the official and documented API of Java. So saying "but Java doesn't support this" is kind of a weird idea.