函数式编程

Closure闭包 Lambda和Monad

  闭包(也称为词法闭包或函数闭包)是一个函数代表一段代码块,这个函数将其上下文环境(如某个局部变量A)一同劫持,这样,当这个函数在其它上下文环境被调用时,调用者还能够访问原来环境(那个局部变量A)。具体可见Javascript的闭包

  在本质上,闭包一段封闭的可以在以后的时间被执行的代码块,能够一直保持其第一次被创建时候的环境(注:每个人都记得自己的第一次) ,也就是说,无论以后它在什么环境中被调用,还是可以使用当初创建它时的那些环境比如局部变量等,这种局部变量称为非局部( non-local )变量或 自由free 变量。

  看一段c#代码:

using System;

class Test
{  

    static Action CreateAction()
    {
        int counter = 0;
        return delegate
        {
            // Yes, it could be done in one statement;
            // but it is clearer like this.
            counter++;
            Console.WriteLine("counter={0}", counter);
        };
    }

  static void Main()
    {
        Action action = CreateAction();
        action();
        action();
    }
}

  我们调用了两次CreateAction,其结果是不断增加的(counter++)。

  闭包是一种聚合,包含了函数和环境(free变量),当编译器创建一个新的闭包时,动态分配一个指向相同代码的新指针,自由free变量有了不同的值。

  所以,谈到闭包,自由变量和代码段是两个关键词。Java中有通过匿名函数代码段方式实现闭包,匿名函数会绑定当时的环境(自由变量)。

Lambda表达式

  Lambda表达式是对闭包源码阶段的一种称呼,表达式只存在源码这个阶段,Lambda表达式在运行时效果是对象的生成。这样的对象被称为闭包。  

  在Java中,早就使用匿名函数来表达闭包,比如在常见的并发异步编程中,随着摩尔定律继续提供给我们更多的核数,而不是更快的内核,串行的API将限制多核的处理能力。因此,并行处理越来越热门,包括事件驱动和异步编程等,下面是一个观察者模式的reactive模式:

public interface ActionListener {
  void actionPerformed(ActionEvent e);
}

  这是一个观察者监听者,为了监听被监听者,我们加入监听者如下:

button.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent e) {
   ui.dazzle(e.getModifiers());
  }
});

  许多有用的并行API库都依赖于这种模式。其中监听者响应执行的代码必须在独立的线程中运行。

  但是使用这种匿名函数存在笨重等各种问题,下面一段代码演示这个问题:

public class FirstLambdaExpression {
    public String classvariable = "类级别变量";

    public static void main(String[] arg) {
        new FirstLambdaExpression().lambdaExpression();
    }

    public void lambdaExpression() {
        final String finalVariable = "方法外部final变量";
        new Thread(new Runnable() {
            String variable = "Runnable类的成员变量";

            public void run() {
                String variable = "Run方法的变量";
                System.out.println("->" + variable);
                System.out.println("->" + this.variable);
                System.out.println("->" + finalVariable);
                System.out.println("->" + classvariable);

            }
        }).start();
    }
}

输出结果是:

->Run方法的变量
->Runnable类的成员变量
->方法外部final变量
->类级别变量

  匿名方法可以方法方法以外的局部变量(自由变量),但是这个局部变量必须标识为final。匿名方法的问题是会引起大量代码嵌套后的“大楼风格”。而lambda应该是一种简约的单一的函数,类似平房。这里引入函数式接口。

函数式接口FUNCTIONAL INTERFACES

  函数式接口是只有一个方法的接口,这种方法只代表一个单一的功能合约。

interface Runnable { void run(); }
// 这是函数式的


interface Foo { boolean equals(Object obj); }
//这不是函数式的, equals代表存在一个隐式的成员,因为相等是两个比较是否相等,这里只列出参与比较的一个成员,另外一个成员隐藏了,估计是实现类的字段属性。


interface Bar extends Foo {int compare(String o1, String o2); }
// 这是函数式的,两个比较的成员都出现在方法参数中。


interface Comparator {
  boolean equals(Object obj);
  int compare(T o1, T o2);//无对象Non-Object的方法
}
//函数式的; Comparator至少有一个抽象的非对象的方法


interface Foo {int m(); Object clone(); }
//不是函数式的; 方法Object.clone 非公开public

  一般的回调功能都是函数式的,如 RunnableCallable, 或 Comparator等,这样的函数式接口也称为SAM (Single Abstract Method)类型。Java8中加入了java.util.function

  下面是Java8的一段Lambda代码:

public class FirstLambdaExpression {
    public String variable = "Class Level Variable";
    public static void main(String[] arg) {
        new FirstLambdaExpression().lambdaExpression();
    }
    public void lambdaExpression(){
        String variable = "Method Local Variable";
        String nonFinalVariable = "This is non final variable";
        new Thread (() -> {
            //Below line gives compilation error
            //String variable = "Run Method Variable"
            System.out.println("->" + variable);
            System.out.println("->" + this.variable);
       }).start();
    }
}

这里输出:

  1. ->Method Local Variable
  2. ->Class Level Variable

  注意的是这里的this.variable变成了类成员变量值,而之前我们使用匿名函数是this.variable只能是当前匿名函数中的局部变量。lambda是不允许创造阴影shadowing变量的。

  Lambda表达式的一般语法包含一个参数列表,加上箭头标记的' - >',最后是内容Body三个部分组成。该内容部分可以简单地是一个表达式或多个语句块。如果是一个表达式,那么这个表达式将被执行,还必须“return”返回一个结果;如果它是一个代码语句块,那么它也将被执行,也需要返回一个结果。 break和continue是在顶层非法的,但在循环中允许的。

  lambda表达式其实也就是一段匿名函数,只是结构更轻量短小,有方法参数和一段方法函数体(一段代码块),如下:

(String s)-> s.lengh;

() -> 43;

(int x, int y) -> x + y;

  第一个表达式需要字符串变量作为参数,然后返回字符串的长度。第二个不带任何参数,并返回43。最后第三个是接受两个整数x和y,并返回其相加结果。这三者都符合输入 ->输出模型。

  下面改写一个匿名函数为Lambda,我们有一个 writeToFile函数,是用来打开一个FileWriter对象,我们希望打开FileWriter能够将数据写入FileWriter,为了实现这个功能,我们将这个功能作为一个匿名函数传入:

void save(final String data) {
 writeToFile("file.db", new FileWriteFunction() {
  public void apply(Writer file) throws IOException {
   file.write(data) ;
  }
 }) 
}

  我们传入writeToFile的是一个FileWriteFunction匿名函数,这个匿名函数实际是一个回调函数,用于届时实现写入文件的功能。用Lambda替代这个匿名函数:

  writeToFile("file.db", file -> file.write(data) );

  这是一个lambda表达式,我们前面说过,当lambda运行被使用时,它是作为一个闭包使用,比如我们在别处调用代码如下:

void save(String data) {
  writeToFile("file.db", file -> file.write(data) );
}

  匿名函数只能访问环境的final变量,而Lambda能访问任何变量,这里闭包可以访问环境自由变量data,但是它一旦被闭包捕获,data也是最终闭合的(到达终点站,掉入黑洞),不能再被重新分配。

  下面着重谈谈lambda表达式作为闭包使用时,与被使用时的上下文环境的配合问题,实际上,闭包看成一个集成模块,黑盒子,它有输入输出,输入的是环境自由变量,输出要解决的是是Lambdad目标类型推断问题。

 

Lambda目标类型

  lambda中返回的目标类型并不在表达式中声明,编译器会根据lambda被使用时的上下文推断返回目标的类型,Lambda是不能没有目标类型的,其实,倒过来看,如果把目标接口看成一个接口,而Lambda表达式只不过是实现这个接口的一个方法而已。如下:

//Lambda表达式被嵌套在方法参数中,如下黑体两个圆括号
//目标类型将是方法参数类型
String user = doSomething(() -> list.getProperty(“propName”));


//Lambda表达式被一个线程的构造函数封闭new Thread(Lambda封闭在这里)
//目标类型就是构造参数类型,这里是Runnable
new Thread (() -> {
System.out.println("Running in different thread");
}).start();

  目标接口有下面约束:

  • 目标接口应该是一个函数接口。
  • 参数的数量和类型应该等同于目标接口的的方法的。
  • 该表达式的返回类型应与该接口的函数式方法兼容。
  • 表达式抛出的异常类型应与接口的异常类型兼容。

  编译器可以推断接口类型,就不需要在lambda表达式显式地定义它们。或者说,编译器同时关注参数类型(输入)和目标类型(输出),如果知道了目标类型,可以忽略参数类型,如下:

Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);

s1和s2的参数类型没有声明,因为目标类型是Comparator<String>,lambda表达式 显然是一个实现两个String比较的Comparetor,也就是实现compare(String, String)的一个变幻手段(语法糖),因此编译器推断s1和s2也是String。

如果目标类型只能有一个,那么参数类型的圆括号()就是可选的,可以写也可以不写:

ActionListenr listenr = event -> event.getWhen();

这里event()的圆括号没有写,因为返回的目标类型只是一个ActionListenr类型。原来的写法是:

ActionListener listenr = (ActionEvent event) ->event.getWhen();

 

Lambda为什么不要方法名?

  lambda表达式只有工作在函数式接口下,而函数式接口只有一个方法,当我们用一个函数式接口作为lambda的目标类型时,编译器会检查到函数式方法的签名, 那么参数类型也将被忽略,本来参数类型规定已经是多余,那么方法名称再指定更是多余。

 

Monad

  Monad单子可以认为是一系列lambda的串联,如同职责链模式,在某一个上下文中,函数可以按顺序链接在一起,并控制数据是如何从一个函数传递到下一函数。Monad经常使用闭包实现逻辑。

 

下页

Java8教程

用Java 8 lambda优化JDBC

Java8的Lambda和排序

使用Java8的Lambda实现模板模式

猜你喜欢