Java基础系列(二十七):Lambda进阶

时间:2022-12-13 20:06:20

变量作用域

我们首先来看一个栗子:

public static void repeatMessage(String text, int delay) {
    ActionListener listener = event -> {
        System.out.println(text);
        Toolkit.getDefaultToolkit().beep();
    };
    new Timer(delay, listener).start();
}

如果我们现在去调用这个方法:

repeatMessage("Hello", 1000);

现在我们回过头去看看那个lambda表达式中的变量text。注意这个变量并不是在这个lambda表达式中定义的。实际上,这时 repeatMessage 方法的一个参数变量。

这里似乎有些问题,因为lambda表达式的代码可能会在 repeatMessage 调用返回很久以后才运行,而那时这个参数变量已经不存在了。如何保留text变量呢?

我们首先来回顾一下lambda表达式中有哪几个部分:

  1. 一个代码块
  2. 参数
  3. *变量的值,这里是指非参数而且不在代码中定义的变量。

在我们的例子中,lambda表达式有一个*变量text。表示lambda表达式的数据结构必须存储*变量的值,在这里就是字符串 “Hello” 。我们说它被lambda表达式捕获。

这里需要注意,在Java中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在lambda表达式中,只能引用值不会改变的变量。下面是一个错误的示例:

public static void count(int start, int delay) {
    Actionlistener listener = event -> {
        start--;
        System.out.println(start);
    };
    new Timer(delay, listener).start();
}

如果我们在lambda表达式中改变变量,并发执行多个操作时会不安全,具体的原因我们会在并发的课程中去详细的讲解。

另外如果在lambda表达式中引用变量,而这个变量可能在外部改变,这样也是不合法的:

public static void repeat(String text, int count) {
    for (int i = 1; i <= count; i++) {
        ActionListener listener = event -> {
            System.out.println(i + ": " + text);
        }
        new Timer(1000, listener).start();
    }
}

总而言之,lambda表达式中捕获的变量必须实际上是最终变量,这个最终变量是指,这个变量在初始化之后就不会再为它赋新值。

lambda表达式的体和嵌套块有相同的作用域。这里同样使用命名冲突和遮蔽的有关规则。在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。

Path first = Paths.get("/usr/bin");
Comparator<String> comp = (first, second) -> first.length() - second.length();

在方法中,不能有两个同名的局部变量,因此,lambda表达式中同样也不能有同名的局部变量。

在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。

public class Application() {
    public void init() {
        ActionListener listener = event -> {
            System.out.println(this.toString());
        }
    }
}

表达式this.toString()会调用Application对象的toString方法,而不是ActionListener实例的方法。在lambda表达式中,this的使用并没有任何特殊之处。lambda表达式的作用域嵌套在init方法中,与出现在这个方法中的其他位置一样,lambda表达式中this的含义并没有变化。

处理lambda表达式

使用lambda表达式的重点是延迟执行。毕竟,如果想要立即执行代码,完全可以直接执行,而无需把它包装在一个lambda表达式中。之所以希望以后再执行代码,这有很多原因,如:

  1. 在一个单独的线程中运行代码
  2. 多次运行代码
  3. 在算法的适当位置运行代码
  4. 发生某种情况时执行代码
  5. 只在必要时才会去运行代码

假如我们想要重复一个动作n次,将这个动作和重复次数传递到一个repeat方法:

repeat(10, () -> System.out.println("Hello World!"));

要接受这个lambda表达式,需要选择(偶尔可能需要提供)一个函数式接口。下表中列出了 Java API 中提供的最重要的函数式接口。经过比较,我们可以选用Runnable接口:

public static void repeat(int n, Runnable action) {
    for (int i = 0; i < n; i++) {
        action.run();
    }
}

当我们调用action.run()时会执行这个lambda表达式的主体。

函数式接口 参数类型 返回类型 抽象方法名 描述 其他方法
Runnable void run 作为无参数或返回值的动作进行
Supplier<T> T get 提供一个T类型的值
Consumer<T> T void accpet 处理一个T类型的值 andThen
BiConsumer<T,U> T,U void accpet 处理一个T和U类型的值 andThen
Function<T,R> T R apply 有一个T类型参数的函数 compose,andThen,identity
BiFunction<T,U,R> T,U R apply 有T和U类型的参数 andThen
UnaryOperator<T> T T apply 类型T上的一元操作符 compose,andThen,identity
BinaryOperator<T> T,T T apply 类型T的二元操作符 andThen,maxBy,minBy
Predicate<T> T boolean test 布尔值函数 and,or,negate,isEqual
BiPrediacte<T,U> T,U boolean test 有两个参数的布尔值函数 and,or,negate

如果我们需要让这些例子显得更为立体和实用(比如去接收一些基本类型的值),我们需要去选择一个更为合适的函数式接口,那么我们可以从以下的列表中去选用适当的函数式接口去接收对应参数类型的基本数据类型。

函数式接口 参数类型 返回类型 抽象方法名
BooleanSupplier none boolean getAsBoolean
PSupplier none p getAsP
PConsumer p void accept
ObjPConsumer<T> T,p void accept
PFunction<T> p T apply
PToQFunction p q applyAsQ
ToPFunction<T> T p applyAsP
ToPBiFunction<T,U> T,U p applyAsP
PUnaryOperator p p applyAsP
PBinaryOperator p,p p applyAsP
PPredicate p boolean test

PS:
p,q 为 int,long,double;P,Q 为Int,Long,Double


公众号

扫码或微信搜索 Vi的技术博客,关注公众号,不定期送书活动各种福利~

Java基础系列(二十七):Lambda进阶