42

As many of you may know, there is a classical example of the Operation enum (using Java 8 standard interface now though), that is the following:

enum Operation implements DoubleBinaryOperator {
    PLUS("+") {
        @Override
        public double applyAsDouble(final double left, final double right) {
            return left + right;
        }
    },
    MINUS("-") {
        @Override
        public double applyAsDouble(final double left, final double right) {
            return left - right;
        }
    },
    MULTIPLY("*") {
        @Override
        public double applyAsDouble(final double left, final double right) {
            return left * right;
        }
    },
    DIVIDE("/") {
        @Override
        public double applyAsDouble(final double left, final double right) {
            return left / right;
        }
    };

    private final String symbol;

    private Operation(final String symbol) {
        this.symbol = symbol;
    }

    public String getSymbol() {
        return symbol;
    }
}

Tested with:

Arrays.stream(Operation.values())
        .forEach(op -> System.out.println("Performing operation " + op.getSymbol() + " on 2 and 4: " + op.applyAsDouble(2, 4)));

It provides:

Performing operation + on 2 and 4: 6.0
Performing operation - on 2 and 4: -2.0
Performing operation * on 2 and 4: 8.0
Performing operation / on 2 and 4: 0.5

But I feel like we can do better with Java 8, hence I implemented the following:

enum Operation implements DoubleBinaryOperator {
    PLUS    ("+", (l, r) -> l + r),
    MINUS   ("-", (l, r) -> l - r),
    MULTIPLY("*", (l, r) -> l * r),
    DIVIDE  ("/", (l, r) -> l / r);

    private final String symbol;
    private final DoubleBinaryOperator binaryOperator;

    private Operation(final String symbol, final DoubleBinaryOperator binaryOperator) {
        this.symbol = symbol;
        this.binaryOperator = binaryOperator;
    }

    public String getSymbol() {
        return symbol;
    }

    @Override
    public double applyAsDouble(final double left, final double right) {
        return binaryOperator.applyAsDouble(left, right);
    }
}

Functionally it is equivalent, however are both implementations still similar, or are there some hidden details that make the new version worse as the old version?

And lastly, is the lambda way the preferred way to do it as of Java 8?

skiwi
  • 66,971
  • 31
  • 131
  • 216
  • 5
    Effective Java, 3rd edition now recommends the lambda approach using this very example. – shmosel Apr 24 '18 at 16:36
  • Question: does usage of an enum here give any benefit over a simple public class with constants (or an inner public class)? i.e. `public static final DoubleBinaryOperator PLUS = (l, r) -> l + r;` ? – Andrejs Nov 05 '19 at 12:04
  • 2
    @Andrejs they have names and can have additional properties, like the `symbol`, which allows creating maps. This works smoothly as you get the “iterate over all constants” logic for free. – Holger Apr 16 '20 at 06:43
  • Basically this is a proper [application of the strategy pattern](https://dzone.com/articles/strategy-pattern-implemented-as-an-enum-using-lamb). – Philzen Jan 06 '22 at 00:48

4 Answers4

28

Obviously, the lambda version is far more readable. Not only is it shorter, it allows a reader to see the the implementation operator at the first glance in the constructor. Imagine you want to extend the enum to support int calculation as well…

From a performance point of view you are exchanging the anonymous enum inner classes by generated lambda classes. The lambda version adds another level of delegation but that’s no challenge to the HotSpot optimizer. It’s unlikely to see any difference regarding the execution performance.

However, when applying the lambda pattern consequently you might get a speedup of the startup of applications using the class. The reason is that for the traditional specialized enum approach the Java compiler has to generate an inner class for each case which resides either in the file system or (possibly zip-compressed) in a Jar file. Generating the byte code for a lambda class (which has a very simple structure) on the fly is usually faster than loading a class. It might help as well that there is no access checking for the generated lambda classes.

To summarize it:

  • The lambda approach is easier to read and its code is more maintainable (the big point)
  • The execution performance is roughly the same
  • The startup time might be shorter for the lambda approach

So it’s a big win for the lambda. Yes, I think the lambda way the preferred way to do it as of Java 8.

Holger
  • 285,553
  • 42
  • 434
  • 765
20

It depends how you define better.

In your case and in my opinion, the lambdas are a pure win. You can even reuse some existing JDK functions, e.g.:

enum Operation implements DoubleBinaryOperator {
    PLUS    ("+", Double::sum),
    ...
}

This is short and readable. I don't think anything reasonable can be said about performance w/o benchmarking your code.

Lambdas are implemented with invokeDynamic to dynamically link call site to actual code to execute; no anonymous, inner classes.

emesx
  • 12,555
  • 10
  • 58
  • 91
6

I'm surprised no one mentioned this, but the lambda approach can get hairy when implementing multiple methods. Passing a bunch of nameless lambdas into a constructor will be more concise, but not necessarily more readable.

Also, the benefit of using a lambda decreases as the function grows in size. If your lambda is more than several lines long, an override might be just as easy to read, if not easier.

shmosel
  • 49,289
  • 6
  • 73
  • 138
  • 2
    Agree, Effective Java 3rd Edition, Item 43: `lambdas lack names and documentation; if a computation isn't self-explanatory or exceeds a few lines, don't put it in a lambda [...] lambdas of three lines is a reasonable maximum`. – Andrejs Nov 05 '19 at 11:46
5

Define worse, most likely it uses a little more byte code and is slightly slower.

Unless these are important to you, I would use the approach you find clearer and simpler.

Peter Lawrey
  • 525,659
  • 79
  • 751
  • 1,130
  • Would it really be slower? I was questioning myself earlier, and *if* performance matters, then it should get inlined pretty quickly, at which point it behaves exactly the same as the traditional solution? Only the memory footprint would remain unless I'm mistaken. – skiwi Apr 29 '14 at 10:23
  • 1
    @skiwi in theory it could be the same, but there is more work for the optimiser to do which usually means slower performance in reality. esp as a single method call is far more expensive than the operation performed. – Peter Lawrey Apr 29 '14 at 10:25
  • 1
    @bigGuy The byte code will tell you how large the `.class` file will be and if the byte code is the same you get the same result. If the byte code is different (and I would be amazed if it wasn't) you need to run it to see how much difference, if any, it makes. – Peter Lawrey Apr 29 '14 at 10:27
  • Theoretically it would be slower, in practice it's probably a useless optimization. – Anubian Noob Apr 29 '14 at 15:10