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.

有帮助吗?

解决方案

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.

许可以下: CC-BY-SA归因
scroll top