[Java 8] (11) 使用Lambda的原则

要想更好的使用函数式编程,仅仅熟悉其语法结构是远远不够的。必须从思想和设计层面,去考虑它,去接纳它。这种编程范式和大多数开发人员所熟知的面向对象编程范式是不同的。

下面我们从以下几个方面来回顾一下使用函数式编程的要点:

多用声明式,少用命令式

要想更好的使用函数式编程,首先必须要提升代码的抽象程度。之所以使用函数式编程在完成同样任务时需要的代码量比命令式要少,很大程度上就是源于函数式风格的代码的抽象程度更高,能够表达的语义也就更丰富。

当我们使用命令式风格的代码时,我们习惯很快地就进入到细节上的考量,比如解决这个问题需要使用多少次循环,每次循环需要进行的操作有哪些等。这是命令式风格的必然,正是由于较低的抽象程度而导致我们的考虑方式也陷入了琐碎的细节中。此情此景,应了那句话“只见树木不见森林”。所以稍有经验的开发人员,都会特别看重代码封装,目的也就是为了将抽象层次低的琐碎细节给隐藏起来,减少代码的噪声。

而使用函数式风格进行编码时,首先我们应该明确这段代码的目标。比如,对集合根据某种条件进行过滤,对集合中的每个元素进行某种操作。这才是代码的目标,不要陷入到代码的底层细节中。比如,遍历集合并逐一判断,将符合要求的元素放入一个新的集合中,最后返回这个新的集合。一个用来简单判断你是否陷入到对细节的思考的方法是:仔细审视你的描述中有没有出现“遍历”或者“循环”这样的字眼。如果出现了这些字眼,多半表示你的思考方式过于底层了。在进行函数式编程时,这是一种需要避免的思维习惯。所以,这也许就是之前提到过的内部遍历器(Internal
Iterator)的意义,它将遍历的底层逻辑给封装起来,给你的思维腾出空间,让你有机会去从更宏观,更抽象的角度来审视需要解决的问题。

比如,当我们需要拿到一组价格中的最高价格时,99.9%的Java开发人员都会这样实现:

int max = 0;
for(int price : prices) {
    if(max < price) max = price;
}

以上就是最最典型的命令式思考方式,严重依赖于循环与遍历这类语法结构。可是仔细想想,循环和遍历只不过是一种语法现象,它和我们要解决的实际问题并没有直接的关系。我们为何不从更高的角度来审视这个问题呢?输入的参数是一个集合,而输出的参数是一个元素。这就是典型的规约操作(Reduction Operation),它已经被JDK提供了,我们只需要使用它即可:

final int max = prices.stream().reduce(0, Math::max);

代码变的简洁了很多,没有额外创造出任何“轮子”,使用的都是已经存在的基础方法。比如reduce方法和Math.max的方法引用。这也是函数式风格的另一个吸引人的地方:可重用性。

青睐不变性(Immutability)

对于并发程序设计而言,变量的可变性可以说是万恶之源。当多个线程同时对某个变量进行修改的时候,由于可能发生的竞态条件(Race Condition),在很多情况下会得到错误的结果。这也从另一个角度解释了为何在命令式的代码中,实现高效而正确的并发代码是非常困难的。要处理的细节太多,各种小陷阱和过于底层的实现方式,能不困难吗?

而使用函数式编程时,就从本质上避免了创建过多的可变变量。程序的执行并不会改变对象的状态,而是通过输入的对象根据逻辑直接创建出了全新的对象。这也是为什么函数式代码更容易被并行化的原因,从根源上杜绝了破坏并行化的罪魁祸首 - 可变变量(Mutable Variable)。

因此,当你发现代码中定义了可变变量时,考虑是否有办法对它们进行重构来避免之。

减少副作用(Side
Effects)

所谓副作用,就是这段代码对其外部的程序状态有影响。比如修改了某个实例的状态,修改了全局变量等等。

所谓的副作用,最常见的就是返回值为void的方法,这种方法一般都是通过修改对象的状态来完成计算逻辑。这也意味着程序中存在着可变量。因此,方法的副作用往往和可变量有着千丝万缕的联系。当消除了方法的副作用时,多半也意味着可变量的数量也减小了。如果一个方法没有副作用,意味着只要输入的参数不变,输出的结果就永远是一样的。

当使用Lambda表达式时,需要确保代码没有副作用。如果代码存在副作用,也就违背了Lambda表达式的初衷,毕竟Lambda就是为了函数式编程而生的,而函数式编程的特点之一就是函数的实现应该无副作用。

使用表达式(Expression),而不是语句(Statement)

表达式和语句都是程序中用来执行某些操作的指令。只不过它们之间存在一些区别:

  • 语句:执行了操作,但是不会返回任何值
  • 表达式:执行了操作,并且有返回值

因此,优先使用表达式也就是希望减少副作用和可变量。因为语句虽然没有返回值,但是它仍然会执行某些操作,而这些操作通常就是对程序状态进行修改,否则语句的意义何在?

同时,表达式和语句不一样也在于表达式是可以组合起来的,也就是前面提到过的函数链(Function Chaining)。在函数式编程中,函数链十分强大,并且拥有很好的可重用性,每个函数就像一块乐高积木,而这些积木则可以拼装出无穷无尽的组合。

设计高阶函数

高阶函数是Java 8中引进的一个重大特性,过去我们只能向方法传入对象或者值作为参数。而高阶函数的引入,让我们也可以将函数作为参数传入。这无疑能够大大地提高代码的抽象程度,同时减少代码的噪声,比如过去频繁使用的匿名内部类,当它的定义符合函数接口的规范时,就可以直接使用Lambda表达式或者更为简练的方法引用。

比如,这样的代码在Java 8之前的GUI程序中层出不穷:

button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        JOptionPane.showMessageDialog(frame, "you clicked!");
    }
});

而现在我们可以让它插上Lambda表达式的翅膀:

button.addActionListener(event ->
    JOptionPane.showMessageDialog(frame, "you clicked!"));

这不仅让这部分代码变的简练,还减少了源代码中需要的import语句。 因为此时需要实现的接口ActionListener是不需要引入的,同时ActionEvent也可以通过借助类型推导来自动引入。

合理地设计和应用高阶函数,可以让我们非常巧妙和简洁地实现一些常用的设计模式,而不需要像面向对象设计那样创建一大批的类型和接口。

关于性能

有些有些开发人员会担心,大规模的将命令式代码替换成以Lambda表达式和方法引用为基础的声明式代码会对程序性能造成一些影响。但是实际上,这个担心可以说是多余的:在大多数情况下,性能只会变的更好。

Java 8规范提供了一些方法用来帮助编译器进行优化。其中比较和Lambda表达式关系比较密切的是一个叫做”调用动态优化的字节码“(Invoke Dynamic Optimized Bytecode)的指令。结合这个指令可以让Lambda的表达式的执行更快。

比如,下面是一段用来统计集合中质数个数的命令式程序:

long primesCount = 0;
for(long number : numbers) {
    if(isPrime(number)) primesCount += 1;
}

将命令式重构成下面的声明式:

final long primesCount = numbers
    .stream()
    .filter(number -> isPrime(number))
    .count();

当这个集合是1-100000的整型数时,无论是命令式还是声明式的运行时间都是0.025秒左右。但是即便耗时相同,由于使用声明式的种种优势,我们还是可以认为声明式的风格更好:更简练,没有副作用,易于并行化。

而且,由于对每个整型数值判断其是否是质数都是独立的任务。因此,上述程序也非常容易被并行化,仅仅是将stream方法替换成parallelStream就行:

final long primesCount = numbers
    .parallelStream()
    .filter(number -> isPrime(number))
    .count();

此时,执行时间缩短到了0.006秒!也就是说,性能上升了大约400%。

采用函数式编码风格

从Java 8开始,Java也是一门像Scala等语言那样的混合范式编程语言了。了解在Java 8中如何通过Lambda表达式,方法引用和高阶函数等语法来实现函数式编码本身并不困难。

困难的地方在于,如何灵活地将这种新型编程范式和现有的命令式面向对象范式有机地融合在一起。这需要你对手头的问题进行仔细的思考,甚至是颠覆性的思考。当然,通过寻找一些典型的用例也是一个非常好的学习方式。

我们可以采取一种循序渐进的方式来对编写代码。首先让它能够工作,再让它变的更好,变的更优美。而毫无疑问,函数式编码会让代码更加优美,更加充满诗意。

时间: 2024-10-11 06:24:22

[Java 8] (11) 使用Lambda的原则的相关文章

Java Web 设计模式之开闭原则

1.开闭原则(OCP) 遵循开闭原则设计出的模块具有两个主要特征: (1)对于扩展是开放的(Open for extension).这意味着模块的行为是可以扩展的.当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为.也就是说,我们可以改变模块的功能. (2)对于修改是关闭的(Closed for modification).对模块行为进行扩展时,不必改动模块的源代码或者二进制代码.模块的二进制可执行版本,无论是可链接的库.DLL或者.EXE文件,都无需改动. 2.通过UML

Java Web设计模式之依赖倒换原则

1.依赖倒置原则 A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象. B.抽象不应该依赖于具体,具体应该依赖于抽象. 2.用UML图来说明一下: 代码说明: (1)管理员接口 1 package com.alibaba.com.miao; 2 3 public interface IEmployee { 4 5 public String code(ICode code); 6 } (2)编码接口 1 package com.alibaba.com.miao; 2 3 public

Java函数式编程和lambda表达式

为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于某种语法或调用API去进行编程.例如,我们现在需要从一组数字中,找出最小的那个数字,若使用用命令式编程实现这个需求的话,那么所编写的代码如下: public static void main(String[] args) { int[] nums = new int[]{1, 2, 3, 4, 5,

Java基础11:Java泛型详解

Java基础11:Java泛型详解 泛型概述 泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用. 什么是泛型?为什么要使用泛型? 泛型,即"参数化类型".一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参.那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参). 泛型的本质是为了参数化类型(在不创建新的类型的

适用于Java开发人员的SOLID设计原则简介

看看这篇针对Java开发人员的SOLID设计原则简介.抽丝剥茧,细说架构那些事——[优锐课] 当你刚接触软件工程时,这些原理和设计模式不容易理解或习惯.我们都遇到了问题,很难理解SOLID + DP的思想,甚至很难正确实施它们.确实,“为什么要SOLID?”的整个概念,以及如何实施设计模式,这需要时间和大量实践. 我可以说实话,关于SOLID设计模式以及TDD等其他领域,从本质上讲,它们很难教.很难以正确的方式将所有这些知识和信息传授给年轻人. 让SOLID 变得容易 在本文中,我将以尽可能简单

C++11 里lambda表达式的学习

最近看到很多关于C++11的文档,有些是我不怎么用到,所以就略过去了,但是lambda表达式还是比较常用的,其实最开始学习python的时候就觉得lambda这个比较高级,为什么C++这么弱.果然C++增加这个东西. 语法 [ capture ] ( params ) mutable exception attribute -> ret { body }      (1) [ capture ] ( params ) -> ret { body }                       

初窥c++11:lambda函数

为什么需要lambda函数 匿名函数是许多编程语言都支持的概念,有函数体,没有函数名.1958年,lisp首先采用匿名函数,匿名函数最常用的是作为回调函数的值.正因为有这样的需求,c++引入了lambda 函数,你可以在你的源码中内联一个lambda函数,这就使得创建快速的,一次性的函数变得简单了.例如,你可以把lambda函数可在参数中传递给std::sort函数 #include <algorithm> #include <cmath> void abssort(float*

java进阶11 static final关键字

static表示"全局"或者"静态"的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块. 被static修饰的成员变量和成员方法独立于该类的任何对象,也就是说. 它不依赖类的特定的实例,被类的所有实例共享. package Static; public class Static { /* * 使用的情况 * 1:在对象之间共享值时 * 2:方便访问变量 * */ public static void main(String[] args){ Sta

Java基础11 对象引用(转载)

对象引用 我们沿用之前定义的Human类,并有一个Test类: public class Test{    public static void main(String[] args){        Human aPerson = new Human(160);    }  class Human{    public Human(int h){        this.height = h;    }    public int getHeight(){        return this