3

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!

  • 3
    I don't know why your colleague said all that, but it doesn't have a lot to do with your code. `Consumer` is a functional interface, and it matches the `user.setName()` method because its method signature conforms to the `accept` method of the `Consumer` interface. – Jesper May 02 '17 at 10:48
  • 1
    @Lichtbringer invite your colleague to write an answer/comment here? – Eugene May 02 '17 at 10:49

1 Answers1

6

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.

Community
  • 1
  • 1
biziclop
  • 48,926
  • 12
  • 77
  • 104
  • So does `user::setFoo` behind the scenes create an anonymous class that takes the user as input alongside the function to be called? –  May 02 '17 at 10:54
  • 2
    @Lichtbringer Sort of, yes. You can test it by calling `setter.getClass()` in your method. – biziclop May 02 '17 at 10:55
  • Is there a way to use a lambda directly instead of having to refrain to the `::` syntax? –  May 02 '17 at 10:55
  • 1
    @Lichtbringer How about `name -> user.setFoo(name)` – Flown May 02 '17 at 11:12
  • @Flown as I do `setter.accept(newValue)` in the method this looks clattered. I should've been clearer. I know how to replace it, in general, just not how one would do it here as argument for another function. –  May 02 '17 at 11:22
  • 1
    @Lichtbringer I don't get it. What do you want to do? – Flown May 02 '17 at 11:29
  • @Flown replace: `setOnlyIfNotNull(newUserName, user::setName);` with `setOnlyIfNotNull(newUserName, {AWESOME_LAMBDA});` –  May 02 '17 at 11:31
  • 2
    @Lichtbringer It hasn't changed: `setOnlyIfNotNull(newUserName, name -> user.setName(name));` – Flown May 02 '17 at 11:33
  • 2
    Well, functions can’t be “first-class objects” as in Java, they are not objects. Whether they are “first-class values”, might be controversial. E.g. see [this answer](https://stackoverflow.com/a/15241404/2711488) and its 2013-10-24 revision… – Holger May 02 '17 at 12:40
  • 1
    @Holger Yes, that's sort of what I was getting at, that this is a very different mechanism than what you get in Javascript for example. – biziclop May 02 '17 at 13:36