java中lambda表达式语法说明

 更新时间:2016年09月11日 17:03:20   作者:robin  
“Lambda 表达式”(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。

语法说明

一个lambda表达式由如下几个部分组成:

1. 在圆括号中以逗号分隔的形参列表。在CheckPerson.test方法中包含一个参数p,代表了一个Person类的实例。注意:lambda表达式中的参数的类型是可以省略的;此外,如果只有一个参数的话连括号也是可以省略的。比如上一节曾提到的代码:

  p -> p.getGender() == Person.Sex.MALE
  && p.getAge() >= 18
      && p.getAge() <= 25

2. 箭头符号:->。用来分隔参数和函数体。

3. 函数体。由一个表达式或代码块组成。在上一节的例子中使用了这样的表达式:

p.getGender() == Person.Sex.MALE
      && p.getAge() >= 18
      && p.getAge() <= 25

如果使用的是表达式,java运行时会计算并返回表达式的值。另外,还可以选择在代码块中使用return语句:

  p -> {
    return p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25;
  }

不过return语句并不是表达式。在lambda表达式中需要将语句用花括号括起来,然而却没有必要在只是调用一个返回值为空的方法时也用花括号括起来,所以如下的写法也是正确的:

  email -> System.out.println(email)

lambda表达式和方法的声明看起来有很多类似的地方。所以也可以把lambda表达式视为匿名方法,也就是没有定义名字的方法。

以上提到的lambda表达式都是只使用了一个参数作为形参的表达式。下面的实例类,Caulator,演示了如何使用多个参数作为形参:

package com.zhyea.zytools;

public class Calculator {

  interface IntegerMath {
    int operation(int a, int b);
  }

  public int operateBinary(int a, int b, IntegerMath op) {
    return op.operation(a, b);
  }

  public static void main(String... args) {
    Calculator myApp = new Calculator();
    IntegerMath addition = (a, b) -> a + b;
    IntegerMath subtraction = (a, b) -> a - b;
    System.out.println("40 + 2 = " + myApp.operateBinary(40, 2, addition));
    System.out.println("20 - 10 = " + myApp.operateBinary(20, 10, subtraction));
  }
}

代码中operateBinary方法使用了两个整型参数执行算数操作。这里的算数操作本身就是IntegerMath接口的一个实例。在上面的程序中使用lambda表达式定义了两个算数操作:addition和subtraction。执行程序会打印如下内容:

40 + 2 = 42
20 - 10 = 10

访问外部类的局部变量

类似于局部类或匿名类,lambda表达式也可以访问外部类的局部变量。不同的是,使用lambda表达式时无需考虑覆盖之类的问题。lambda表达式只是一个词法上的概念,这意味着它不需要从超类中继承任何名称,也不会引入新的作用域。也就是说,在lambda表达式中的声明和在它的外部环境中的声明意义是一样的。在下面的例子中对此作了演示:

package com.zhyea.zytools;

import java.util.function.Consumer;

public class LambdaScopeTest {

  public int x = 0;

  class FirstLevel {

    public int x = 1;

    void methodInFirstLevel(int x) {
      //如下的语句会导致编译器在statement A处报错“local variables referenced from a lambda expression must be final or effectively final”
      // x = 99;
      Consumer<integer> myConsumer = (y) ->{
        System.out.println("x = " + x); // Statement A
        System.out.println("y = " + y);
        System.out.println("this.x = " + this.x);
        System.out.println("LambdaScopeTest.this.x = " + LambdaScopeTest.this.x);
      };

      myConsumer.accept(x);
    }
  }

  public static void main(String... args) {
    LambdaScopeTest st = new LambdaScopeTest();
    LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
    fl.methodInFirstLevel(23);
  }
}

这段代码会输出如下内容:

x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0

如果使用示例中lambda表达式myConsumer中的参数y替换为x,编译器就会报错:

 

  Consumer<integer> myConsumer = (x) ->{
        // ....
      };

编译器报错信息是:“variable x is already defined in method methodInFirstLevel(int)”,就是说在方法methodInFirstLevel中已经定义了变量x。报错是因为lambda表达式不会引入新的作用域。也因此呢,可以在lambda表达式中直接访问外部类的域字段、方法以及形参。在这个例子中,lambda表达式myConsumer直接访问了方法methodInFirstLevel的形参x。而访问外部类的成员时也是直接使用this关键字。在这个例子中this.x指的就是FirstLevel.x。

然而,和局部类或匿名类一样,lambda表达式也只能访问局部变量或外部被声明为final(或等同于final)的成员。比如,我们将示例代码methodInFirstLevel方法中“x=99”前面的注释去掉:

      //如下的语句会导致编译器在statement A处报错“local variables referenced from a lambda expression must be final or effectively final”
      x = 99;
      Consumer<integer> myConsumer = (y) ->{
        System.out.println("x = " + x); // Statement A
        System.out.println("y = " + y);
        System.out.println("this.x = " + this.x);
        System.out.println("LambdaScopeTest.this.x = " + LambdaScopeTest.this.x);
      };

因为在这段语句中修改了参数x的值,使得methodInFirstLevel的参数x不可以再被视为final式的。因此java编译器就会在lambda表达式访问局部变量x的地方报出类似“local variables referenced from a lambda expression must be final or effectively final”这样的错误。

目标类型

该如何判断lambda表达式的类型呢。再来看一下筛选适龄服兵役人员的代码:

   p -> p.getGender() == Person.Sex.MALE
          && p.getAge() >= 18
          && p.getAge() <= 25

这段代码在两处用到过:

public static void printPersons(List<Person> roster, CheckPerson tester) —— 方案三
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) —— 方案六

在调用printPersons方法时,这个方法期望一个CheckPerson 类型的参数,此时上面的那个表达式就是一个CheckPerson 类型的表达式。在调用printPersonsWithPredicate方法时,期望一个Predicate<Person>类型的参数,此时同样的表达式就是Predicate<Person>类型的。像这样子的,由方法期望的类型来决定的类型就叫做目标类型(其实我觉得scala中的类型推断用在这里更合适)。java编译器就是通过目标类型的上下文语境或者发现lambda表达式时的位置来判断lambda表达式的类型的。这也就意味着只能在Java编译器可以推断出类型的位置使用Lambda表达式:

  1. 变量声明;
  2. 赋值;
  3. 返回语句;
  4. 数组初始化;
  5. 方法或者构造器参数;
  6. lambda表达式方法体;
  7. 条件表达式(?:);
  8. 抛出异常时。

目标类型和方法参数

对于方法参数,Java编译器还需要依赖两个语言特性来决定目标类型:重载解析和类型参数推断。

看一下下面的这两个函数式接口( java.lang.Runnable and java.util.concurrent.Callable<V>):

public interface Runnable {
    void run();
  }

  public interface Callable<v> {
    V call();
  }

Runnable.run()方法没有返回值,而Callable.call()方法有。

假设我们像下面这样重载了invoke方法:

 void invoke(Runnable r) {
    r.run();
  }

  <t> T invoke(Callable<t> c) {
    return c.call();
  }

那么在下面的语句中将会调用哪个方法呢:

String s = invoke(() -> "done");

调用的是invoke(Callable<T>),因为这个方法有返回值,而invoke(Runnable<T>)没有返回值。在这种情况下lambda表达式(() -> “done”)的类型是Callable<T>。

序列化

如果一个lambda表达式的目标类型还有它调用的参数的类型都是可序列化的,那么lambda表达式也是可序列化的。然而就像内部类一样,强烈不建议对lambda表达式进行序列化。

相关文章

  • Springboot jar包远程调试详解

    Springboot jar包远程调试详解

    这篇文章主要为大家详细介绍了Springboot jar包远程调试,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-09-09
  • java String源码和String常量池的全面解析

    java String源码和String常量池的全面解析

    下面小编就为大家分享一篇java String源码和String常量池的全面解析,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2017-12-12
  • spring boot整合mybatis使用c3p0数据源连接mysql

    spring boot整合mybatis使用c3p0数据源连接mysql

    这篇文章主要为大家详细介绍了spring boot整合mybatis使用c3p0数据源连接mysql,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-03-03
  • java 将字符串、list 写入到文件,并读取内容的案例

    java 将字符串、list 写入到文件,并读取内容的案例

    这篇文章主要介绍了java 将字符串、list 写入到文件,并读取内容的案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09
  • java并发学习-CountDownLatch实现原理全面讲解

    java并发学习-CountDownLatch实现原理全面讲解

    这篇文章主要介绍了java并发学习-CountDownLatch实现原理全面讲解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • java字符串格式化(String类format方法)

    java字符串格式化(String类format方法)

    这篇文章主要介绍了java字符串格式化(String类format方法),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-02-02
  • java实现MD5加密的方法小结

    java实现MD5加密的方法小结

    这篇文章主要介绍了java实现MD5加密的方法,结合具体实例形式总结分析了java实现md5加密的常用操作技巧与使用方法,需要的朋友可以参考下
    2017-10-10
  • 构建多模块的Spring Boot项目步骤全纪录

    构建多模块的Spring Boot项目步骤全纪录

    这篇文章主要给大家介绍了关于如何构建多模块的Spring Boot项目的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用SpringBoot具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-05-05
  • Springboot使用redis进行api防刷限流过程详解

    Springboot使用redis进行api防刷限流过程详解

    这篇文章主要介绍了Springboot使用redis进行api防刷限流过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-12-12
  • 使用java实现BBS论坛发送邮件过程详解

    使用java实现BBS论坛发送邮件过程详解

    这篇文章主要介绍了使用java发送邮件过程详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04

最新评论