java8新特性之lambda表达式

“lambda 表达式”是一块代码,你可以将它传递出去,这样后面就可以执行一次或多次。很多情况下指定这样一个代码块是有用的:

  • 传递给 Arrays.sort 一个比较方法
  • 在单独的线程中运行任务
  • 指定一个当按钮被点击时应该发生的行为

然而,java 是面向对象语言,其中的(几乎)所有一切都是对象。java 中没有函数类型。作为一种替代,函数被表达为对象,也就是实现了特定接口的类的实例。lambda 表达式给你一种便捷的语法来创建这样的实例。

android studio 中如何使用 lambda 参考: gradle-retrolambda

lambda 表达式语法

  • 一个简单的表达式
String[] friends = {"peter", "paul", "mary"};
Arrays.sort(friends, (x, y) -> x.compareTo(y));
- 如果 lambda 表达式的表达体执行一个无法用一个表达式表示的计算,那么用编写方法的方式来写:即用 {} 包裹代码并明确写上 return 语句:
Arrays.sort(friends, (String x, String y) -> {
    int d = x.length() - y.length();
    if (d < 0) return -1;
    else if (d > 0) return 1;
    else return 0;
});
  • 如果 lambda 表达式没有参数,提供空的小括号,就像没有参数的方法:
Runnable task = () -> { for(int i = 0; i < 10; i++) Log.d("MainActivity", i + "");};
  • 如果 lambda 表达式的参数类型可以被推倒出来,则可以省略类型,如:
Comparator comparator = (x, y) -> x.compareTo(y);
  • 如果某个方法只有一个参数,并且该参数类型是可以推导出来的,你甚至可以省略小括号。
  • 永远不需要为 lambda 表达式指定返回类型。编译器会从表达式推断出类型,并且会检查返回类型是否与期望的类型先匹配。

函数式接口

java 有许多表达行为的接口,例如 Runnable 或 Comparetor 。 lambda 表达式兼容这些接口。无论何时,当你期望只有一个抽象方法的接口对象时,就可以提供一个 lambda 表达式。 这样的接口称为 函数式接口。如:

Arrays.sort(friends, (x, y) -> x.compareTo(y));

第二个参数接受一个实现了 Comparetor 接口的类的实例。调用该对象的 compare 方法会执行 lambda 表达式中的代码。这些对象和类的管理完全依赖于实现,并且是高度优化的。

在 java 中,对 lambda 表达式你只能做一件事:将其放入类型为函数式接口的变量中,这样它就被转换为该接口的实例

注意:不能将一个 lambda 表达式赋值给一个 Object 类型的变量,因为 Object 是类,不是函数式接口。

方法引用和构造函数引用

有时,你想要传递给其他代码块的操作已经有实现的方法了。有一种特殊的语法方法引用,它甚至比调用方法的 lambda 表达式更短。类似的便捷方法构造函数也有。

方法引用

如果你想不区分大小写地对字符串进行排序,就可以这样调用:

Arrays.sort(friends, (x, y) -> x.compareToIgnoreCase(y));

方法引用:

Arrays.sort(friends, String :: compareToIgnoreCase);

其它例子:

//删除列表所有的 null 值
list.removeIf(Objects :: isNull); 等同于 x -> Objects.isNull(x)
//打印列表
list.forEach(System.out :: println); 等同于 x -> System.out.println(x)

删除

表达式 String :: compareToIgnoreCase 是方法引用,它等同于 lambda 表达式 (x, y) -> x.compareToIgnoreCase(y)。

有3种使用方式,操作符 :: 将方法名称与类或对象名称分隔开:

  1. 类 :: 实例方法 –> 第一个参数变成方法的接受者,并且其它参数也传递给该方法。
  2. 类 :: 静态方法 –> 所有的参数传递给静态方法
  3. 对象 :: 实例方法 –> 在给定的对象上调用方法

构造函数引用

构造函数引用于方法引用类似,不同的是构造函数引用中的方法名都是 new。如:

int[] :: new //是一个含有一个参数的构造函数,该参数为数组的长度。它等同于 lambda 表达式 n -> new int[n]

使用 lambda 表达式

实现延迟执行

使用 lambda 表达式就在于延迟执行。毕竟,如果你想立即执行一段代码,无须将代码封装进 lambda ,你可以直接调用。延迟执行的原因很多,例如:

  1. 在另一个单独的线程运行代码
  2. 多次运行代码
  3. 在算法恰当时刻运行代码(例如排序中的比较操作)
  4. 当某些情况发生时运行代码(点击事件,数据到达等)
  5. 只有在有需要的时候才运行代码

常用函数式接口

  • Runnable
  • Supplier
  • Function
  • Consumer

实现自己的函数式接口

  1. 声明接口
  2. 将接口对象传入函数的参数中
  3. 在函数里调用接口中的方法
  4. 调用函数,传入 lambda 表达式

lambda 表达式和变量作用域

lambda 表达式的作用域

  • lambda 表达式的方法体与嵌套代码块有着相同的作用域。因此,也使用同样的命名冲突和屏蔽规则。

    如不允许局部变量与参数变量同名;方法内部不允许有同名的变量。

  • lambda 表达式中的关键字 this 代表的是创建 lambda 表达式的方法的 this 参数。

访问来自闭合作用域的变量

经常要在 lambda 表达式中访问来自闭合方法或类的变量。如下例子:

public static void repeatMessage(String text, int count){
    Runnable r = () -> {
      for (int i = 0; i < count; i++){
          System.out.println(text);
      }  
    };

    new Thread(r).start();
}

//调用
repeatMessage("Hello", 1000);

lambda 表达式有三个部分:

  1. 代码块
  2. 参数
  3. 自由变量的值 —— 自由变量是指,既不是参数变量。也不是代码内部定义的变量。

上述示例中, lambda 表达式有两个自由变量, text 和 count。代表 lambda 表达式的数据结构必须存储这些变量的值(也就是 “hello” 和 “1000”)。我们说这些变量值以及被 lambda 表达式捕获了(通过具体的实现细节来完成捕获。例如,可以将 lambda 表达式转变为带有一个方法的对象,这样自由变量的值就可以复制到该对象的实例变量中)。

约束:在 lambda 表达式中,你只能引用那些值不会改变的变量。

如下会出错:

for (int i = 0; i < 100; i++){
        new Thread(() -> System.out.println(i)).start();
    }

也就是说捕获的变量应该是个 final 变量。

然而这样是可以的:

for (String s : friends){
        new Thread(() -> System.out.println(s)).start();
    }

每一个迭代器会创建一个新的 s 变量,并且从 数组 friends 中将下一个值赋给 s.

同样, lambda 表达式不能修改任何捕获的变量。

描述带有自由变量值的代码块的技术名词是闭包(closure), 在 java 中, lambda 表达式就是闭包