Question

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?

Était-ce utile?

La solution

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.

Autres conseils

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.

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.

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.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top