JAVA学习笔记——Lambda 表达式

目录

  • 引入
  • 表达式语法
  • 函数式接口
  • 方法引用和构造器引用
    • 方法引用
    • 构造器引用
  • 变量作用域
  • 处理 lambda 表达式

引入

Lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。在 Java 中传递一个代码段并不容易,不能直接传递代码段。Java 是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。

在其他语言中,可以直接处理代码块。Java 设计者很长时间以来一直拒绝增加这个特性。毕竟,Java 的强大之处就在于其简单性和一致性。如果只要一个特性能够让代码稍简洁一些,就把这个特性增加到语言中,这个语言很快就会变得一团糟,无法管理。不过,在另外那些语言中,并不只是创建线程或注册按钮点击事件处理器更容易;它们的大部分 API 都更简单、更一致而且更强大。在 Java 中,也可以编写类似的 API 利用类对象实现特定的功能,不过这种 API 使用可能很不方便。

就现在来说,问题已经不是是否增强 Java 来支持函数式编程,而是要如何做到这一点。设计者们做了多年的尝试,终于找到一种适合 Java 的设计——Lambda表达式。

表达式语法

基本形式:参数,箭头(->),表达式。

// 基本形式
(String first, String second) -> first.length() - second.length()

// 多表达式语句块
(String first, String second) ->
{
	if (first.length() < second.length()) 
		return -1;
	else if (first.length() > second.length()) 
		return 1;
	else 
		return 0;
}
// 没有参数
() -> { 
		for (int i = 100; i >= 0; i--) 
			System.out.println(i); 
	}

简单来说,lambda 表达式使我们更方便地传递了一段语句块,实现的功能类似于其他语言的“函数”,可选择是否传入参数,不需要定义返回值。

虽然 lambda 表达式提供了种种便利,但仍不提倡使用 lambda 表达式实现过于复杂的功能,因为这是“方法”需要完成的事情。而且 lambda 表达式能够简化代码,关注于实现核心逻辑,若功能过于复杂,则违背了 lambda 表达式的设计初衷了。

函数式接口

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式。这种接口称为函数式接口 (functional interface)。

为了展示如何转换为函数式接口,下面考虑 Arrays.sort 方法。它的第二个参数需要一个 Comparator 实例,Comparator 就是只有一个方法的接口,所以可以提供一个 lambda 表达式:

Arrays.sort (words, (first, second) -> first.length() - second.length()) ;

在底层,Arrays.sort 方法会接收实现了 Comparator 的某个类的对象。在这个对象上调用 compare 方法会执行这个 lambda 表达式的体。这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效得多。

ArrayList 类有一个 removelf 方法,它的参数就是一个 Predicate。这个接口专门用来传递 lambda 表达式。例如,下面的语句将从一个数组列表删除所有 null 值:list.removelf(e -> e == null);

方法引用和构造器引用

方法引用

有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如,假设你希望只要出现一个定时器事件就打印这个事件对象。

// lambda 表达式
Timer t = new Timer(1000, event -> System.out.println(event));
// 方法引用
Timer t = new Timer(1000, System.out::println);

表达式 System.out::println 是一个方法引用 (method reference),它等价于 lambda 表达式 x -> System.out.println(x)

使用 :: 操作符可以分隔方法名与对象或类名,主要有以下三种情况:

  1. object::instanceMethod
  2. Class::staticMethod
  3. Class::instanceMethod

在前 2 种情况中,方法引用等价于提供方法参数的 lambda 表达式。前面已经提到,System.out::println 等价于 x -> System.out.println(x)。 类似地,Math::pow 等价于(x, y) -> Math.pow(x, y)

对于第 3 种情况,第 1 个参数会成为方法的目标。例如,String::compareToIgnoreCase 等同于 (x, y) -> x.compareToIgnoreCase(y)

可以在方法引用中使用 this 参数,例如:this::equals 等同于 x -> this.equals(x)。使用 super 也是合法的,例如:super::instanceMethod

构造器引用

构造器引用与方法引用很类似,只不过方法名为 new。例如,Person::newPerson 构造器的一个引用。

可以用数组类型建立构造器引用。例如,int[]::new 是一个构造器引用,它有一个参数:即数组的长度。这等价于 lambda 表达式 x -> new int[x]

Java 有一个限制,无法构造泛型类型 T 的数组。数组构造器引用对于克服这个限制很有用。表达式 new T[n] 会产生错误,因为这会改为 new Object[n]

假设我们需要一个 Person 对象数组。Stream 接口有一个 toArray 方法可以返回 Object 数组:

Object[] people = stream.toArray();

不过,这并不让人满意。用户希望得到一个 Person 引用数组,而不是 Object 引用数组。流库利用构造器引用解决了这个问题。可以把 Person[]::new 传入 toArray 方法:

Person口 people = stream.toArray(Person[]::new);

toArray 方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。

变量作用域

lambda 表达式也可以访问表达式以外的变量:

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);

在上例中,text 不是 lambda 表达式的参数,而是静态方法 repeatMessage 的参数。而当 lambda 表达式开始回调时,静态方法可能已经结束,此时 text 变量已经不存在,从而产生错误。

可以看到,lambda 表达式可以捕获外围作用域中变量的值。 在 Java 中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在 lambda 表达式中,只能引用值不会改变的变量。

之所以有这个限制的原因在于如果在 lambda 表达式中改变变量,并发执行多个动作时就会不安全。另外如果在 lambda 表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。

这里有一条规则:lambda 表达式中捕获的变量必须实际上是最终变量 (effectively final) 实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。

public class ApplicationO
{
	public void init()
	{
		ActionListener listener * event ->
		{
			System.out.print n(this.toString());
			...
		}
	...
	}
}

在一个 lambda 表达式中使用 this 关键字时,是指创建这个 lambda 表达式的方法的 this 参数。表达式 this.toString() 会调用 Application 对象的 toString 方法,而不是 ActionListener 实例的方法。

处理 lambda 表达式

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

  • 在一个单独的线程中运行代码
  • 多次运行代码
  • 在算法的适当位置运行代码(例如,排序中的比较操作)
  • 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等)
  • 只在必要时才运行代码
public interface IntConsumer
{
	void accept(int value);
}
public static void repeat(int n, IntConsumer action)
{
	for (int i = 0; i < n; i++) action.accept(i);
}

repeat(10, i-> System.out.println("Countdown: " + (9 - i));

上述程序利用一个函数式接口,实现了告诉我们每次调用 lambda 表达式在第几次迭代中。接下来分析上述代码:

  1. repeat 定义了一个函数式接口,调用 repeat 后,传入lambda 表达式。
  2. repeat 内部,用 for 循环调用 action.accept(i)
  3. 此时 action 对应的是一个 lambda 表达式,同时也是一个函数式接口,只有一个参数 i
  4. 调用 action.accept(i),将参数 i 传入 lambda 表达式,执行 System.out.println("Countdown: " + (9 - i)
  5. 一直循环,当跳出 for 循环后,repeat 方法结束,程序结束。
表1 常用的函数式接口

JAVA学习笔记——Lambda 表达式_第1张图片

表2 基本类型的函数式接口

JAVA学习笔记——Lambda 表达式_第2张图片


参考资料

  1. 《Java核心技术 卷1 基础知识》
  2. 《Lambda表达式详解》

你可能感兴趣的:(JAVA学习笔记,java,编程语言)