Lambda初体验

a function (or a subroutine) defined, and possibly called, without being bound to an identifier。

以上是维基百科上对于“Lambda expression”的解释,简单来说就是:一个不用被绑定到一个标识符上,并且可能被调用的函数。

这个解释还不够通俗,lambda表达式可以这样定义(不精确):一段带有输入参数的可执行语句块。

先提供一个没用stream的lambda表达式的例子:

1
2
3
//这里省略list的构造
List<String> names = ...;
Collections.sort(names, (o1, o2) -> o1.compareTo(o2));

-

1
2
3
4
5
6
7
8
//这里省略list的构造
List<String> names = ...;
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});

上面两段代码分别是:使用lambda表达式来排序和使用匿名内部类来排序。

这个例子可以很明显的看出lambda表达式简化代码的效果。接下来展示lambda表达式和其好基友Stream的配合:

1
2
3
4
List<String> names = new ArrayList<>();
names.add("TaoBao");
names.add("ZhiFuBao");
List<String> lowercaseNames = names.stream().map((String name) -> {return name.toLowerCase();}).collect(Collectors.toList());

这段代码就是对一个字符串的列表,把其中包含的每个字符串都转换成全小写的字符串。注意代码第四行的map方法调用,这里map方法就是接受了一个lambda表达式(其实是一个java.util.function.Function的实例)。

看看在Java8之前,如果我们想做上面代码的操作应该怎么办。

  • 普通青年的代码:

    1
    2
    3
    4
    5
    6
    7
    List<String> names = new ArrayList<>();
    names.add("TaoBao");
    names.add("ZhiFuBao");
    List<String> lowercaseNames = new ArrayList<>();
    for (String name : names) {
    lowercaseNames.add(name.toLowerCase());
    }
  • 文艺青年的代码(借助Guava):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    List<String> names = new ArrayList<>();
    names.add("TaoBao");
    names.add("ZhiFuBao");
    List<String> lowercaseNames = FluentIterable.from(names).transform(new Function<String, String>() {
    @Override
    public String apply(String name) {
    return name.toLowerCase();
    }
    }).toList();

在此,不再讨论普通青年和文艺青年的代码风格孰优孰劣(有兴趣的可以去google搜索“命令式编程vs声明式编程”)。

文艺青年代码初看起来看起来干扰信息有点多,Function匿名类的构造语法稍稍有点冗长。所以Java8的lambda表达式给我们提供了创建SAM(Single Abstract Method)接口更加简单的语法糖。

Lambda语法详解

抽象一下lambda表达式的一般语法:

1
2
3
4
5
6
(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
statment1;
statment2;
//.............
return statmentM;
}

从lambda表达式的一般语法可以看出来,还是挺符合上面给出的非精确版本的定义–“一段带有输入参数的可执行语句块”。

上面的lambda表达式语法可以认为是最全的版本,写起来还是稍稍有些繁琐。别着急,下面陆续介绍一下lambda表达式的各种简化版:

  1. 参数类型省略–绝大多数情况,编译器都可以从上下文环境中推断出lambda表达式的参数类型。这样lambda表达式就变成了:

    1
    2
    3
    4
    5
    6
    (param1,param2, ..., paramN) -> {
    statment1;
    statment2;
    //.............
    return statmentM;
    }
  2. 当lambda表达式的参数个数只有一个,可以省略小括号。lambda表达式简写为:

    1
    2
    3
    4
    5
    6
    param1 -> {
    statment1;
    statment2;
    //.............
    return statmentM;
    }

    所以最开始的例子再次简化为:

    1
    List<String> lowercaseNames = names.stream().map(name -> {return name.toLowerCase();}).collect(Collectors.toList());
  3. 当lambda表达式只包含一条语句时,可以省略大括号、return和语句结尾的分号。lambda表达式简化为:

    1
    param1 -> statment

    所以最开始的例子再次简化为:

    1
    List<String> lowercaseNames = names.stream().map(name -> name.toLowerCase()).collect(Collectors.toList());
  4. 使用Method Reference(方法引用):

    1
    List<String> lowercaseNames = names.stream().map(String::toLowerCase).collect(Collectors.toList());

Lambda表达式眼中的外部世界

lambda表达式其实是快速创建SAM接口的语法糖,原先的SAM接口都可以访问接口外部变量,lambda表达式肯定也是可以(不但可以,在java8中还做了一个小小的升级)。

1
2
3
4
String[] array = {"a", "b", "c"};
for(Integer i : Lists.newArrayList(1,2,3)){
Stream.of(array).map(item -> Strings.padEnd(item, i, '@')).forEach(System.out::println);
}

上面的这个例子中,map中的lambda表达式访问外部变量Integer i。并且可以访问外部变量是lambda表达式的一个重要特性,这样我们可以看出来lambda表达式的三个重要组成部分:

  • 输入参数
  • 可执行语句
  • 存放外部变量的空间

不过lambda表达式访问外部变量有一个非常重要的限制:变量不可变(只是引用不可变,而不是真正的不可变)。

1
2
3
4
String[] array = {"a", "b", "c"};
for(int i = 1; i<4; i++){
Stream.of(array).map(item -> Strings.padEnd(item, i, '@')).forEach(System.out::println);
}

上面的代码,会报编译错误。因为变量i被lambda表达式引用,所以编译器会隐式的把其当成final来处理。

以前java的匿名内部类在访问外部变量的时候,外部变量必须用final修饰。在java8对这个限制做了优化(前面说的小小优化),可以不用显示使用final修饰,但是编译器隐式当成final来处理。

Lambda眼中的this

在lambda中,this不是指向lambda表达式产生的那个SAM对象,而是声明它的外部对象。

方法引用(Method reference)和构造器引用(construct reference)

方法引用

方法引用可以在某些条件成立的情况下,更加简化lambda表达式的声明。

方法引用语法格式有以下三种:

  • objectName::instanceMethod
  • ClassName::staticMethod
  • ClassName::instanceMethod

前两种方式类似,等同于把lambda表达式的参数直接当成instanceMethod|staticMethod的参数来调用。比如System.out::println等同于x->System.out.println(x)Math::max等同于(x, y)->Math.max(x,y)

最后一种方式,等同于把lambda表达式的第一个参数当成instanceMethod的目标对象,其他剩余参数当成该方法的参数。比如String::toLowerCase等同于x->x.toLowerCase()

构造器引用

构造器引用语法如下:ClassName::new,把lambda表达式的参数当成ClassName构造器的参数 。例如BigDecimal::new等同于x->new BigDecimal(x)

参考

  • ifeve.com
  • 《Java SE 8 for the Really Impatient》
  • Java 8 Tutorial
  • Java 8 API doc