9

As a rule, in the context of a large project, is it considered safe to take make an existing, ubiquitously used interface into a functional interface?

E.g., given an existing interface and class:

public interface Interface {
    public double calculateSomething(double x);
    public void doSomething();
}

which is implemented by

class InterfaceImplementer implements Interface {

     public double calculateSomething(double x) {
          return 2 * x;
     }

     public void doSomething() {
         // insert specific behavior here
     } 
}

can I safely change the interface by defining all but one method as default:

public interface Interface {
    public double calculateSomething(double x);

    default void doSomething() {
         // insert some default behavior here
    }
}

So that I can go from defining an object as

Interface object = new InterfaceImplementer() {

    @Override
    public double calculateSomething(double x) {
        return 2 * x;
    }
};

to

Interface object = (x) -> 2 * x;

while still being able to define objects in the old, tedious way.

From what I can tell, this runs no risk of upsetting any existing code, and I've made such a change to a large project and had no runtime or compile errors. But I want some confirmation whether this matches up with common knowledge and best practices.

MC Emperor
  • 22,334
  • 15
  • 80
  • 130
Frank Harris
  • 587
  • 2
  • 16

4 Answers4

8

Any interface that only has a single non-default method (only one method needs to be implemented in a class) is by definition a functional interface. This is a good rule!

However, a @FunctionalInterface annotation has the advantage of enforcing the "only one method in the interface for a functional interface"-rule. So if you added it to your original two-method interface, you would have gotten a compiler error. Therefore by explicitly adding @FunctionalInterface you declare your intent and make your code more clear to future maintainers.

Thorbjørn Ravn Andersen
  • 73,784
  • 33
  • 194
  • 347
  • Previously, it had many non-default methods. The change I made was adding default behavior to all but one (in practice, that one method is overridden for every instance of the implementer class, hence it being ideal for making it functional). So I don't need to make any changes to the implementer, but I can avoid needing to use the implementer in most cases. The question is whether there is any risk in making those non-default methods into default methods. Again, I *think* the answer to that is a very clear no, but I want to make sure I'm not overlooking something. – Frank Harris Sep 29 '17 at 21:08
  • Risk? It sounds to me that you may end up essentially with an abstract class thinly disguised as an interface (which in many ways could make sense, but in my experience it is not a good idea overdoing the defaults - those were made to be able to add extra code to existing interfaces, not to build classes with). I would consider refactoring to make intent clearer. – Thorbjørn Ravn Andersen Sep 29 '17 at 23:14
3

On java code level, I can think of one problem: since this interface already had contained 2 methods at some point in the past, you may want to add another method to it later on. You won't be able to add another method to a functional interface, since it has to remain a functional interface so you can use it as a functional interface. You will have to create an interface that inherits from this one. Which leads me to the main point.

It may have been logical to have those 2 methods in one interface before, but is it really logical now? Refactor the code, separate the interfaces; either make one extend another or use an interface that inherits from both, your call. If the interface is to be used as a functional one, make it functional. It will be clean. It will be understandable. You will be able to add methods to one of those interfaces in the future without further refactoring.

Dariusz
  • 21,561
  • 9
  • 74
  • 114
  • If you want to add a method in the future, can't you just make it default? – Frank Harris Sep 29 '17 at 20:35
  • @FrankHarris true, but again, these points are not "risky", the way you worded your question sounds like you're asking if this could be a "breaking change" – Nir Alfasi Sep 29 '17 at 20:39
  • That is the question, yes. The two major constraints are that I don't have the resources to do a large refactoring, and that this small refactoring can't break anything. My *understanding* is that this is so; if it worked before the change, it must work after the change, but it allows me to make more efficient use of the interface in the future, without having to make any *other* changes to existing code. With respect to not being able to add more methods, it seems like if it made sense not to be a functional interface in the future, I could do that larger refactoring then? – Frank Harris Sep 29 '17 at 21:03
1

The Java API states:

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.

Therefore there is no risk to add that annotation.

Timothy Truckle
  • 15,071
  • 2
  • 27
  • 51
  • Ahh, I put the annotation in there for clarity, but that's not what I'm asking. The initial interface wasn't functional since it had more than one non-default method. What I'm asking about is changing `doSomething()` defined in the interface from `public` to `default` and defining it as `doAThing()`. Does that clarify? – Frank Harris Sep 29 '17 at 20:20
  • 1
    @FrankHarris what you wrote here in the comment is *not* what you wrote in the question above. – Nir Alfasi Sep 29 '17 at 20:38
1

Well, it's not the @Functional that might break anything, but adding a default implementation may lead to compilation errors for abstract classes that implement or interfaces that extend multiple interfaces declaring methods with override-equivalent signatures:

The following compiles fine:

interface I {
    void a();
}

interface J {
    void a();
}

interface K extends I, J {}

while this doesn't:

interface I {
    default void a() {}
}

interface J {
    void a();
}

interface K extends I, J {}

The default method a() inherited from I conflicts with another method inherited from J

So if you do this in a library, code using this may fail to compile after the change.

Hulk
  • 6,399
  • 1
  • 30
  • 52