上下文与作用域

上下文

Lambda 表达式只能在以下四种情况下使用:Assignment Context、Method Invocation Context、return Context、Cast Context。

Assignment Context

在赋值运算符的右边可以出现一个 Lambda 表达式。

public class Main {
  public static void main(String[] argv) {
    Calculator iCal = (x,y)-> x + y;//from   www.j  a  v a  2s .c o  m
    System.out.println(iCal.calculate(1, 2));
  }
}

@FunctionalInterface
interface Calculator{
  int calculate(int x, int y);
}

Method Invocation Context

我们可以使用 Lambda 表达式作为方法或构造函数的参数。

public class Main {
  public static void main(String[] argv) {
    engine((x,y)-> x / y);//  www  .  j  a v a2s . co m
  }
  private static void engine(Calculator calculator){
    long x = 2, y = 4;
    long result = calculator.calculate(x,y);
    System.out.println(result);
  }
}

@FunctionalInterface
interface Calculator{
  long calculate(long x, long y);
}

Return Context

我们可以在返回语句中使用 Lambda 表达式,其目标类型在方法返回类型中声明。

public class Main {
  public static void main(String[] argv) {
    System.out.println(create().calculate(2, 2));
  }//  ww w .j  a v  a2s  .  com
  private static Calculator create(){
    return (x,y)-> x / y;
  }
}

@FunctionalInterface
interface Calculator{
  long calculate(long x, long y);
}

Cast Context

我们可以使用一个 lambda 表达式,cast 指定的类型是其目标类型。

public class Main {
  public static void main(String[] argv) {
    engine((IntCalculator) ((x,y)-> x + y));
  }
  private static void engine(IntCalculator calculator){
    int x = 2, y = 4;
    int result = calculator.calculate(x,y);
    System.out.println(result);
  }
  private static void engine(LongCalculator calculator){
    long x = 2, y = 4;
    long result = calculator.calculate(x,y);
    System.out.println(result);
  }
}

@FunctionalInterface
interface IntCalculator{
  int calculate(int x, int y);
}

@FunctionalInterface
interface LongCalculator{
  long calculate(long x, long y);
}

Variable Scope | 变量作用域

在 Lambda 中,变量的作用域与访问操作主要遵循以下规则:

  • 本地变量(Local Variable)可以访问但是不可以修改。
  • 类成员变量与静态变量可以被读写,即闭包中的 this 实际指向的是创建该 Lambda 表达式的方法的 this 参数。
  • 函数式接口的默认方法不可以在 Lambda 表达式中被访问。

局部变量

lambda 表达式的方法体与嵌套代码块有着相同的作用域。因此它也适用同样的命名冲突和屏蔽规则。在 lambda 表达式中不允许声明一个与局部变量同名的参数或者局部变量。

Path first = Paths.get("/usr/bin");
Comparator<String> comp = (first,second) ->
    Integer.compare(first.length(),second.length());
//错误,变量first已经定义了

在一个方法里,你不能有两个同名的局部变量,因此,你也不能在 lambda 表达式中引入这样的变量。在下一个示例中,lambda 表达式有两个自由变量,text 和 count。数据结构表示 lambda 表达式必须存储这两个变量的值,即“Hello”和 20。我们可以说,这些值已经被 lambda 表达式捕获了(这是一个技术实现的细节。例如,你可以将一个 lambda 表达式转换为一个只含一个方法的对象,这样自由变量的值就会被复制到该对象的实例变量中)。

public class T1 {

  public static void main(String[] args) {
    repeatMessage("Hello", 20);
  }

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

this

当你在 lambda 表达式中使用 this 关键字,你会引用创建该 lambda 表达式的方法的 this 参数,以下面的代码为例:

public class Application {

  public void doWork() {
    Runnable runner =
      () -> {
        System.out.println(this.toString());
      };
  }
}

表达式 this.toString()会调用 Application 对象的 toString()方法,而不是 Runnable 实例的 toString()方法。在 lambda 表达式中使用 this,与在其他地方使用 this 没有什么不同。lambda 表达式的作用域被嵌套在 doWork()方法中,并且无论 this 位于方法的何处,其意义都是一样的。

引用的变量不可更改

Lambda 表达式可以捕获闭合作用域中的变量值。在 Java 中,为了确保被捕获的值是被良好定义的,需要遵守一个重要的约束。在 lambda 表达式中,被引用的变量的值不可以被更改。例如,下面这个表达式是不合法的:

public static void repeatMessage(String text,int count){
    Runnable r = () -> {
        while(count > 0){
            count--;        //错误,不能更改已捕获变量的值
            System.out.println(text);
            Thread.yield();
         }
     };
     new Thread(r).start();
}

做出这个约束是有原因的。更改 lambda 表达式中的变量不是线程安全的。假设有一系列并发的任务,每个线程都会更新一个共享的计数器。

int matches = 0;
for(Path p : files)
    new Thread(() -> {if(p中包含某些属性) matches++;}).start();    //非法更改matches的值

如果这段代码是合法的,那么会引起十分糟糕的结果。自增操作 matches++不是原子操作,如果多个线程并发执行该自增操作,天晓得会发生什么。不要指望编译器会捕获所有并发访问错误。不可变的约束只作用在局部变量上,如果 matches 是一个实例变量或者闭合类的静态变量,那么不会有任何错误被报告出来即使结果同样未定义。同样,改变一个共享对象也是完全合法的,即使这样并不恰当。例如:

List<Path> matches = new ArrayList<>();
for(Path p: files)
    // 你可以改变matches的值,但是在多线程下是不安全的
    new Thread(() -> {if(p中包含某些属性) matches.add(p);}).start();

注意 matches 是“有效 final”的(一个有效的 final 变量被初始化后,就永远不会再被赋一个新值的变量)。在我们的示例中,matches 总是引用同一个 ArrayList 对象,但是,这个对象是可变的,因此是线程不安全的。如果多个线程同时调用 add 方法,结果将无法预测。

上一页