14

I'm designing an interface with two related methods, similar to this:

public interface ThingComputer {
    default Thing computeFirstThing() {
        return computeAllThings().get(0);
    }

    default List<Thing> computeAllThings() {
        return ImmutableList.of(computeFirstThing());
    }
}

Around half the implementations will only ever compute one thing, while the other half may compute more.

Does this have any precedent in widely-used Java 8 code? I know Haskell does similar things in some typeclasses (Eq for example).

The upside is I have to write significantly less code than if I had two abstract classes (SingleThingComputer and MultipleThingComputer).

The downside is that an empty implementation compiles but blows up at runtime with a StackOverflowError. It's possible to detect the mutual recursion with a ThreadLocal and give a nicer error, but that adds overhead to non-buggy code.

  • 3
    Why would you ever deliberately write code guaranteed to have infinite recursion? Nothing good can possibly come from that. – user22815 Jun 18 '15 at 20:03
  • 1
    Well it's not "guaranteed"; a proper implementation would override one of those methods. – Tavian Barnes Jun 18 '15 at 20:04
  • 6
    You do not know that. A class or interface needs to stand on its own and not assume that a subclass will do things a certain way to avoid major logic errors. Otherwise, it is brittle and cannot live up to its guarantees. Note that I am not saying your interface can or should enforce an implementing class doing throw new Error(); or something stupid, only that the interface itself should not have a brittle contract through default methods. – user22815 Jun 18 '15 at 20:10
  • I agree with @Snowman, it's a landmine. I think it's textbook "evil code" in the sense that Jon Skeet means - note that evil is not the same as bad, it's wicked / clever / tempting, but still wrong :) I recommend youtu.be/lGbQiguuUGc?t=15m26s (or indeed, the entire talk for broader context - it's very interesting). – Konrad Morawski Jun 18 '15 at 23:00
  • 1
    Just because each function can be defined in terms of the other does not imply that they can both be defined in terms of each other. That doesn't fly in any language. You want an interface with 2 abstract inheritors, each implements one in terms of the other but abstracts the other so implementors choose which side they want to implement by inheriting from the correct abstract class. One side must be defined independent of the other, or else pop goes the stack. You have insane defaults presuming implementors will resolve the issue for you, abstract exists to force them to resolve it. – Jimmy Hoffa Jun 19 '15 at 2:57
16

TL;DR: do not do this.

What you show here is brittle code.

An interface is a contract. It says "regardless of what object you get, it can do X and Y." As it is written, your interface does neither X nor Y because it is guaranteed to cause a stack overflow.

Throwing an Error or subclass indicates a severe error that should not be caught:

An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.

Furthermore, VirtualMachineError, the parent class of StackOverflowError, says this:

Thrown to indicate that the Java Virtual Machine is broken or has run out of resources necessary for it to continue operating.

Your program should not be concerned with JVM resources. That is the job of the JVM. Making a program that causes a JVM error as a part of normal operation is bad. It either guarantees your program will crash, or compels users of this interface to trap errors it should not be concerned with.


You might know Eric Lippert from such endeavors as emeritus "member of the C# language design committee." He talks about language features pushing people toward success or failure: while this is not a language feature or part of language design, his point is equally as valid when it comes to implementing interfaces or using objects.

You remember in The Princess Bride when Westley wakes up and finds himself locked in The Pit Of Despair with a hoarse albino and the sinister six-fingered man, Count Rugen? The principle idea of a pit of despair is twofold. First, that it is a pit, and therefore easy to fall into but difficult work to climb out of. And second, that it induces despair. Hence the name.

Source: C++ and the Pit Of Despair

Having an interface throw a StackOverflowError by default pushes developers into the Pit of Despair and is bad idea. Instead, push developers toward the Pit of Success. Make it easy to use your interface correctly and without crashing the JVM.

Delegating between the methods is fine here. However, the dependency should go one way. I like to think of method delegation like a directed graph. Each method is a node on the graph. Each time a method calls another method, draw an edge from the calling method to the method called.

If you draw a graph and notice it is cyclic, that is a code smell. That is a potential for pushing developers in the Pit of Despair. Note that it should not be categorically forbidden, only that one must use caution. Recursive algorithms specifically will have cycles in the call graph: that is fine. Document it and warn developers. If it is not recursive, try to break that cycle. If you cannot, find out what inputs might cause a stack overflow and either mitigate them or document it as a last case if nothing else will work.

  • Great answer. I'm curious why this is such a common practice in Haskell then. Different dev cultures I guess. – Tavian Barnes Jun 19 '15 at 1:55
  • I am not experienced in Haskell and cannot speak to if or why this is more prevalent in functional programming. – user22815 Jun 19 '15 at 2:37
  • Looking into it a little more, it seems the Haskell community isn't really happy with it either. Also the compiler recently learned to check for "minimal complete definitions," so an empty implementation would at least generate a warning. – Tavian Barnes Jun 19 '15 at 3:17
  • 1
    Most functional programming languages have tail call optimization. With this optimization, tail recursion wouldn't blow the stack, thus such practices make sense. – Jason Yeo Aug 13 '15 at 14:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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