经典的Operation枚举示例中的Lambda表达式

43

正如你们中许多人所知道的那样,有一个经典的Operation枚举的例子(现在使用Java 8标准接口),如下所示:

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;
    }
}

测试环境:

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

它提供以下功能:

对2和4执行加法操作:6.0
对2和4进行减法操作:-2.0
对2和4执行乘法操作:8.0
对2和4执行除法操作:0.5

但我认为我们可以通过Java 8做得更好,因此我实现了以下内容:

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);
    }
}

在功能上它们是等价的,但是这两个实现是否仍然相似,或者是否存在一些隐藏的细节使新版本比旧版本更糟糕?

最后,作为Java 8的首选方式,Lambda方式是否是首选方式?


5
《Effective Java》第三版现在推荐使用Lambda表达式的方式来解决这个例子。 - shmosel
问题:在这里使用枚举是否比使用简单的公共类常量(或内部公共类)有任何好处?例如:public static final DoubleBinaryOperator PLUS = (l, r) -> l + r; - Andrejs
2
@Andrejs 它们有名称并且可以具有其他属性,例如 symbol,它允许创建映射。这很顺畅,因为您可以免费获得“迭代所有常量”的逻辑。 - Holger
基本上这是策略模式的恰当应用。 - Philzen
4个回答

29
显然,lambda版本更易读。它不仅更短,而且允许读者在构造函数中一眼看到实现运算符。想象一下,你想扩展 enum 以支持 int 计算……
从性能角度来看,您将匿名的enum内部类替换为生成的lambda类。lambda版本添加了另一层委托,但这对于HotSpot优化器来说并不是一个挑战。在执行性能方面不太可能看到任何差异。
然而,如果始终应用lambda模式,则可以加快使用该类的应用程序的启动速度。原因是,对于传统的专门的enum方法,Java编译器必须为每个case生成一个内部类,该内部类位于文件系统中或(可能是压缩的)Jar文件中。即时生成具有非常简单结构的lambda类的字节码通常比加载类要快。此外,对于生成的 lambda 类没有访问检查可能也有所帮助。
总结一下:
  • lambda方法更易于阅读,其代码更易于维护(重点)
  • 执行性能大致相同
  • lambda方法的启动时间可能更短
因此,lambda是一个巨大的胜利。是的,我认为获取Java 8以来使用lambda方式是首选方法。

21

这取决于您如何定义“更好”。

在您的情况和我的观点中,Lambda表达式是纯粹的胜利。您甚至可以重复使用一些现有的JDK函数,例如:

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

这段话简短易懂。如果没有对你的代码进行基准测试,就不可能做出任何合理的性能评估。

使用 invokeDynamic 实现了 Lambda 表达式,以动态链接调用点和要执行的实际代码,而没有使用匿名内部类。


6

我很惊讶没有人提到这一点,但是使用lambda方法在实现多个方法时可能会变得复杂。将许多无名lambda传递给构造函数可能更加简洁,但不一定更易读。

此外,使用lambda的好处随着函数规模的增长而减少。如果您的lambda超过几行,重写可能会更容易阅读,甚至更容易。


2
同意,Effective Java第三版,第43项:“Lambda表达式缺少名称和文档;如果计算不是自解释的或超过几行,请勿将其放入Lambda表达式[...]语句长度超过三行时,应该遵循合理的最大限制。” - Andrejs

5

“Define worse”的意思是更糟糕,很可能会使用更多的字节码,速度稍微慢一些。

除非这些对您很重要,否则我建议您使用您认为更清晰简单的方法。


它真的会变慢吗? 我之前一直在质疑自己,但如果性能很重要,那么它应该很快就被内联了,在这一点上它的行为与传统解决方案完全相同,除非我弄错了,否则只有内存占用量会保持不变。 - skiwi
1
@skiwi 理论上它可能是相同的,但是优化器需要完成更多的工作,通常意味着在实际情况下性能较慢。尤其是因为单个方法调用比执行的操作要昂贵得多。 - Peter Lawrey
1
@bigGuy,字节码可以告诉你.class文件的大小,如果字节码相同,你会得到相同的结果。如果字节码不同(我会感到惊讶如果它不是这样),你需要运行它来查看有多少差异,如果有的话。 - Peter Lawrey
理论上来说,它会更慢,但在实践中这可能是一种无用的优化。 - Anubian Noob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接